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 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Api
5
+ # Store namespace; children declared here.
6
+ module Store
7
+ autoload :FileSystem, "relaton/api/store/file_system"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Relaton
4
+ module Api
5
+ VERSION = "3.0.0.alpha.1"
6
+ end
7
+ end
@@ -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: []