lutaml-jsonschema 0.1.25 → 0.1.26
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 +4 -4
- data/CLAUDE.md +13 -5
- data/README.md +81 -17
- data/lib/lutaml/jsonschema/exports/adoc.rb +64 -10
- data/lib/lutaml/jsonschema/schema_set.rb +4 -5
- data/lib/lutaml/jsonschema/schema_store.rb +2 -3
- data/lib/lutaml/jsonschema/source_file.rb +97 -0
- data/lib/lutaml/jsonschema/version.rb +1 -1
- data/lib/lutaml/jsonschema.rb +1 -0
- data/script/axe_audit +34 -20
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7c72a0b9f42bf072c40e4f52b986ba536f59ed1eaa96650bd6df86d469a16e1
|
|
4
|
+
data.tar.gz: 818f5e758c204d40d20f371ccb3cc6e772ab2c9e7b54e59c5f02de9b0050b9db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85878a652c2ffee03bed715b5ee1eb83c0b54b8d16e04ef5dee20977ebed04f7b239a8f2a0c43a2c49e25b14a96a55dc620e9c8033b5f4d20afd3fc0108fef56
|
|
7
|
+
data.tar.gz: 4ab26811feb7b7be7ff0ebe407582ba1adf2c6a2109c84d4a411e16e544cec7186ad276043425f027178fd2782fbb5aea54c6c921a90c069341d722c38d2f850
|
data/CLAUDE.md
CHANGED
|
@@ -16,14 +16,14 @@ bundle exec rspec # full suite
|
|
|
16
16
|
bundle exec rspec spec/lutaml/cli_spec.rb # one file
|
|
17
17
|
bundle exec rspec spec/lutaml/cli_spec.rb:76 # one example
|
|
18
18
|
bundle exec rubocop # lint (-a to autofix; .rubocop_todo.yml holds baselined offenses)
|
|
19
|
-
bundle exec rake # spec + rubocop (what CI runs)
|
|
19
|
+
bundle exec rake # spec + rubocop + perf gate + axe AA audit (what CI runs)
|
|
20
20
|
|
|
21
21
|
bundle exec rake frontend:build # npm install + vite build → frontend/dist
|
|
22
22
|
cd frontend && npm test # vitest (frontend unit tests)
|
|
23
23
|
|
|
24
24
|
bundle exec exe/lutaml-jsonschema spa SCHEMA_FILES... [--diff BASELINE...]
|
|
25
25
|
bundle exec exe/lutaml-jsonschema combine|validate SCHEMA_FILES...
|
|
26
|
-
bundle exec exe/lutaml-jsonschema export SCHEMA_FILES... -f cddl|openapi|xsd|adoc
|
|
26
|
+
bundle exec exe/lutaml-jsonschema export SCHEMA_FILES... -f cddl|openapi|xsd|adoc|lml
|
|
27
27
|
bundle exec exe/lutaml-jsonschema diff OLD NEW [--format json]
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -34,13 +34,14 @@ bundle exec exe/lutaml-jsonschema diff OLD NEW [--format json]
|
|
|
34
34
|
Data flow: schema files → `SchemaSet.load_from_files` → `Spa::SpaBuilder` → `Spa::SpaDocument` → `to_json` → `Spa::VueInlinedStrategy` (embeds `window.SCHEMA_DATA` + inlined JS/CSS into `index.html`).
|
|
35
35
|
|
|
36
36
|
- **`Base`** — everything inherits from `Lutaml::Model::Serializable`. `Base` also forward-declares `Schema` to break the circular `Schema ↔ PropertyEntry ↔ Link` references.
|
|
37
|
-
- **`Schema`** — the central model of a JSON Schema document. Every `$`-prefixed keyword is a `dollar_`-prefixed attribute (`$schema` → `dollar_schema`) via json mappings. `Schema.from_json` / `to_json` are lutaml-model serialization.
|
|
37
|
+
- **`Schema`** — the central model of a JSON Schema document. Every `$`-prefixed keyword is a `dollar_`-prefixed attribute (`$schema` → `dollar_schema`) via json mappings. `Schema.from_json` / `to_json` are lutaml-model serialization. The model is **lossless**: `map_unmapped to: :extensions` collects every unmodeled keyword (vendor `x-*`, dialect additions) verbatim and re-emits it — new keywords land as attributes, not in a hand-rolled serializer.
|
|
38
38
|
- **`SchemaSet`** — in-memory named collection of schemas plus `$ref` resolution (local `#/...` pointers and relative `./file.json` refs through `ReferenceResolver`) and validation.
|
|
39
39
|
- **`SchemaStore`** — `SchemaSet` subclass that adds on-disk persistence through **lutaml-store** (`PackageStore`, package shape from `LjrPackageDefinition`).
|
|
40
40
|
- **`Combiner`** — compatibility facade; bundling lives in `SchemaSet#bundle` (hoists definitions, adds a top-level `$ref` property per source schema, rewrites `#/definitions/` → `#/$defs/`).
|
|
41
41
|
- **`Spa::*`** — `SpaBuilder` flattens a `SchemaSet` into a `SpaDocument` (metadata, `SpaSchema` list, flat `search_index` for FlexSearch). `Spa::Generator` renders through an `OutputStrategy`; `VueInlinedStrategy` is the only implementation.
|
|
42
42
|
- **`Configuration`** — YAML config for the SPA (title, theme, appearance…), loaded by `Cli#spa -c`.
|
|
43
|
-
- **`Cli`** — Thor commands: `spa` (generate docs; `--diff` embeds a classified change set), `combine` (merge schemas), `export` (cddl/openapi/xsd/adoc), `diff` (exits 1 on breaking changes), `validate` (meta-schema checks), `version`.
|
|
43
|
+
- **`Cli`** — Thor commands: `spa` (generate docs; `--diff` embeds a classified change set), `combine` (merge schemas), `export` (cddl/openapi/xsd/adoc/lml; adoc `--embeddable` drops the level-0 title and namespaces anchors for Metanorma `include::`), `diff` (exits 1 on breaking changes), `validate` (offline meta-schema checks against fully vendored meta-schemas, JSON Pointer error paths), `version`.
|
|
44
|
+
- **`Exports::*`** — one renderer per target language (CDDL, OpenAPI 3.1, XSD, AsciiDoc, LML); all read the same `SchemaSet`, and each documents its approximations on the class. LML output parses back through `lutaml-lml`; CDDL through the RFC 8610 reference parser.
|
|
44
45
|
- **`TupleItem`** — schema-or-boolean wrapper used by `items`/`prefixItems` arrays and composition collections; models that accept `true`/`false` store booleans in companion `*_boolean` attributes and expose `*_value` helpers.
|
|
45
46
|
|
|
46
47
|
## JSON engine: yeptris
|
|
@@ -51,9 +52,16 @@ Two distinct paths, both engine-backed:
|
|
|
51
52
|
- **Model serialization** (`Schema.from_json`/`to_json`, `Configuration.from_yaml`, lutaml-store I/O) goes through lutaml-model, whose `AdapterResolver` auto-detects yeptris from the bundle and routes both `:json` and `:yaml` to its `YeptrisAdapter`. That adapter re-raises parse failures as `JSON::ParserError`, so error handling must cover **both** exception types — see the rescue in `Cli#validate`.
|
|
52
53
|
- Raw-parse speed depends on the resolved yeptris build shipping a **native materializer for the running Ruby version**; without one it falls back to a pure-Ruby tape engine that is several times slower than `JSON.parse` on large documents (yeptris-ruby#278). Check with `defined?(Yeptris::Native)` (0.6.28.x shipped only Ruby 3.3; 3.4-native starts at 0.6.29.1).
|
|
53
54
|
- The GC-pressure SIGSEGV in the native JSON materializer (yeptris-ruby#279) is fixed in the yeptris **0.6.34 line** (staged pairs made GC-visible); the gemspec floors at `~> 0.6.34`, excluding the crashy 0.6.29.x–0.6.33.x native builds. Env-gated canary: yeptris-ruby#280.
|
|
55
|
+
- The 0.6.34 C **strict-dup pre-scan** was broken (one shared key table, no growth — false duplicates on ordinary schemas, hang on the 17th distinct key); fixed in yeptris v0.6.35 (per-container frame stacks). Lanes without a native materializer (all Windows) ride that marshal route, so engine bugs surface there first.
|
|
56
|
+
|
|
57
|
+
## Frontend: i18n, RTL, a11y
|
|
58
|
+
|
|
59
|
+
- All UI strings come from the typed catalog `frontend/src/i18n/messages.ts` (`en` + `ar`); `t()`/`tf()` read a Vue ref, so the header switch re-renders live. A missing key in a locale is a type error — add both languages.
|
|
60
|
+
- The locale mirrors `lang`/`dir` onto `<html>`; layout uses CSS logical properties (`*-inline-*`), so RTL needs no second stylesheet. `?locale=ar` boots straight into a locale.
|
|
61
|
+
- `script/axe_audit` (in the default `rake` gate) scans the generated site with the vendored axe-core; browser-less lanes skip cleanly. The browser specs (boot, PDF, RTL render) run wherever Chrome exists.
|
|
54
62
|
|
|
55
63
|
## Repo notes
|
|
56
64
|
|
|
57
|
-
- `README.md` is unreplaced gem-template boilerplate — don't treat it as documentation.
|
|
58
65
|
- `TODO.redoc/`, `TODO.refactor/`, `TODO.store/` are untracked, numbered working notes (known gaps and planned refactors), not code. `TODO.sota/` holds the SOTA program (purpose, per-area tasks with done-when criteria, and the goal prompt).
|
|
59
66
|
- CI is metanorma/ci's `generic-rake` workflow; releases run through `.github/workflows/release.yml`.
|
|
67
|
+
- Ruby 4.0 binds brace-less hash literals as keyword arguments — always brace hashes passed to positional-only methods (specs included).
|
data/README.md
CHANGED
|
@@ -1,39 +1,103 @@
|
|
|
1
1
|
# Lutaml::Jsonschema
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
**JSON Schema, fully legible** — a lossless typed model for machines and
|
|
4
|
+
Redoc-class, sovereign single-file documentation for humans, inside the
|
|
5
|
+
LutaML/metanorma standards ecosystem.
|
|
6
|
+
|
|
7
|
+
Parses JSON Schema documents (draft-04 through 2020-12, plus the
|
|
8
|
+
interagent-era hyper-schema corpus) into lutaml-model objects, resolves and
|
|
9
|
+
bundles `$ref` graphs (local pointers, cross-file, and opt-in remote HTTP(S)
|
|
10
|
+
with an offline SHA-256 cache), validates against fully vendored
|
|
11
|
+
meta-schemas, diffs schema versions with breaking-change classification, and
|
|
12
|
+
generates a self-contained documentation SPA — no runtime fetches, no
|
|
13
|
+
network, one `index.html`.
|
|
14
|
+
|
|
15
|
+
**The model is lossless**: every keyword the Ruby model does not name —
|
|
16
|
+
vendor `x-*` extensions, dialect additions, hyper-schema keywords — is
|
|
17
|
+
collected verbatim and re-emitted on serialization (`map_unmapped`).
|
|
6
18
|
|
|
7
19
|
## Installation
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
```bash
|
|
22
|
+
gem install lutaml-jsonschema
|
|
23
|
+
```
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
or add to your Gemfile:
|
|
12
26
|
|
|
13
27
|
```bash
|
|
14
|
-
bundle add
|
|
28
|
+
bundle add lutaml-jsonschema
|
|
15
29
|
```
|
|
16
30
|
|
|
17
|
-
|
|
31
|
+
## CLI
|
|
18
32
|
|
|
19
33
|
```bash
|
|
20
|
-
|
|
34
|
+
# generate the single-file documentation SPA
|
|
35
|
+
lutaml-jsonschema spa SCHEMA_FILES... [--diff BASELINE...] [-c CONFIG.yml]
|
|
36
|
+
|
|
37
|
+
# merge schema documents into one bundle
|
|
38
|
+
lutaml-jsonschema combine SCHEMA_FILES...
|
|
39
|
+
|
|
40
|
+
# export to other data-definition languages
|
|
41
|
+
lutaml-jsonschema export SCHEMA_FILES... -f cddl|openapi|xsd|adoc|lml
|
|
42
|
+
|
|
43
|
+
# semantic diff; exits non-zero when a breaking change is found
|
|
44
|
+
lutaml-jsonschema diff OLD_SCHEMA NEW_SCHEMA [--format json]
|
|
45
|
+
|
|
46
|
+
# validate against the dialect's meta-schema (offline; --format json)
|
|
47
|
+
lutaml-jsonschema validate SCHEMA_FILE
|
|
21
48
|
```
|
|
22
49
|
|
|
23
|
-
|
|
50
|
+
The SPA supports light/dark themes, FlexSearch search, per-property and
|
|
51
|
+
per-definition detail views, composition tabs, a JSON builder, a change view
|
|
52
|
+
(`--diff`), RTL locales (`?locale=ar`), and prints to PDF.
|
|
24
53
|
|
|
25
|
-
|
|
54
|
+
## Library
|
|
26
55
|
|
|
27
|
-
|
|
56
|
+
```ruby
|
|
57
|
+
require "lutaml/jsonschema"
|
|
28
58
|
|
|
29
|
-
|
|
59
|
+
schema = Lutaml::Jsonschema::Schema.from_json(json_string)
|
|
60
|
+
schema.extensions # => unknown keywords, preserved verbatim
|
|
61
|
+
schema.to_json # lossless re-emission
|
|
30
62
|
|
|
31
|
-
|
|
63
|
+
set = Lutaml::Jsonschema::SchemaSet.load_from_files(*paths)
|
|
64
|
+
set.bundle # hoist definitions, rewrite refs
|
|
65
|
+
set.dereference("demo") # resolve the $ref graph
|
|
32
66
|
|
|
33
|
-
|
|
67
|
+
Lutaml::Jsonschema::Exports::Cddl.new(set).render # RFC 8610
|
|
68
|
+
Lutaml::Jsonschema::Exports::Openapi.new(set).render # OAS 3.1 components
|
|
69
|
+
Lutaml::Jsonschema::Exports::Xsd.new(set).render # W3C XML Schema
|
|
70
|
+
Lutaml::Jsonschema::Exports::Adoc.new(set).render # AsciiDoc reference
|
|
71
|
+
Lutaml::Jsonschema::Exports::Lml.new(set).render # LutaML UML models
|
|
72
|
+
|
|
73
|
+
Lutaml::Jsonschema::Diff.between(old_schema, new_schema) # classified changes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`SchemaStore` (a `SchemaSet` subclass) adds on-disk persistence through
|
|
77
|
+
[lutaml-store](https://github.com/lutaml/lutaml-store).
|
|
78
|
+
|
|
79
|
+
## JSON engine
|
|
80
|
+
|
|
81
|
+
Parsing rides [yeptris](https://github.com/leptris/yeptris-ruby), the org's
|
|
82
|
+
native YAML/JSON engine (FFI over libyeptris, strict RFC 8259 semantics with
|
|
83
|
+
`JSON.parse` parity), auto-detected by lutaml-model for both JSON and YAML.
|
|
84
|
+
JSON generation stays on the stdlib `json` gem.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
34
87
|
|
|
35
|
-
|
|
88
|
+
```bash
|
|
89
|
+
bundle exec rake # specs + rubocop + perf gate + axe AA audit
|
|
90
|
+
bundle exec rspec # full suite
|
|
91
|
+
cd frontend && npm test # SPA unit tests
|
|
92
|
+
bundle exec rake frontend:build # build frontend/dist (needed for spa)
|
|
93
|
+
```
|
|
36
94
|
|
|
37
|
-
|
|
95
|
+
CI runs the full matrix (3 Ruby lines × ubuntu/macos/windows) on every push,
|
|
96
|
+
including the browser gates (headless-Chrome boot, PDF, RTL render) and the
|
|
97
|
+
WCAG AA audit. `spec/fixtures/` carries the real-world corpora (heroku,
|
|
98
|
+
interagent) the specs round-trip.
|
|
99
|
+
|
|
100
|
+
## Contributing
|
|
38
101
|
|
|
39
|
-
|
|
102
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
103
|
+
https://github.com/lutaml/lutaml-jsonschema.
|
|
@@ -7,8 +7,8 @@ module Lutaml
|
|
|
7
7
|
# Metanorma/standards-body pipelines.
|
|
8
8
|
#
|
|
9
9
|
# Approximation notes: descriptions and constraints are prose; boolean
|
|
10
|
-
# schemas render as "any value" / "no value"; external $refs
|
|
11
|
-
# their
|
|
10
|
+
# schemas render as "any value" / "no value"; external HTTP(S) $refs
|
|
11
|
+
# retain their schema URI as a link.
|
|
12
12
|
#
|
|
13
13
|
# With embeddable: true the document title (the level-0 `=` heading)
|
|
14
14
|
# is dropped and definition anchors are prefixed with the schema
|
|
@@ -22,23 +22,34 @@ module Lutaml
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def render
|
|
25
|
-
|
|
26
|
-
parts
|
|
27
|
-
|
|
28
|
-
parts << render_schema(name, schema)
|
|
25
|
+
@linked_schemas = {}
|
|
26
|
+
parts = rendered_sections.map do |name, section|
|
|
27
|
+
@linked_schemas[name] ? "[##{schema_anchor_id(name)}]\n#{section}" : section
|
|
29
28
|
end
|
|
29
|
+
parts.unshift(document_title) unless @embeddable
|
|
30
30
|
"#{parts.join("\n\n").strip}\n"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
+
def rendered_sections
|
|
36
|
+
sections = {}
|
|
37
|
+
until (pending = @schema_set.schemas.keys - sections.keys).empty?
|
|
38
|
+
pending.each do |name|
|
|
39
|
+
sections[name] = render_schema(name, @schema_set.schemas.fetch(name))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
sections
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
def document_title
|
|
36
46
|
title = @schema_set.schemas.values.first&.title
|
|
37
47
|
"= #{title || 'JSON Schema Reference'}"
|
|
38
48
|
end
|
|
39
49
|
|
|
40
50
|
def render_schema(name, schema)
|
|
41
|
-
@
|
|
51
|
+
@current_name = name
|
|
52
|
+
@current_schema = schema
|
|
42
53
|
lines = ["== #{schema.title || name}", ""] + body_lines(schema)
|
|
43
54
|
schema.definition_entries.each do |entry|
|
|
44
55
|
lines << render_definition(entry) << ""
|
|
@@ -161,10 +172,52 @@ module Lutaml
|
|
|
161
172
|
end
|
|
162
173
|
|
|
163
174
|
def ref_link(ref)
|
|
164
|
-
|
|
175
|
+
if ref.start_with?("http://", "https://")
|
|
176
|
+
return "link:#{ref}[#{ref.split(%r{[/#]}).last}]"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
target = @schema_set.resolve_ref(ref, @current_schema)
|
|
180
|
+
resolved = resolved_ref_link(target) if target.is_a?(Schema)
|
|
181
|
+
return resolved if resolved
|
|
182
|
+
|
|
183
|
+
name = local_reference_name(ref)
|
|
165
184
|
"<<#{anchor_id(name)},#{name}>>"
|
|
166
185
|
end
|
|
167
186
|
|
|
187
|
+
def local_reference_name(ref)
|
|
188
|
+
if ref.start_with?("#") && !ref.start_with?("#/")
|
|
189
|
+
named_anchor_definition(ref.delete_prefix("#"))
|
|
190
|
+
else
|
|
191
|
+
ref.split("/").last.to_s.gsub("~1", "/").gsub("~0", "~")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def resolved_ref_link(target)
|
|
196
|
+
@schema_set.schemas.each do |name, schema|
|
|
197
|
+
if schema.equal?(target)
|
|
198
|
+
@linked_schemas[name] = true
|
|
199
|
+
return "<<#{schema_anchor_id(name)},#{schema.title || name}>>"
|
|
200
|
+
end
|
|
201
|
+
entry = schema.definition_entries.find do |definition|
|
|
202
|
+
definition.schema_value.equal?(target)
|
|
203
|
+
end
|
|
204
|
+
return "<<#{anchor_id(entry.name, name)},#{entry.name}>>" if entry
|
|
205
|
+
end
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def schema_anchor_id(name)
|
|
210
|
+
"schema-#{anchor_slug(name)}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def named_anchor_definition(anchor)
|
|
214
|
+
entry = @current_schema.definition_entries.find do |definition|
|
|
215
|
+
value = definition.schema_value
|
|
216
|
+
value.is_a?(Schema) && value.dollar_anchor == anchor
|
|
217
|
+
end
|
|
218
|
+
entry ? entry.name : anchor
|
|
219
|
+
end
|
|
220
|
+
|
|
168
221
|
# definition anchors: `def-<name>` standalone, `def-<schema>-<name>`
|
|
169
222
|
# when embedded so a host document (and sibling includes) cannot
|
|
170
223
|
# collide
|
|
@@ -172,9 +225,10 @@ module Lutaml
|
|
|
172
225
|
name.to_s.gsub(/[^A-Za-z0-9_-]/, "-").downcase
|
|
173
226
|
end
|
|
174
227
|
|
|
175
|
-
def anchor_id(name)
|
|
228
|
+
def anchor_id(name, schema_name = @current_name)
|
|
229
|
+
prefix = @embeddable ? "#{anchor_slug(schema_name)}-" : ""
|
|
176
230
|
id = name.to_s.gsub(/[^A-Za-z0-9_-]/, "-")
|
|
177
|
-
"def-#{
|
|
231
|
+
"def-#{prefix}#{id}"
|
|
178
232
|
end
|
|
179
233
|
|
|
180
234
|
def escape_cell(text)
|
|
@@ -35,16 +35,15 @@ module Lutaml
|
|
|
35
35
|
remote_cache_dir: remote_cache_dir,
|
|
36
36
|
remote_loader: remote_loader)
|
|
37
37
|
paths.each do |path|
|
|
38
|
-
|
|
39
|
-
schema = Schema.from_json(data)
|
|
38
|
+
schema, source = SourceFile.load(path)
|
|
40
39
|
name = File.basename(path, ".*")
|
|
41
|
-
set.add(name, schema, path,
|
|
40
|
+
set.add(name, schema, path, source)
|
|
42
41
|
end
|
|
43
42
|
set
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
def self.load_from_directory(dir, **)
|
|
47
|
-
paths = Dir.glob(File.join(dir,
|
|
46
|
+
paths = Dir.glob(File.join(dir, SourceFile::DIRECTORY_GLOB))
|
|
48
47
|
load_from_files(*paths, base_dir: dir, **)
|
|
49
48
|
end
|
|
50
49
|
|
|
@@ -257,7 +256,7 @@ module Lutaml
|
|
|
257
256
|
if @base_dir
|
|
258
257
|
path = File.join(@base_dir, target_file)
|
|
259
258
|
if File.exist?(path)
|
|
260
|
-
loaded =
|
|
259
|
+
loaded, = SourceFile.load(path)
|
|
261
260
|
add(File.basename(target_file, ".*"), loaded, path)
|
|
262
261
|
return loaded
|
|
263
262
|
end
|
|
@@ -34,11 +34,10 @@ module Lutaml
|
|
|
34
34
|
def self.load_from_files(*paths, base_dir: nil, **)
|
|
35
35
|
store = new(**)
|
|
36
36
|
paths.each do |path|
|
|
37
|
-
|
|
38
|
-
schema = Schema.from_json(data)
|
|
37
|
+
schema, source = SourceFile.load(path)
|
|
39
38
|
name = File.basename(path, ".*")
|
|
40
39
|
schema.dollar_id ||= name
|
|
41
|
-
store.add(name, schema, path,
|
|
40
|
+
store.add(name, schema, path, source)
|
|
42
41
|
end
|
|
43
42
|
store.base_dir = base_dir if base_dir
|
|
44
43
|
store
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yeptris"
|
|
5
|
+
require "yeptris/yaml"
|
|
6
|
+
|
|
7
|
+
module Lutaml
|
|
8
|
+
module Jsonschema
|
|
9
|
+
# Reads a schema source file into JSON text for `Schema.from_json`.
|
|
10
|
+
#
|
|
11
|
+
# `.json` files are returned byte-for-byte. `.yaml`/`.yml` files hold
|
|
12
|
+
# YAML-encoded JSON Schema: they are safe-loaded (no aliases, no Ruby
|
|
13
|
+
# classes), checked to contain only JSON-compatible values, and
|
|
14
|
+
# re-emitted as JSON so the schema goes through the same JSON keyword
|
|
15
|
+
# mapping. Scalar typing is the YAML 1.1 behaviour of the underlying
|
|
16
|
+
# Psych-backed loader (unquoted `yes`/`no` are booleans; unquoted
|
|
17
|
+
# dates and symbols are rejected) — quote such values.
|
|
18
|
+
module SourceFile
|
|
19
|
+
class InvalidYamlError < Error; end
|
|
20
|
+
|
|
21
|
+
YAML_EXTENSIONS = %w[.yaml .yml].freeze
|
|
22
|
+
DIRECTORY_GLOB = "*.{json,yaml,yml}"
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def yaml?(path)
|
|
27
|
+
YAML_EXTENSIONS.include?(File.extname(path.to_s).downcase)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns `[schema, source_json]`.
|
|
31
|
+
def load(path)
|
|
32
|
+
source = read(path)
|
|
33
|
+
[Schema.from_json(source), source]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def read(path)
|
|
37
|
+
data = File.read(path, encoding: "utf-8")
|
|
38
|
+
return data unless yaml?(path)
|
|
39
|
+
|
|
40
|
+
yaml_to_json(data, path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def yaml_to_json(data, path = nil)
|
|
44
|
+
value = safe_load_yaml(data, path)
|
|
45
|
+
check_json_compatible!(value, path, "")
|
|
46
|
+
JSON.pretty_generate(value)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def safe_load_yaml(data, path)
|
|
50
|
+
Yeptris::YAML.safe_load(data, permitted_classes: [], aliases: false)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise InvalidYamlError, "#{path || 'YAML source'}: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_json_compatible!(value, path, pointer)
|
|
56
|
+
case value
|
|
57
|
+
when Hash
|
|
58
|
+
check_mapping!(value, path, pointer)
|
|
59
|
+
when Array
|
|
60
|
+
check_sequence!(value, path, pointer)
|
|
61
|
+
when Float
|
|
62
|
+
invalid!(path, pointer, "non-finite number") unless value.finite?
|
|
63
|
+
when String, Integer, true, false, nil
|
|
64
|
+
nil
|
|
65
|
+
else
|
|
66
|
+
invalid!(path, pointer, "unsupported value #{value.class}")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_mapping!(mapping, path, pointer)
|
|
71
|
+
mapping.each do |key, child|
|
|
72
|
+
unless key.is_a?(String)
|
|
73
|
+
invalid!(path, pointer, "non-string mapping key #{key.inspect}")
|
|
74
|
+
end
|
|
75
|
+
check_json_compatible!(child, path, "#{pointer}/#{escape(key)}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def check_sequence!(sequence, path, pointer)
|
|
80
|
+
sequence.each_with_index do |child, index|
|
|
81
|
+
check_json_compatible!(child, path, "#{pointer}/#{index}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def escape(key)
|
|
86
|
+
key.gsub("~", "~0").gsub("/", "~1")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def invalid!(path, pointer, message)
|
|
90
|
+
location = pointer.empty? ? "/" : pointer
|
|
91
|
+
raise InvalidYamlError,
|
|
92
|
+
"#{path || 'YAML source'} at #{location}: #{message} " \
|
|
93
|
+
"is not JSON-compatible"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/lutaml/jsonschema.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Lutaml
|
|
|
16
16
|
autoload :TupleItem, "lutaml/jsonschema/tuple_item"
|
|
17
17
|
autoload :Schema, "lutaml/jsonschema/schema"
|
|
18
18
|
autoload :ReferenceResolver, "lutaml/jsonschema/reference_resolver"
|
|
19
|
+
autoload :SourceFile, "lutaml/jsonschema/source_file"
|
|
19
20
|
autoload :SchemaSet, "lutaml/jsonschema/schema_set"
|
|
20
21
|
autoload :SchemaStore, "lutaml/jsonschema/schema_store"
|
|
21
22
|
autoload :Combiner, "lutaml/jsonschema/combiner"
|
data/script/axe_audit
CHANGED
|
@@ -72,25 +72,39 @@ page = File.join(dir, "index.html")
|
|
|
72
72
|
File.write(File.join(dir, "audit.html"),
|
|
73
73
|
File.read(page).sub("</body>", "#{INJECT}\n</body>"))
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
75
|
+
# both locales gate: the default LTR render and the Arabic RTL render
|
|
76
|
+
# (the mirrored layout must stay accessible, TODO.sota/09)
|
|
77
|
+
VARIANTS = [
|
|
78
|
+
{ label: "en", query: "" },
|
|
79
|
+
{ label: "ar (rtl)", query: "?locale=ar" },
|
|
80
|
+
].freeze
|
|
81
|
+
|
|
82
|
+
failed = false
|
|
83
|
+
|
|
84
|
+
VARIANTS.each do |variant|
|
|
85
|
+
dom, err, status = Open3.capture3(
|
|
86
|
+
chrome, "--headless=new", "--disable-gpu", "--virtual-time-budget=20000",
|
|
87
|
+
"--dump-dom", "file://#{File.join(dir, 'audit.html')}#{variant[:query]}"
|
|
88
|
+
)
|
|
89
|
+
abort "axe audit: Chrome failed: #{err[0, 400]}" unless status.success?
|
|
90
|
+
|
|
91
|
+
match = dom[/data-axe="([^"]*)"/, 1]
|
|
92
|
+
abort "axe audit (#{variant[:label]}): no result captured" unless match
|
|
93
|
+
|
|
94
|
+
result = JSON.parse(match.gsub(""", '"'))
|
|
95
|
+
abort "axe audit: axe failed: #{result['error']}" if result["error"]
|
|
96
|
+
|
|
97
|
+
if result["violations"].empty?
|
|
98
|
+
puts "axe audit (#{variant[:label]}): PASS (#{result['passes']} rule groups)"
|
|
99
|
+
else
|
|
100
|
+
failed = true
|
|
101
|
+
puts "axe audit (#{variant[:label]}): " \
|
|
102
|
+
"#{result['violations'].sum { |v| v['nodes'] }} violations"
|
|
103
|
+
result["violations"].each do |violation|
|
|
104
|
+
puts " [#{violation['impact']}] #{violation['id']} x#{violation['nodes']}"
|
|
105
|
+
violation["targets"].each { |target| puts " #{target}" }
|
|
106
|
+
end
|
|
94
107
|
end
|
|
95
|
-
exit 1
|
|
96
108
|
end
|
|
109
|
+
|
|
110
|
+
exit 1 if failed
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-jsonschema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|
|
@@ -212,6 +212,7 @@ files:
|
|
|
212
212
|
- lib/lutaml/jsonschema/schema.rb
|
|
213
213
|
- lib/lutaml/jsonschema/schema_set.rb
|
|
214
214
|
- lib/lutaml/jsonschema/schema_store.rb
|
|
215
|
+
- lib/lutaml/jsonschema/source_file.rb
|
|
215
216
|
- lib/lutaml/jsonschema/spa/generator.rb
|
|
216
217
|
- lib/lutaml/jsonschema/spa/metadata.rb
|
|
217
218
|
- lib/lutaml/jsonschema/spa/output_strategy.rb
|