From eb5cada54e2bc403ab7316500ca8edbe58c813da Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sat, 7 Sep 2024 03:46:55 +0800 Subject: [PATCH 1/9] Add `gem "grape"` in Gemfile --- Gemfile | 3 +++ Gemfile.lock | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Gemfile b/Gemfile index c8615d8..3a10596 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,9 @@ gem "csv" # sql LIKE search gem "ransack" +# API +gem "grape" + # Use Redis adapter to run Action Cable in production # gem "redis", ">= 4.0.1" diff --git a/Gemfile.lock b/Gemfile.lock index 227e5cc..d609f9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,6 +107,21 @@ GEM irb (~> 1.10) reline (>= 0.3.8) drb (2.2.1) + dry-core (1.0.1) + concurrent-ruby (~> 1.0) + zeitwerk (~> 2.6) + dry-inflector (1.1.0) + dry-logic (1.5.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-types (1.7.2) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) erubi (1.13.0) globalid (1.2.1) activesupport (>= 6.1) @@ -116,6 +131,12 @@ GEM google-protobuf (4.28.0-x86_64-linux) bigdecimal rake (>= 13) + grape (2.1.3) + activesupport (>= 6) + dry-types (>= 1.1) + mustermann-grape (~> 1.1.0) + rack (>= 2) + zeitwerk i18n (1.14.5) concurrent-ruby (~> 1.0) importmap-rails (2.0.1) @@ -145,6 +166,10 @@ GEM mini_mime (1.1.5) minitest (5.25.1) msgpack (1.7.2) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + mustermann-grape (1.1.0) + mustermann (>= 1.0.0) net-imap (0.4.16) date net-protocol @@ -247,6 +272,7 @@ GEM rubocop-performance rubocop-rails ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) rubyzip (2.3.2) sass-embedded (1.78.0-aarch64-linux-gnu) google-protobuf (~> 4.27) @@ -308,6 +334,7 @@ DEPENDENCIES csv dartsass-rails debug + grape importmap-rails jbuilder puma (>= 5.0) From 18a32da34269d7a8920aecf93d428a2073d3ce16 Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 00:15:46 +0800 Subject: [PATCH 2/9] Set `inflect.acronym "API"` in config/initializers/inflections.rb --- config/initializers/inflections.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 9e049dc..ddbc68e 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -13,6 +13,6 @@ # end # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym "RESTful" -# end +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym "API" +end From 4986c0b88da90b16d592f4e8415799159b80901d Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 00:16:31 +0800 Subject: [PATCH 3/9] Create basic api files in app/api --- app/api/application_api.rb | 8 ++++++++ app/api/v1/base.rb | 9 +++++++++ app/api/v1/fey_api.rb | 36 ++++++++++++++++++++++++++++++++++++ app/api/v1/poinsot_api.rb | 36 ++++++++++++++++++++++++++++++++++++ app/api/v1/safolu_api.rb | 36 ++++++++++++++++++++++++++++++++++++ app/api/v2/base.rb | 35 +++++++++++++++++++++++++++++++++++ app/api/v2/searches_api.rb | 14 ++++++++++++++ app/api/v2/terms_api.rb | 14 ++++++++++++++ 8 files changed, 188 insertions(+) create mode 100644 app/api/application_api.rb create mode 100644 app/api/v1/base.rb create mode 100644 app/api/v1/fey_api.rb create mode 100644 app/api/v1/poinsot_api.rb create mode 100644 app/api/v1/safolu_api.rb create mode 100644 app/api/v2/base.rb create mode 100644 app/api/v2/searches_api.rb create mode 100644 app/api/v2/terms_api.rb diff --git a/app/api/application_api.rb b/app/api/application_api.rb new file mode 100644 index 0000000..2da4baa --- /dev/null +++ b/app/api/application_api.rb @@ -0,0 +1,8 @@ +class ApplicationAPI < Grape::API + content_type :text, "text/plain" + content_type :json, "application/json" + default_format :json + + mount V1::Base + mount V2::Base +end diff --git a/app/api/v1/base.rb b/app/api/v1/base.rb new file mode 100644 index 0000000..e0b0cfc --- /dev/null +++ b/app/api/v1/base.rb @@ -0,0 +1,9 @@ +module V1 + class Base < ApplicationAPI + version :v1, using: :path + + mount FeyAPI + mount PoinsotAPI + mount SafoluAPI + end +end diff --git a/app/api/v1/fey_api.rb b/app/api/v1/fey_api.rb new file mode 100644 index 0000000..eeaf69d --- /dev/null +++ b/app/api/v1/fey_api.rb @@ -0,0 +1,36 @@ +module V1 + class FeyAPI < Base + resources :p do + params do + requires :name, type: String, desc: "詞彙,對應 Term#name" + end + get ":name" do + { + h: [ + { + name: params[:name], + d: [ + { + f: "String, required", + e: [ + "String" + ], + s: [ + "String" + ], + r: [ + "String" + ], + type: "String, optional" + } + ] + } + ], + t: "String, required", + stem: "String, optional", + tag: "String, optional" + } + end + end + end +end diff --git a/app/api/v1/poinsot_api.rb b/app/api/v1/poinsot_api.rb new file mode 100644 index 0000000..cfbbf94 --- /dev/null +++ b/app/api/v1/poinsot_api.rb @@ -0,0 +1,36 @@ +module V1 + class PoinsotAPI < Base + resources :m do + params do + requires :name, type: String, desc: "詞彙,對應 Term#name" + end + get ":name" do + { + h: [ + { + name: params[:name], + d: [ + { + f: "String, required", + e: [ + "String" + ], + s: [ + "String" + ], + r: [ + "String" + ], + type: "String, optional" + } + ] + } + ], + t: "String, required", + stem: "String, optional", + tag: "String, optional" + } + end + end + end +end diff --git a/app/api/v1/safolu_api.rb b/app/api/v1/safolu_api.rb new file mode 100644 index 0000000..c763478 --- /dev/null +++ b/app/api/v1/safolu_api.rb @@ -0,0 +1,36 @@ +module V1 + class SafoluAPI < Base + resources :s do + params do + requires :name, type: String, desc: "詞彙,對應 Term#name" + end + get ":name" do + { + h: [ + { + name: params[:name], + d: [ + { + f: "String, required", + e: [ + "String" + ], + s: [ + "String" + ], + r: [ + "String" + ], + type: "String, optional" + } + ] + } + ], + t: "String, required", + stem: "String, optional", + tag: "String, optional" + } + end + end + end +end diff --git a/app/api/v2/base.rb b/app/api/v2/base.rb new file mode 100644 index 0000000..1a5defb --- /dev/null +++ b/app/api/v2/base.rb @@ -0,0 +1,35 @@ +module V2 + class Base < ApplicationAPI + version :v2, using: :path + + helpers do + def fail400(data: nil) + error!({ status: "fail", data: data }, 400) + end + + def fail403(data: nil) + error!({ status: "fail", data: data }, 403) + end + + def fail404(data: nil) + error!({ status: "fail", data: data }, 404) + end + + def success200(data: nil) + { status: "success", data: data } + end + + def error500(error: nil, message: "something_went_wrong!") + if Rails.env.development? && error.present? + env["api.format"] = :txt + error!("Grape caught this error: #{error.message} (#{error.class})\n#{error.backtrace.join("\n")}") + else + error!({ status: "error", message: message }, 500) + end + end + end + + mount TermsAPI + mount SearchesAPI + end +end diff --git a/app/api/v2/searches_api.rb b/app/api/v2/searches_api.rb new file mode 100644 index 0000000..a8854d7 --- /dev/null +++ b/app/api/v2/searches_api.rb @@ -0,0 +1,14 @@ +module V2 + class SearchesAPI < Base + resources :searches do + params do + requires :q, type: String, desc: "搜尋關鍵字" + end + get ":q" do + { + name: params[:q] + } + end + end + end +end diff --git a/app/api/v2/terms_api.rb b/app/api/v2/terms_api.rb new file mode 100644 index 0000000..74c9374 --- /dev/null +++ b/app/api/v2/terms_api.rb @@ -0,0 +1,14 @@ +module V2 + class TermsAPI < Base + resources :terms do + params do + requires :name, type: String, desc: "詞彙,對應 Term#name" + end + get ":name" do + { + name: params[:name] + } + end + end + end +end From 5809d50c4fe3c711e3f34351afd6f92641e48c5b Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 00:17:36 +0800 Subject: [PATCH 4/9] Add `mount ApplicationAPI => "/api"` in config/routes and add grape:routes tasks --- config/routes.rb | 4 ++++ lib/tasks/grape.rake | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 lib/tasks/grape.rake diff --git a/config/routes.rb b/config/routes.rb index f8a374e..2e41c1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,10 @@ Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + # Use command below to check grape routes + # $ bin/rails grape:routes + mount ApplicationAPI => "/api" + resources :terms, only: %i[index show] # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake new file mode 100644 index 0000000..57b10de --- /dev/null +++ b/lib/tasks/grape.rake @@ -0,0 +1,42 @@ +# from: https://github.com/syedmusamah/grape_on_rails_routes/blob/master/lib/tasks/grape.rake +namespace :grape do + desc "show API routes" + task routes: :environment do + mapping = method_mapping + + grape_klasses = ObjectSpace.each_object(Class).select { |klass| klass < Grape::API } + routes = grape_klasses.flat_map(&:routes) + .uniq do |r| + r.send(mapping[:path]) + r.send(mapping[:method]).to_s + end + + method_width, path_width, version_width, desc_width = widths(routes, mapping) + + puts " #{"Verb".rjust(method_width)} | #{"URI".ljust(path_width)} | #{"Ver".ljust(version_width)} | #{"Description".ljust(desc_width)}" + routes.each do |api| + method = api.send(mapping[:method]).to_s.rjust(method_width) + path = api.send(mapping[:path]).to_s.ljust(path_width) + version = api.send(mapping[:version]).to_s.ljust(version_width) + desc = api.send(mapping[:description]).to_s.ljust(desc_width) + puts " #{method} | #{path} | #{version} | #{desc}" + end + end + + def widths(routes, mapping) + [ + routes.map { |r| r.send(mapping[:method]).try(:length) }.compact.max || 0, + routes.map { |r| r.send(mapping[:path]).try(:length) }.compact.max || 0, + routes.map { |r| r.send(mapping[:version]).try(:length) }.compact.max || 0, + routes.map { |r| r.send(mapping[:description]).try(:length) }.compact.max || 0 + ] + end + + def method_mapping + { + method: "request_method", + path: "path", + version: "version", + description: "description" + } + end +end From 435bb6f406b392c9a53a149481d89cbc0b729f71 Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 10:22:17 +0800 Subject: [PATCH 5/9] Complete GET /api/v1/s/:name API --- app/api/v1/safolu_api.rb | 50 +++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/app/api/v1/safolu_api.rb b/app/api/v1/safolu_api.rb index c763478..2f54ca7 100644 --- a/app/api/v1/safolu_api.rb +++ b/app/api/v1/safolu_api.rb @@ -2,34 +2,32 @@ module V1 class SafoluAPI < Base resources :s do params do - requires :name, type: String, desc: "詞彙,對應 Term#name" + requires :name, type: String, desc: "蔡中涵大辭典詞彙,對應 Term#name" end get ":name" do - { - h: [ - { - name: params[:name], - d: [ - { - f: "String, required", - e: [ - "String" - ], - s: [ - "String" - ], - r: [ - "String" - ], - type: "String, optional" - } - ] - } - ], - t: "String, required", - stem: "String, optional", - tag: "String, optional" - } + dictionary = Dictionary.find_by(name: "蔡中涵大辭典") + term = dictionary.terms.includes(:stem, descriptions: %i[examples synonyms]).find_by(name: params[:name]) + + if term.present? + result = { t: term.lower_name } + result[:stem] = term.stem.name if term.stem.present? + result[:tag] = "[疊 #{term.repetition}]" if term.repetition.present? + + result[:h] = [] + result[:h][0] = { d: [] } + result[:h][0][:name] = term.name if term.lower_name != term.name + term.descriptions.each do |description| + description_hash = { f: description.content } + description_hash[:e] = description.examples.map(&:content) if description.examples.present? + description_hash[:s] = description.synonyms.alts.map(&:content) if description.synonyms.alts.present? + description_hash[:r] = description.synonyms.refs.map(&:content) if description.synonyms.refs.present? + result[:h][0][:d] << description_hash + end + + result + else + { term: :not_found } + end end end end From dcd0582623174047af292011ac1abcf6f4276da6 Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 10:26:51 +0800 Subject: [PATCH 6/9] Complete GET /api/v1/p/:name API --- app/api/v1/fey_api.rb | 48 ++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/app/api/v1/fey_api.rb b/app/api/v1/fey_api.rb index eeaf69d..c7ee7a9 100644 --- a/app/api/v1/fey_api.rb +++ b/app/api/v1/fey_api.rb @@ -2,34 +2,30 @@ module V1 class FeyAPI < Base resources :p do params do - requires :name, type: String, desc: "詞彙,對應 Term#name" + requires :name, type: String, desc: "方敏英字典詞彙,對應 Term#name" end get ":name" do - { - h: [ - { - name: params[:name], - d: [ - { - f: "String, required", - e: [ - "String" - ], - s: [ - "String" - ], - r: [ - "String" - ], - type: "String, optional" - } - ] - } - ], - t: "String, required", - stem: "String, optional", - tag: "String, optional" - } + dictionary = Dictionary.find_by(name: "方敏英字典") + term = dictionary.terms.includes(:stem, descriptions: %i[examples synonyms]).find_by(name: params[:name]) + + if term.present? + result = { t: term.lower_name } + result[:stem] = term.stem.name if term.stem.present? + + result[:h] = [] + result[:h][0] = { d: [] } + result[:h][0][:name] = term.name if term.lower_name != term.name + term.descriptions.each do |description| + description_hash = { f: description.content } + description_hash[:e] = description.examples.map(&:content) if description.examples.present? + description_hash[:s] = description.synonyms.alts.map(&:content) if description.synonyms.alts.present? + result[:h][0][:d] << description_hash + end + + result + else + { term: :not_found } + end end end end From f4d046489c3e0f9572f0f78aae16dd052eeb28c6 Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 10:36:09 +0800 Subject: [PATCH 7/9] Complete GET /api/v1/m/:name API --- app/api/v1/poinsot_api.rb | 49 ++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/app/api/v1/poinsot_api.rb b/app/api/v1/poinsot_api.rb index cfbbf94..5004e56 100644 --- a/app/api/v1/poinsot_api.rb +++ b/app/api/v1/poinsot_api.rb @@ -2,34 +2,31 @@ module V1 class PoinsotAPI < Base resources :m do params do - requires :name, type: String, desc: "詞彙,對應 Term#name" + requires :name, type: String, desc: "博利亞潘世光阿法字典詞彙,對應 Term#name" end get ":name" do - { - h: [ - { - name: params[:name], - d: [ - { - f: "String, required", - e: [ - "String" - ], - s: [ - "String" - ], - r: [ - "String" - ], - type: "String, optional" - } - ] - } - ], - t: "String, required", - stem: "String, optional", - tag: "String, optional" - } + dictionary = Dictionary.find_by(name: "博利亞潘世光阿法字典") + term = dictionary.terms.includes(:stem, descriptions: %i[examples synonyms]).find_by(name: params[:name]) + console + if term.present? + result = { t: term.lower_name } + result[:stem] = term.stem.name if term.stem.present? + + result[:h] = [] + result[:h][0] = { d: [] } + result[:h][0][:name] = term.name if term.lower_name != term.name + term.descriptions.each do |description| + description_hash = { f: description.content } + description_hash[:e] = description.examples.map(&:content) if description.examples.present? + description_hash[:s] = description.synonyms.alts.map(&:content) if description.synonyms.alts.present? + description_hash[:type] = description.description_type if description.description_type.present? + result[:h][0][:d] << description_hash + end + + result + else + { term: :not_found } + end end end end From bf3dcbc0313a48d9c780a23c46522cba3a2b5322 Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 10:59:45 +0800 Subject: [PATCH 8/9] Complete GET /api/v2/terms/:name API --- app/api/v2/terms_api.rb | 61 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/app/api/v2/terms_api.rb b/app/api/v2/terms_api.rb index 74c9374..f83acac 100644 --- a/app/api/v2/terms_api.rb +++ b/app/api/v2/terms_api.rb @@ -2,12 +2,65 @@ module V2 class TermsAPI < Base resources :terms do params do - requires :name, type: String, desc: "詞彙,對應 Term#name" + requires :name, type: String, desc: "所有字典的詞彙,對應 Term#name" end get ":name" do - { - name: params[:name] - } + terms = Term.includes(:dictionary, :stem, descriptions: %i[examples synonyms]).where(name: params[:name]) + + if terms.exists? + result = [] + + terms.each do |term| + term_hash = { + dictionary: term.dictionary.name, + dialect: term.dictionary.dialect, + name: term.name, + is_stem: term.is_stem, + descriptions: [] + } + + term_hash[:stem] = term.stem.name if term.stem.present? + term_hash[:lower_name] = term.lower_name if term.name != term.lower_name + term_hash[:repetition] = term.repetition if term.repetition.present? + term_hash[:audio] = term.audio if term.audio.present? + + term.descriptions.each do |description| + description_hash = { + content: description.content + } + description_hash[:type] = description.description_type if description.description_type.present? + description_hash[:glossary_serial] = description.glossary_serial if description.glossary_serial.present? + description_hash[:glossary_level] = description.glossary_level if description.glossary_level.present? + description_hash[:image] = description.image if description.image.present? + + description.examples.each do |example| + example_hash = { content: example.content } + example_hash[:content_zh] = example.content_zh if example.content_zh.present? + + description_hash[:examples] ||= [] + description_hash[:examples] << example_hash + end + + description.synonyms.each do |synonym| + synonym_hash = { + term_type: synonym.term_type, + content: synonym.content + } + + description_hash[:synonyms] ||= [] + description_hash[:synonyms] << synonym_hash + end + + term_hash[:descriptions] << description_hash + end + + result << term_hash + end + + result + else + { term: :not_found } + end end end end From 4ea50b858721dc3496651eb8e407fee43407be8f Mon Sep 17 00:00:00 2001 From: wildjcrt Date: Sun, 8 Sep 2024 14:11:19 +0800 Subject: [PATCH 9/9] Complete to search Mandarin in GET /api/v2/searches/:q API --- app/api/v2/searches_api.rb | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/api/v2/searches_api.rb b/app/api/v2/searches_api.rb index a8854d7..797f3fe 100644 --- a/app/api/v2/searches_api.rb +++ b/app/api/v2/searches_api.rb @@ -2,12 +2,30 @@ module V2 class SearchesAPI < Base resources :searches do params do - requires :q, type: String, desc: "搜尋關鍵字" + requires :q, type: String, desc: "搜尋族語/漢語關鍵字,族語 1~3 字使用精確搜尋,超過 3 字用 sql LIKE 搜尋。漢語一律用 sql LIKE 搜尋 Description#content。" end get ":q" do - { - name: params[:q] - } + result = [] + + if params[:q].match?(/\A[a-zA-Z'’ʼ^]+\z/) # 族語搜尋 + case params[:q].size + when 1, 2, 3 + Term.includes(:descriptions).select(:id, :name).where(lower_name: params[:q]).group(:name).each do |term| + result << { term: term.name, description: term.short_description } + end + else + Term.select(:id, :name).ransack(lower_name_cont: params[:q]).result.group(:name).each do |term| + result << { term: term.name, description: term.short_description } + end + end + else # 漢語搜尋 + term_ids = Description.ransack(content_cont: params[:q]).result.pluck(:term_id) + Term.includes(:descriptions).select(:id, :name).where(id: term_ids).group(:name).each do |term| + result << { term: term.name, description: term.short_description } + end + end + + result.sort_by { |element| element[:term].size } end end end