lutaml-openapi 0.1.0

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: a7b193ad406f534945c41bfbd3bce71181ec841b9f4924f8c107a4ca2a2160ed
4
+ data.tar.gz: e909c86543746e00d56973d4fead8dbe02742318f904acd5213a56d9925e1c77
5
+ SHA512:
6
+ metadata.gz: 5d1c384ca671c9dbfa95ef73fd9806e67808af4534e8b4cd697321ea9fdc7a8d0f783da5ae8beaebb60c1d33dc15da782ad614954f6cd8d80263fb2e857fd200
7
+ data.tar.gz: 3c5de6c1bcd9a4454ffe79d6a17914a5f82bb0e4a70d5cda51a5899ff4d9f47f825c8ee35b753ea9833c885834f138785997d24ff71447ea0a4ee82c0b0ae628
data/.rubocop_todo.yml ADDED
File without changes
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "lutaml-jsonschema" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["ronald.tse@ribose.com"](mailto:"ronald.tse@ribose.com").
data/LICENSE.md ADDED
@@ -0,0 +1,33 @@
1
+ Licenses & Copyright
2
+ ====================
3
+
4
+ This license file adheres to the formatting guidelines of
5
+ [readable-licenses](https://github.com/nevir/readable-licenses).
6
+
7
+
8
+ Ribose's BSD 2-Clause License
9
+ -----------------------------
10
+
11
+ Copyright (c) 2026, [Ribose Inc](https://www.ribose.com).
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without modification,
15
+ are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice,
18
+ this list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
33
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/PLAN.md ADDED
@@ -0,0 +1,103 @@
1
+ # PLAN.md — lutaml-openapi: OpenAPI as a model-based standard
2
+
3
+ Closes [metanorma/metanorma-ogc#153](https://github.com/metanorma/metanorma-ogc/issues/153)
4
+ ("Model-based standard (OpenAPI)", open since 2020-06-15): make the OpenAPI
5
+ definition the **primary publication** of the OGC API family, with the
6
+ implementers' guide and most of the normative OGC/ISO publication **driven by
7
+ the OpenAPI model** — the ISO-10303 play (expressir → lutaml-express →
8
+ metanorma-plugin-lutaml) applied to OpenAPI.
9
+
10
+ ## Mission
11
+
12
+ Parse OpenAPI **3.0 and 3.1** documents into a lossless lutaml-model object
13
+ graph; render them as (a) sovereign single-file human documentation and
14
+ (b) normative Metanorma sections through `metanorma-plugin-openapi`, with
15
+ Markdown descriptions properly converted to AsciiDoc via **coradoc**.
16
+
17
+ ## Repos and org parity
18
+
19
+ | Repo | Org | Role | Setup cloned from |
20
+ |---|---|---|---|
21
+ | `lutaml/lutaml-openapi` | lutaml | the model gem | lutaml-xsd / lutaml-jsonschema (generic-rake CI, cimas release train, rubocop baseline, BSD-2-Clause) |
22
+ | `metanorma/metanorma-plugin-openapi` | metanorma | the Metanorma plugin | metanorma-plugin-lutaml (rake.yml + release.yml, BSD-2-Clause, Ribose authorship) |
23
+
24
+ Team access mirrors siblings: lutaml → staff:admin, developers:push,
25
+ ci:push, express:push, editors:push; metanorma → staff:admin,
26
+ developers:push, ci:push, editors:push.
27
+
28
+ ## Architecture
29
+
30
+ ```
31
+ OpenAPI 3.0/3.1 (YAML/JSON, CommonMark descriptions)
32
+ │ parse (3.0 normalized into the 3.1-shaped model; source dialect kept)
33
+ ▼
34
+ Lutaml::Openapi::Document ── components.schemas ──► Lutaml::Jsonschema::Schema
35
+ │ (lossless: map_unmapped)
36
+ ├─► SPA (own Vue frontend, lutaml-xsd precedent) [human guide]
37
+ └─► metanorma-plugin-openapi ── Coradoc.convert(md → adoc) [normative]
38
+ requirements-class rendering per the OGC API - Features
39
+ "requirements class OpenAPI 3.1" pattern (ModSpec-aware)
40
+ ```
41
+
42
+ ### Design decisions
43
+
44
+ 1. **3.1-shaped model, 3.0 frontend.** 3.1 is the superset and its Schema
45
+ objects are 2020-12 — lutaml-jsonschema's native dialect. The 3.0 parser
46
+ normalizes into the 3.1 shape (`nullable` → `type: [T, null]`,
47
+ boolean `exclusiveMinimum` → numeric, `binary`/`file` formats) while
48
+ `source_dialect` is kept on the document; serialization re-emits 3.0
49
+ semantics for 3.0 sources. Anything unmodeled rides `map_unmapped`
50
+ extensions — the document round-trips verbatim.
51
+ 2. **Descriptions are Markdown (CommonMark per OAS).** The model stores them
52
+ verbatim (losslessness). Conversion to AsciiDoc happens at render time via
53
+ `Coradoc.convert(desc, from: :markdown, to: :asciidoc)` — the coradoc tree
54
+ is the manipulation point (opoudjis's pipeline in metanorma-ogc#153). The
55
+ SPA keeps its own markdown-lite renderer.
56
+ 3. **Schema layer is delegated.** `components.schemas` materializes as
57
+ `Lutaml::Jsonschema::Schema` — schema-level features (diff, exports, meta-
58
+ schema validation) come free and stay MECE with lutaml-jsonschema.
59
+ 4. **The plugin targets ModSpec.** metanorma#125 shipped the model-based
60
+ standards machinery (modspec 0.2.3). The plugin renders
61
+ requirements-class sections that reference/extend OpenAPI artifacts —
62
+ the Features API pattern — not free-form prose.
63
+ 5. **Own SPA frontend** in lutaml-openapi (lutaml-xsd precedent), seeded
64
+ from lutaml-jsonschema's frontend patterns (i18n catalog, axe audit in
65
+ the default rake gate, browser/PDF gates, determinism guard).
66
+
67
+ ## Milestones
68
+
69
+ - **M0 — repos live with sibling parity** (this milestone): both repos
70
+ created, teams set, CI + release workflows, lint baseline, first PR merged.
71
+ *Done-when:* both repos' CI green on main; team access matches siblings.
72
+ - **M1 — 3.1 model + 3.0 frontend parser**: `Document`, `Path`, `Operation`,
73
+ `Components`, `Response`, `Parameter`, `RequestBody`, `SecurityScheme`,
74
+ `Server`; schemas as `Lutaml::Jsonschema::Schema`; lossless round-trip;
75
+ both OGC corpus documents (oas30 + oas31) parse, round-trip, and
76
+ meta-validate. *Done-when:* both corpus files round-trip with zero
77
+ modeled-keyword loss (golden specs); unknown keywords preserved.
78
+ - **M2 — SPA**: OpenAPI human guide (paths/operations/components views,
79
+ search, builder for examples); i18n + AA audit + determinism + browser
80
+ gates as in lutaml-jsonschema. *Done-when:* the two corpus documents render
81
+ as single-file SPAs that boot in headless Chrome and pass axe AA.
82
+ - **M3 — `metanorma-plugin-openapi`**: `lutaml_openapi` /
83
+ `openapi_requirements_class` blocks emitting normative AsciiDoc sections;
84
+ descriptions through coradoc; ModSpec requirement-class alignment.
85
+ *Done-when:* an AsciiDoc document embedding the Features API OpenAPI
86
+ renders through metanorma into valid HTML/PDF with normative sections.
87
+ - **M4 — OGC acceptance**: a metanorma-ogc#153 close-out demonstration —
88
+ OGC API - Features Part 1 published as (a) our SPA and (b) a Metanorma
89
+ document with model-driven normative sections. *Done-when:* the demonstration
90
+ is linked on metanorma-ogc#153 and the ticket can close.
91
+
92
+ ## Non-goals (for now)
93
+
94
+ - OpenAPI 2.0/Swagger; AsyncAPI.
95
+ - Markdown *authoring* of OpenAPI (opoudjis's coradoc round-trip) — the 2026
96
+ note makes requirements authoring the focus; revisit later.
97
+ - We do not host/serve anything: artifacts are sovereign files.
98
+
99
+ ## Standing laws (inherited)
100
+
101
+ lutaml-model serialization only (no hand-rolled to_h); autoload over
102
+ require_relative; PRs only, never commit to main; no AI attribution; exact
103
+ release versions from the owner; real objects in specs, never doubles.
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Lutaml::Openapi
2
+
3
+ OpenAPI 3.0/3.1 as a lossless lutaml-model — the model layer for
4
+ model-based OpenAPI standards (see PLAN.md for the program; closes
5
+ metanorma/metanorma-ogc#153's model layer).
6
+
7
+ ## Status
8
+
9
+ Scaffold. Milestones and acceptance criteria live in [PLAN.md](PLAN.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Openapi
5
+ # An OpenAPI document: the root object (3.1 shape).
6
+ #
7
+ # Losslessness lives on the raw hash: `components` (and every other
8
+ # section) is kept verbatim, and 3.0 sources denormalize back on
9
+ # serialization. The typed schema view (#schemas) materializes
10
+ # lutaml-jsonschema models on demand — that view applies the
11
+ # jsonschema model's own normalizations, so round-trip fidelity is
12
+ # asserted on the hash, not the typed layer.
13
+ class Document < Lutaml::Model::Serializable
14
+ attribute :openapi, :string
15
+ attribute :source_dialect, :string
16
+ attribute :info, :hash
17
+ attribute :servers, :hash, collection: true
18
+ attribute :paths, :hash
19
+ attribute :webhooks, :hash
20
+ attribute :security, :hash, collection: true
21
+ attribute :tags, :hash, collection: true
22
+ attribute :external_docs, :hash
23
+ attribute :components, :hash
24
+
25
+ # Keywords the model does not name (vendor extensions, dialect
26
+ # additions): collected verbatim, re-emitted on serialization.
27
+ attribute :extensions, :hash, default: -> { {} }
28
+
29
+ json do
30
+ map "openapi", to: :openapi
31
+ map "info", to: :info
32
+ map "servers", to: :servers
33
+ map "paths", to: :paths
34
+ map "webhooks", to: :webhooks
35
+ map "components", to: :components
36
+ map "security", to: :security
37
+ map "tags", to: :tags
38
+ map "externalDocs", to: :external_docs
39
+ map_unmapped to: :extensions
40
+ end
41
+
42
+ # components.schemas as { name => Lutaml::Jsonschema::Schema } —
43
+ # the typed view for diff/exports/validation.
44
+ def schemas
45
+ ((components && components["schemas"]) || {}).to_h do |name, value|
46
+ [name, Lutaml::Jsonschema::Schema.from_json(JSON.generate(value))]
47
+ end
48
+ end
49
+
50
+ def to_hash(options = {})
51
+ hash = super
52
+ hash.delete("source_dialect") # internal: tracks the frontend used
53
+ return hash unless source_dialect == "3.0"
54
+
55
+ Oas30.denormalize(hash)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Openapi
5
+ # The OpenAPI 3.0 frontend: normalizes 3.0 documents into the 3.1
6
+ # shape before modeling, and denormalizes back on serialization —
7
+ # both directions as pure hash walks.
8
+ module Oas30
9
+ module_function
10
+
11
+ def normalize(hash)
12
+ walk(hash) do |node|
13
+ node = normalize_nullable(node)
14
+ normalize_exclusive(node, "exclusiveMinimum", "minimum") ||
15
+ normalize_exclusive(node, "exclusiveMaximum", "maximum") || node
16
+ end
17
+ end
18
+
19
+ def denormalize(hash)
20
+ walk(hash) do |node|
21
+ node = denormalize_nullable(node)
22
+ node = denormalize_exclusive(node, "exclusiveMinimum", "minimum")
23
+ denormalize_exclusive(node, "exclusiveMaximum", "maximum")
24
+ end
25
+ end
26
+
27
+ def walk(node, &)
28
+ case node
29
+ when Hash
30
+ node = yield node
31
+ node.transform_values { |v| walk(v, &) }
32
+ when Array
33
+ node.map { |v| walk(v, &) }
34
+ else
35
+ node
36
+ end
37
+ end
38
+
39
+ # 3.0 `nullable: true` → a null-member type union; `nullable:
40
+ # false` is dropped (3.1 default).
41
+ def normalize_nullable(node)
42
+ return node unless node.key?("nullable")
43
+
44
+ nullable = node.delete("nullable")
45
+ return node unless nullable == true
46
+
47
+ types = Array(node["type"]).compact
48
+ node["type"] = (types << "null").uniq unless types.include?("null")
49
+ node
50
+ end
51
+
52
+ def denormalize_nullable(node)
53
+ types = node["type"]
54
+ return node unless types.is_a?(Array) && types.include?("null")
55
+
56
+ rest = types - ["null"]
57
+ if rest.empty?
58
+ node.delete("type") # 3.0 had only `nullable: true`, no type
59
+ else
60
+ node["type"] = rest.size == 1 ? rest.first : rest
61
+ end
62
+ node["nullable"] = true
63
+ node
64
+ end
65
+
66
+ # 3.0 boolean exclusivity rides beside the bound; 3.1 carries the
67
+ # bound in the exclusive* keyword itself.
68
+ def normalize_exclusive(node, exclusive, bound)
69
+ return node unless [true, false].include?(node[exclusive])
70
+
71
+ flag = node.delete(exclusive)
72
+ # the bound MOVES into exclusive*: minimum must not survive it
73
+ node[exclusive] = node.delete(bound) if flag == true && node.key?(bound)
74
+ node
75
+ end
76
+
77
+ def denormalize_exclusive(node, exclusive, bound)
78
+ value = node[exclusive]
79
+ return node unless value.is_a?(Numeric)
80
+
81
+ node = node.merge(bound => value)
82
+ node[exclusive] = true
83
+ node
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Openapi
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/jsonschema"
4
+ require "lutaml/model"
5
+ require "yeptris/yaml"
6
+ require "json"
7
+
8
+ module Lutaml
9
+ # OpenAPI 3.0/3.1 as a lossless lutaml-model.
10
+ #
11
+ # The 3.1 shape is the model (its Schema objects are JSON Schema
12
+ # 2020-12, carried by lutaml-jsonschema); 3.0 documents are
13
+ # normalized into it by the 3.0 frontend while `source_dialect` is
14
+ # kept, so serialization re-emits 3.0 semantics for 3.0 sources.
15
+ # Anything unmodeled rides the extensions catch-all — documents
16
+ # round-trip verbatim.
17
+ module Openapi
18
+ autoload :VERSION, "lutaml/openapi/version"
19
+ autoload :Document, "lutaml/openapi/document"
20
+ autoload :Oas30, "lutaml/openapi/oas30"
21
+
22
+ # Parse an OpenAPI document (YAML or JSON, 3.0 or 3.1) into a
23
+ # lossless {Document}.
24
+ def self.parse(string)
25
+ data = parse_raw(string)
26
+ dialect = data["openapi"].to_s.start_with?("3.0") ? "3.0" : "3.1"
27
+ data = Oas30.normalize(data) if dialect == "3.0"
28
+
29
+ Document.from_json(JSON.generate(data)).tap do |doc|
30
+ doc.source_dialect = dialect
31
+ end
32
+ end
33
+
34
+ def self.parse_raw(string)
35
+ stripped = string.lstrip
36
+ if stripped.start_with?("{", "[")
37
+ require "yeptris/json"
38
+ Yeptris::JSON.load(string)
39
+ else
40
+ Yeptris::YAML.load(string)
41
+ end
42
+ end
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lutaml-openapi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
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: json
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: lutaml-jsonschema
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.1.25
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.25
40
+ - !ruby/object:Gem::Dependency
41
+ name: lutaml-model
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.8.122
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.8.122
54
+ - !ruby/object:Gem::Dependency
55
+ name: yeptris
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.6.34
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 0.6.34
68
+ description: Parse OpenAPI 3.0/3.1 documents into lossless LutaML model objects; the
69
+ model layer for model-based OpenAPI standards
70
+ email:
71
+ - open.source@ribose.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rubocop_todo.yml"
77
+ - CHANGELOG.md
78
+ - CODE_OF_CONDUCT.md
79
+ - LICENSE.md
80
+ - PLAN.md
81
+ - README.md
82
+ - Rakefile
83
+ - lib/lutaml/openapi.rb
84
+ - lib/lutaml/openapi/document.rb
85
+ - lib/lutaml/openapi/oas30.rb
86
+ - lib/lutaml/openapi/version.rb
87
+ homepage: https://github.com/lutaml/lutaml-openapi
88
+ licenses:
89
+ - BSD-2-Clause
90
+ metadata:
91
+ homepage_uri: https://github.com/lutaml/lutaml-openapi
92
+ source_code_uri: https://github.com/lutaml/lutaml-openapi
93
+ changelog_uri: https://github.com/lutaml/lutaml-openapi/blob/main/CHANGELOG.md
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 3.2.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.6.9
109
+ specification_version: 4
110
+ summary: OpenAPI 3.0/3.1 as a lossless lutaml-model
111
+ test_files: []