add sync API with swagger documentation at /api
CI / scan_ruby (push) Successful in 23s
CI / scan_js (push) Successful in 15s
CI / lint (push) Successful in 22s
CI / test (push) Successful in 47s

This commit is contained in:
2026-01-31 22:39:12 +01:00
parent fa36305244
commit 4fe95ca538
7 changed files with 199 additions and 8 deletions
+3
View File
@@ -18,6 +18,9 @@ gem "turbo-rails"
gem "stimulus-rails"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"
gem "grape"
gem "grape-swagger"
gem "rswag-ui"
gem "caxlsx"
gem "caxlsx_rails"
+51
View File
@@ -120,6 +120,26 @@ GEM
docile (1.4.1)
dotenv (3.2.0)
drb (2.2.3)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.2.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.3.1)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-types (1.9.0)
bigdecimal (>= 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
ed25519 (1.4.0)
erb (6.0.1)
erubi (1.13.1)
@@ -131,6 +151,15 @@ GEM
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
grape (3.1.1)
activesupport (>= 7.1)
dry-configurable
dry-types (>= 1.1)
mustermann-grape (~> 1.1.0)
rack (>= 2)
zeitwerk
grape-swagger (2.1.3)
grape (>= 1.7, < 4.0)
htmlentities (4.4.2)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
@@ -181,6 +210,10 @@ GEM
minitest (6.0.1)
prism (~> 1.5)
msgpack (1.8.0)
mustermann (3.0.4)
ruby2_keywords (~> 0.0.1)
mustermann-grape (1.1.0)
mustermann (>= 1.0.0)
net-imap (0.6.2)
date
net-protocol
@@ -273,6 +306,9 @@ GEM
logger (~> 1)
nokogiri (~> 1)
rubyzip (>= 3.0.0, < 4.0.0)
rswag-ui (2.17.0)
actionpack (>= 5.2, < 8.2)
railties (>= 5.2, < 8.2)
rubocop (1.84.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
@@ -305,6 +341,7 @@ GEM
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (3.2.2)
securerandom (0.4.1)
selenium-webdriver (4.40.0)
@@ -387,6 +424,8 @@ DEPENDENCIES
caxlsx
caxlsx_rails
debug
grape
grape-swagger
image_processing (~> 1.2)
importmap-rails
jbuilder
@@ -395,6 +434,7 @@ DEPENDENCIES
puma (>= 5.0)
rails (~> 8.1.2)
roo (~> 3.0)
rswag-ui
rubocop-rails-omakase
selenium-webdriver
simplecov
@@ -445,6 +485,11 @@ CHECKSUMS
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
dry-types (1.9.0) sha256=7b656fe0a78d2432500ae1f29fefd6762f5a032ca7000e4f36bc111453d45d4d
ed25519 (1.4.0) sha256=16e97f5198689a154247169f3453ef4cfd3f7a47481fde0ae33206cdfdcac506
erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
@@ -452,6 +497,8 @@ CHECKSUMS
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
grape (3.1.1) sha256=774f16782d917a90e69de0499dfaab571e5ad967569ac066a2b0b918af12de69
grape-swagger (2.1.3) sha256=9ee955ada77c10ea2f2d2da5edcc4fc3cddcc40a6936f1b2558ee7b44e0f1153
htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
@@ -472,6 +519,8 @@ CHECKSUMS
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
minitest (6.0.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
mustermann (3.0.4) sha256=85fadcb6b3c6493a8b511b42426f904b7f27b282835502233dd154daab13aa22
mustermann-grape (1.1.0) sha256=8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5
net-imap (0.6.2) sha256=08caacad486853c61676cca0c0c47df93db02abc4a8239a8b67eb0981428acc6
net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
@@ -508,6 +557,7 @@ CHECKSUMS
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
roo (3.0.0) sha256=6fdd7a9158d657c69768b4168754ff2110cc21fdc01a1bec1010820cb05c91b1
rswag-ui (2.17.0) sha256=5f707b9b5e8171ddf9f519f6e401e79e419bd1d07387508603e76124f2443212
rubocop (1.84.0) sha256=88dec310153bb685a879f5a7cdb601f6287b8f0ee675d9dc63a17c7204c4190a
rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
@@ -515,6 +565,7 @@ CHECKSUMS
rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-webdriver (4.40.0) sha256=16ef7aa9853c1d4b9d52eac45aafa916e3934c5c83cb4facb03f250adfd15e5b
+18 -8
View File
@@ -2,7 +2,7 @@
## Overview
"Sanasto Wiki" is a web-based dictionary application for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
"Sanasto Wiki" is a web-based glossary for simultaneous translators in the living Christianity. The application provides publicly accessible translations while restricting editing and commenting to invited contributors.
---
@@ -188,15 +188,25 @@ See 'public/Kristillisyyden sanasto ver 23.5.2013.xlsx'
---
## API (Optional Future)
## API (Public Sync)
REST API for potential mobile app or integration:
Public JSON endpoint for syncing entries:
```
GET /api/entries
GET /api/entries/:id
GET /api/entries/search?q=:query&lang=:code
POST /api/entries (authenticated)
PATCH /api/entries/:id (authenticated)
GET /api/entries
GET /api/entries?since=2026-01-01T12:00:00Z
```
Responses include all language columns, category and `updated_at`. The optional `since`
parameter filters by `updated_at` (ISO8601).
Swagger docs:
```
GET /api/swagger
```
Swagger UI:
```
GET /api
```
## Deployment
+58
View File
@@ -0,0 +1,58 @@
require "grape"
require "grape-swagger"
require "ostruct"
class Api::Base < Grape::API
format :json
content_type :json, "application/json"
helpers do
def parse_since_param(raw_since)
return nil if raw_since.blank?
Time.iso8601(raw_since)
rescue ArgumentError
error!({ error: "Invalid since parameter. Use ISO8601 timestamp." }, 400)
end
end
resource :entries do
desc "Return public entries in all languages",
attributes: OpenStruct.new(success: nil, produces: nil)
params do
optional :since,
type: String,
desc: "ISO8601 timestamp. Returns entries updated after this time."
end
get do
since_time = parse_since_param(params[:since])
entries_scope = Entry.active_entries
entries_scope = entries_scope.where("updated_at > ?", since_time) if since_time
entries_scope
.order(:updated_at, :id)
.select(
:id,
:category,
:fi,
:en,
:sv,
:no,
:ru,
:de,
:updated_at
)
end
end
add_swagger_documentation(
info: {
title: "Sanasto Wiki API",
description: "Public sync API for Sanasto Wiki glossary entries."
},
mount_path: "/swagger",
hide_documentation_path: true,
format: :json
)
end
+3
View File
@@ -0,0 +1,3 @@
Rswag::Ui.configure do |config|
config.swagger_endpoint "/api/swagger", "Sanasto Wiki API"
end
+3
View File
@@ -51,6 +51,9 @@ Rails.application.routes.draw do
end
end
mount Api::Base => "/api"
mount Rswag::Ui::Engine => "/api"
resources :entries do
resources :comments, only: [ :create ]
collection do
+63
View File
@@ -0,0 +1,63 @@
require "test_helper"
class ApiEntriesTest < ActionDispatch::IntegrationTest
test "returns active entries with language fields" do
entry = entries(:one)
get "/api/entries"
assert_response :success
payload = JSON.parse(response.body)
entry_payload = payload.find { |item| item["id"] == entry.id }
assert_not_nil entry_payload
assert_equal entry.fi, entry_payload["fi"]
assert_equal entry.en, entry_payload["en"]
assert_equal entry.sv, entry_payload["sv"]
assert_equal entry.no, entry_payload["no"]
assert_equal entry.ru, entry_payload["ru"]
assert_equal entry.de, entry_payload["de"]
end
test "filters by updated_at when since param is provided" do
older_entry = Entry.create!(
fi: "Older Entry",
category: :word,
status: :active
)
older_entry.update_column(:updated_at, 2.days.ago)
newer_entry = Entry.create!(
fi: "Newer Entry",
category: :word,
status: :active
)
newer_entry.update_column(:updated_at, 1.hour.ago)
get "/api/entries", params: { since: 1.day.ago.iso8601 }
assert_response :success
payload = JSON.parse(response.body)
returned_ids = payload.map { |item| item["id"] }
assert_includes returned_ids, newer_entry.id
assert_not_includes returned_ids, older_entry.id
end
test "returns bad request for invalid since param" do
get "/api/entries", params: { since: "not-a-time" }
assert_response :bad_request
payload = JSON.parse(response.body)
assert_equal "Invalid since parameter. Use ISO8601 timestamp.", payload["error"]
end
test "exposes swagger docs" do
get "/api/swagger"
assert_response :success
payload = JSON.parse(response.body)
assert_equal "Sanasto Wiki API", payload.dig("info", "title")
assert_includes payload.keys, "paths"
end
end