relaton-api 3.0.0.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +14 -0
- data/lib/relaton/api/app.rb +28 -0
- data/lib/relaton/api/config.rb +15 -0
- data/lib/relaton/api/routes/documents.rb +35 -0
- data/lib/relaton/api/routes/flavors.rb +21 -0
- data/lib/relaton/api/routes/version.rb +19 -0
- data/lib/relaton/api/routes.rb +12 -0
- data/lib/relaton/api/store/file_system.rb +66 -0
- data/lib/relaton/api/store.rb +10 -0
- data/lib/relaton/api/version.rb +7 -0
- data/lib/relaton/api.rb +14 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2b740aec7c4c0447a4f3b508d59506824c9973dff15782c695a1e11967218ab3
|
|
4
|
+
data.tar.gz: 7f6bd80bd1405f37d458303be73b0127d7a510f974bef8a03edbdcc435c0f416
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0db2070321797c88ed253391daf160c7a3718f5b2aed8ae8b3cecc844a5267025dadd710e9eeb9dcf0a5a76b5d130a5fb89a15e48f48812122d8b5e56a225b9e
|
|
7
|
+
data.tar.gz: 2d8b06448153bd9a5457669b4d7a576c71f57a8353fd5e6db228244a1679031220363d83f6313384167530bd0f0acdcbf84f5eed81bff49f686ace549486e1f7
|
data/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# relaton-api (Ruby reference implementation)
|
|
2
|
+
|
|
3
|
+
Config-driven Rack application serving the Relaton API v3 contract over a
|
|
4
|
+
dataset directory. Sister implementation of the npm package @relaton/api;
|
|
5
|
+
both are pinned by the shared conformance corpus.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
gem install relaton-api
|
|
10
|
+
RELATON_DATA_DIR=./data rackup examples/config.ru
|
|
11
|
+
|
|
12
|
+
Structure and rules: autoload-only loading (no require_relative inside
|
|
13
|
+
lib/), routes as a registry (OCP), model-driven responses via the relaton
|
|
14
|
+
gem. See TODO.api/11-… in relaton/api.relaton.org for the roadmap.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
module Api
|
|
7
|
+
# Rack application. Routes are a registry — adding one is a new
|
|
8
|
+
# autoloaded class plus one entry here (open/closed).
|
|
9
|
+
class App
|
|
10
|
+
ROUTES = [Routes::Version, Routes::Flavors, Routes::Documents].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :config, :store
|
|
13
|
+
|
|
14
|
+
def initialize(config)
|
|
15
|
+
@config = config
|
|
16
|
+
@store = Store::FileSystem.new(config.data_dir)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(env)
|
|
20
|
+
route = ROUTES.map(&:new).find { |r| r.matches?(env["PATH_INFO"]) }
|
|
21
|
+
return route.call(env, self) if route
|
|
22
|
+
|
|
23
|
+
[404, { "content-type" => "application/problem+json" },
|
|
24
|
+
[{ type: "about:blank", title: "Not Found" }.to_json]]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
module Api
|
|
5
|
+
# Immutable deployment configuration.
|
|
6
|
+
class Config
|
|
7
|
+
attr_reader :name, :data_dir
|
|
8
|
+
|
|
9
|
+
def initialize(name:, data_dir:)
|
|
10
|
+
@name = name
|
|
11
|
+
@data_dir = data_dir
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Relaton
|
|
7
|
+
module Api
|
|
8
|
+
module Routes
|
|
9
|
+
# GET /v3/documents/{key} — full Relaton JSON by any indexed id
|
|
10
|
+
class Documents
|
|
11
|
+
KEY_PATTERN = %r{\A/v3/documents/(?<key>[^/]+)\z}.freeze
|
|
12
|
+
|
|
13
|
+
def matches?(path)
|
|
14
|
+
KEY_PATTERN.match?(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env, app)
|
|
18
|
+
key = URI.decode_www_form_component(KEY_PATTERN.match(env["PATH_INFO"])[:key])
|
|
19
|
+
document = app.store.fetch(key)
|
|
20
|
+
return not_found(key) unless document
|
|
21
|
+
|
|
22
|
+
[200, { "content-type" => "application/json" }, [JSON.generate(document)]]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def not_found(key)
|
|
28
|
+
[404, { "content-type" => "application/problem+json" },
|
|
29
|
+
[{ type: "about:blank", title: "Not Found",
|
|
30
|
+
detail: "unknown key #{key}" }.to_json]]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
module Api
|
|
7
|
+
module Routes
|
|
8
|
+
# GET /v3/flavors — stream coverage from the store
|
|
9
|
+
class Flavors
|
|
10
|
+
def matches?(path)
|
|
11
|
+
path == "/v3/flavors"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(_env, app)
|
|
15
|
+
[200, { "content-type" => "application/json" },
|
|
16
|
+
[JSON.generate(app.store.streams)]]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
module Api
|
|
7
|
+
module Routes
|
|
8
|
+
# GET /v3/version
|
|
9
|
+
class Version
|
|
10
|
+
def matches?(path) = path == "/v3/version"
|
|
11
|
+
|
|
12
|
+
def call(_env, app)
|
|
13
|
+
[200, { "content-type" => "application/json" },
|
|
14
|
+
[{ name: app.config.name, version: VERSION, model: "relaton" }.to_json]]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relaton
|
|
4
|
+
module Api
|
|
5
|
+
# Route namespace; children declared here.
|
|
6
|
+
module Routes
|
|
7
|
+
autoload :Version, "relaton/api/routes/version"
|
|
8
|
+
autoload :Documents, "relaton/api/routes/documents"
|
|
9
|
+
autoload :Flavors, "relaton/api/routes/flavors"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
module Api
|
|
7
|
+
module Store
|
|
8
|
+
# Reads documents from a dataset directory (Relaton YAML, any stream
|
|
9
|
+
# layout). Builds a lookup map at boot from every docidentifier
|
|
10
|
+
# variant; keys are uppercased, whitespace-stripped (the corpus
|
|
11
|
+
# doctrine — datasets own richer variants via the #109 index).
|
|
12
|
+
class FileSystem
|
|
13
|
+
KeyEntry = Struct.new(:file, :docid)
|
|
14
|
+
|
|
15
|
+
def initialize(data_dir)
|
|
16
|
+
@data_dir = data_dir
|
|
17
|
+
@keys = {}
|
|
18
|
+
@counts = Hash.new(0)
|
|
19
|
+
build_index
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def fetch(key)
|
|
23
|
+
entry = @keys[key.to_s.upcase.delete(" ")]
|
|
24
|
+
return nil unless entry
|
|
25
|
+
|
|
26
|
+
document(entry)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# [{ stream:, count: }] per top-level stream directory
|
|
30
|
+
def streams
|
|
31
|
+
@counts.sort.map { |stream, count| { stream: stream, count: count } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def build_index
|
|
37
|
+
Dir[File.join(@data_dir, "**", "*.yaml")].sort.each do |path|
|
|
38
|
+
doc = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
39
|
+
ids = (doc["docidentifier"] || doc["docid"] || [])
|
|
40
|
+
.filter_map { |d| d["content"] || d["id"] }
|
|
41
|
+
next if ids.empty?
|
|
42
|
+
|
|
43
|
+
@counts[stream_of(path)] += 1
|
|
44
|
+
entry = KeyEntry.new(path, ids.first)
|
|
45
|
+
ids.each { |id| @keys[key_of(id)] ||= entry }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stream_of(path)
|
|
50
|
+
rel = path.delete_prefix("#{@data_dir}/")
|
|
51
|
+
rel.include?("/") ? rel.split("/").first : "data"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def key_of(id)
|
|
55
|
+
id.to_s.upcase.delete(" ")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The dataset YAML is already the model serialization; serving it
|
|
59
|
+
# as JSON is a passthrough, not a re-serialization.
|
|
60
|
+
def document(entry)
|
|
61
|
+
YAML.safe_load(File.read(entry.file), aliases: true)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/relaton/api.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "relaton/api/version"
|
|
4
|
+
|
|
5
|
+
module Relaton
|
|
6
|
+
# Relaton API reference implementation. All constants load lazily via
|
|
7
|
+
# autoloads declared here (the immediate parent namespace file).
|
|
8
|
+
module Api
|
|
9
|
+
autoload :Config, "relaton/api/config"
|
|
10
|
+
autoload :App, "relaton/api/app"
|
|
11
|
+
autoload :Routes, "relaton/api/routes"
|
|
12
|
+
autoload :Store, "relaton/api/store"
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: relaton-api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 3.0.0.alpha.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ribose Inc.
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rack
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: relaton
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: Config-driven Relaton API server over a dataset directory.
|
|
55
|
+
email:
|
|
56
|
+
- open.source@ribose.com
|
|
57
|
+
executables: []
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- README.md
|
|
62
|
+
- lib/relaton/api.rb
|
|
63
|
+
- lib/relaton/api/app.rb
|
|
64
|
+
- lib/relaton/api/config.rb
|
|
65
|
+
- lib/relaton/api/routes.rb
|
|
66
|
+
- lib/relaton/api/routes/documents.rb
|
|
67
|
+
- lib/relaton/api/routes/flavors.rb
|
|
68
|
+
- lib/relaton/api/routes/version.rb
|
|
69
|
+
- lib/relaton/api/store.rb
|
|
70
|
+
- lib/relaton/api/store/file_system.rb
|
|
71
|
+
- lib/relaton/api/version.rb
|
|
72
|
+
homepage: https://github.com/relaton/relaton-api-ruby
|
|
73
|
+
licenses:
|
|
74
|
+
- BSD-2-Clause
|
|
75
|
+
metadata: {}
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '3.0'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 3.6.9
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Relaton API — Rack reference implementation
|
|
93
|
+
test_files: []
|