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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5def50283e10b93eae1ca02c54374c259f64f8b530b9edde9baddc7aa6e17fee
4
- data.tar.gz: 5a6367729b6f4543bcc3d09246bdf663918e2bd977cb2591065563887e8c0fec
3
+ metadata.gz: b7c72a0b9f42bf072c40e4f52b986ba536f59ed1eaa96650bd6df86d469a16e1
4
+ data.tar.gz: 818f5e758c204d40d20f371ccb3cc6e772ab2c9e7b54e59c5f02de9b0050b9db
5
5
  SHA512:
6
- metadata.gz: fa6880c0ef08bdaa4277f1ca2d71f9943fc7a6facfe6071669d0141ed038949b7cf652f32e1b668e7469d3dac2ac57671696114a2d31e42a12549392132230c7
7
- data.tar.gz: ab108839554ad9cdd16ec05eb61756fba5e01a51d6ac0b9f1b541d5ad6939de2ec40696e44a39aaf2ceeeed2044659dfee0d5ce8224100509ab960d87489e83c
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
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/lutaml/jsonschema`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
21
+ ```bash
22
+ gem install lutaml-jsonschema
23
+ ```
10
24
 
11
- Install the gem and add to the application's Gemfile by executing:
25
+ or add to your Gemfile:
12
26
 
13
27
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
28
+ bundle add lutaml-jsonschema
15
29
  ```
16
30
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
31
+ ## CLI
18
32
 
19
33
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- ## Usage
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
- TODO: Write usage instructions here
54
+ ## Library
26
55
 
27
- ## Development
56
+ ```ruby
57
+ require "lutaml/jsonschema"
28
58
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/lutaml-jsonschema. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/lutaml-jsonschema/blob/main/CODE_OF_CONDUCT.md).
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
- ## Code of Conduct
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
- Everyone interacting in the Lutaml::Jsonschema project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/lutaml-jsonschema/blob/main/CODE_OF_CONDUCT.md).
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 link by
11
- # their last path segment.
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
- parts = []
26
- parts << document_title unless @embeddable
27
- @schema_set.schemas.each do |name, schema|
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
- @anchor_prefix = @embeddable ? "#{anchor_slug(name)}-" : ""
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
- name = ref.split("/").last
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-#{@anchor_prefix}#{id}"
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
- data = File.read(path, encoding: "utf-8")
39
- schema = Schema.from_json(data)
38
+ schema, source = SourceFile.load(path)
40
39
  name = File.basename(path, ".*")
41
- set.add(name, schema, path, data)
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, "*.json"))
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 = Schema.from_json(File.read(path))
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
- data = File.read(path, encoding: "utf-8")
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, data)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Jsonschema
5
- VERSION = "0.1.25"
5
+ VERSION = "0.1.26"
6
6
  end
7
7
  end
@@ -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
- dom, err, status = Open3.capture3(
76
- chrome, "--headless=new", "--disable-gpu", "--virtual-time-budget=20000",
77
- "--dump-dom", "file://#{File.join(dir, 'audit.html')}"
78
- )
79
- abort "axe audit: Chrome failed: #{err[0, 400]}" unless status.success?
80
-
81
- match = dom[/data-axe="([^"]*)"/, 1]
82
- abort "axe audit: no result captured" unless match
83
-
84
- result = JSON.parse(match.gsub("&quot;", '"'))
85
- abort "axe audit: axe failed: #{result['error']}" if result["error"]
86
-
87
- if result["violations"].empty?
88
- puts "axe audit: PASS (#{result['passes']} rule groups, no violations)"
89
- else
90
- puts "axe audit: #{result['violations'].sum { |v| v['nodes'] }} violations"
91
- result["violations"].each do |violation|
92
- puts " [#{violation['impact']}] #{violation['id']} x#{violation['nodes']}"
93
- violation["targets"].each { |target| puts " #{target}" }
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("&quot;", '"'))
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.25
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-22 00:00:00.000000000 Z
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