lutaml-jsonschema 0.1.22 → 0.1.24

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +59 -0
  3. data/Rakefile +16 -1
  4. data/benchmark/pipeline_profile.rb +118 -0
  5. data/frontend/dist/app.iife.js +6 -6
  6. data/frontend/dist/style.css +1 -1
  7. data/frontend/src/__tests__/i18n.test.ts +27 -0
  8. data/frontend/src/__tests__/spaDataCompleteness.test.ts +118 -0
  9. data/frontend/src/__tests__/useMarkdownLite.test.ts +47 -0
  10. data/frontend/src/components/AppHeader.vue +6 -5
  11. data/frontend/src/components/AppSidebar.vue +22 -13
  12. data/frontend/src/components/DetailPanel.vue +308 -84
  13. data/frontend/src/components/SchemaBuilder.vue +27 -26
  14. data/frontend/src/components/SearchModal.vue +4 -3
  15. data/frontend/src/components/SeeMore.vue +2 -1
  16. data/frontend/src/composables/useMarkdownLite.ts +79 -3
  17. data/frontend/src/composables/useSchemaTypes.ts +144 -2
  18. data/frontend/src/i18n/index.ts +29 -0
  19. data/frontend/src/i18n/messages.ts +150 -0
  20. data/frontend/src/stores/schemaStore.ts +2 -0
  21. data/frontend/src/stores/uiStore.ts +2 -2
  22. data/frontend/src/style.css +36 -2
  23. data/frontend/src/types.ts +49 -0
  24. data/frontend/src/views/HomeView.vue +3 -3
  25. data/lib/lutaml/jsonschema/cli.rb +154 -15
  26. data/lib/lutaml/jsonschema/combiner.rb +3 -34
  27. data/lib/lutaml/jsonschema/configuration.rb +3 -0
  28. data/lib/lutaml/jsonschema/dialect.rb +49 -0
  29. data/lib/lutaml/jsonschema/diff.rb +275 -0
  30. data/lib/lutaml/jsonschema/exports/adoc.rb +173 -0
  31. data/lib/lutaml/jsonschema/exports/cddl.rb +172 -0
  32. data/lib/lutaml/jsonschema/exports/openapi.rb +48 -0
  33. data/lib/lutaml/jsonschema/exports/xsd.rb +183 -0
  34. data/lib/lutaml/jsonschema/meta_schemas/2019-09/meta/applicator.json +53 -0
  35. data/lib/lutaml/jsonschema/meta_schemas/2019-09/meta/content.json +14 -0
  36. data/lib/lutaml/jsonschema/meta_schemas/2019-09/meta/core.json +54 -0
  37. data/lib/lutaml/jsonschema/meta_schemas/2019-09/meta/format.json +11 -0
  38. data/lib/lutaml/jsonschema/meta_schemas/2019-09/meta/meta-data.json +34 -0
  39. data/lib/lutaml/jsonschema/meta_schemas/2019-09/meta/validation.json +95 -0
  40. data/lib/lutaml/jsonschema/meta_schemas/2019-09/schema.json +42 -0
  41. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/applicator.json +45 -0
  42. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/content.json +14 -0
  43. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/core.json +48 -0
  44. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/format-annotation.json +11 -0
  45. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/meta-data.json +34 -0
  46. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/unevaluated.json +12 -0
  47. data/lib/lutaml/jsonschema/meta_schemas/2020-12/meta/validation.json +95 -0
  48. data/lib/lutaml/jsonschema/meta_schemas/2020-12/schema.json +58 -0
  49. data/lib/lutaml/jsonschema/meta_schemas/NOTICE.md +12 -0
  50. data/lib/lutaml/jsonschema/meta_schemas/draft-04/schema.json +149 -0
  51. data/lib/lutaml/jsonschema/meta_schemas/draft-06/schema.json +155 -0
  52. data/lib/lutaml/jsonschema/meta_schemas/draft-07/schema.json +172 -0
  53. data/lib/lutaml/jsonschema/property_entry.rb +27 -0
  54. data/lib/lutaml/jsonschema/reference_resolver.rb +151 -36
  55. data/lib/lutaml/jsonschema/schema.rb +515 -36
  56. data/lib/lutaml/jsonschema/schema_set.rb +168 -12
  57. data/lib/lutaml/jsonschema/schema_store.rb +4 -4
  58. data/lib/lutaml/jsonschema/spa/generator.rb +4 -2
  59. data/lib/lutaml/jsonschema/spa/metadata.rb +2 -0
  60. data/lib/lutaml/jsonschema/spa/spa_builder.rb +212 -98
  61. data/lib/lutaml/jsonschema/spa/spa_definition.rb +10 -0
  62. data/lib/lutaml/jsonschema/spa/spa_diff_entry.rb +22 -0
  63. data/lib/lutaml/jsonschema/spa/spa_document.rb +3 -0
  64. data/lib/lutaml/jsonschema/spa/spa_property.rb +14 -0
  65. data/lib/lutaml/jsonschema/spa/spa_schema.rb +26 -0
  66. data/lib/lutaml/jsonschema/tuple_item.rb +20 -0
  67. data/lib/lutaml/jsonschema/validator.rb +91 -0
  68. data/lib/lutaml/jsonschema/version.rb +1 -1
  69. data/lib/lutaml/jsonschema.rb +12 -0
  70. data/script/axe_audit +85 -0
  71. metadata +78 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9249e848f66c41b3894e4051e14e53bf7ddfa45201fa058621003aa6c4eef9c
4
- data.tar.gz: 79fbafbb8fee29c7672181288992e9983a98fc8745978a2a40e0eeb5bfa0e10b
3
+ metadata.gz: 229f1667cd05ad90ee3f424ebc68ae1720ec3219eeec3584a1f65d76f09cf90c
4
+ data.tar.gz: 224e628bec6fe5d06986a0dbdee00cb5946b8d39b00e1baa4c3777503db4fbb2
5
5
  SHA512:
6
- metadata.gz: 84ac86a22b2ba9a854a309d708a1ee9cce58128dc6dbd5cc467031363a10f69be49ff550bc12bae9c90f4571c56f05c60b10d92f867911826187d3c6b21adbb5
7
- data.tar.gz: b9a35d6e0b71a3d4e8feb8229e963d3a185829643cbf9d5eaf8b248e30221d88332c7a120dd8ddc1ff41fdc1f84a44c7250fb76732cc315b5bc691b2925159c2
6
+ metadata.gz: 2602ab20233317d4d1ae319739dbc98b86a92598d5decb13a6f5e4e5c3bd0398b69c53698d16c35426599ccd029b565c7a3ceda8546b837d6030d471bcae2c13
7
+ data.tar.gz: f647240a0ce3da5e729da26cbe79e71e02ae3a17bd16b58c71e80bff88711cb8e34d81787caad00522c6494c2ca90c0c0b59966c252f6d96e56e550be6cf355b
data/CLAUDE.md ADDED
@@ -0,0 +1,59 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ `lutaml-jsonschema` parses JSON Schema documents into lutaml-model objects and generates a single-file SPA documentation site from them. It has two halves that must stay in sync:
8
+
9
+ 1. **Ruby gem** (`lib/lutaml/jsonschema/`) — the JSON Schema model and documentation generator.
10
+ 2. **Vue 3 frontend** (`frontend/`) — the SPA itself. The built bundle (`frontend/dist/app.iife.js` + `style.css`) gets inlined into a standalone `index.html`.
11
+
12
+ ## Commands
13
+
14
+ ```sh
15
+ bundle exec rspec # full suite
16
+ bundle exec rspec spec/lutaml/cli_spec.rb # one file
17
+ bundle exec rspec spec/lutaml/cli_spec.rb:76 # one example
18
+ bundle exec rubocop # lint (-a to autofix; .rubocop_todo.yml holds baselined offenses)
19
+ bundle exec rake # spec + rubocop (what CI runs)
20
+
21
+ bundle exec rake frontend:build # npm install + vite build → frontend/dist
22
+ cd frontend && npm test # vitest (frontend unit tests)
23
+
24
+ bundle exec exe/lutaml-jsonschema spa SCHEMA_FILES... [--diff BASELINE...]
25
+ bundle exec exe/lutaml-jsonschema combine|validate SCHEMA_FILES...
26
+ bundle exec exe/lutaml-jsonschema export SCHEMA_FILES... -f cddl|openapi|xsd|adoc
27
+ bundle exec exe/lutaml-jsonschema diff OLD NEW [--format json]
28
+ ```
29
+
30
+ `frontend/dist/` and `Gemfile.lock` are gitignored. `frontend/dist/` must exist (run `rake frontend:build`) for the `spa` command and for packaging — the gemspec ships it via `Dir.glob("frontend/dist/*")`.
31
+
32
+ ## Architecture
33
+
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
+
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.
38
+ - **`SchemaSet`** — in-memory named collection of schemas plus `$ref` resolution (local `#/...` pointers and relative `./file.json` refs through `ReferenceResolver`) and validation.
39
+ - **`SchemaStore`** — `SchemaSet` subclass that adds on-disk persistence through **lutaml-store** (`PackageStore`, package shape from `LjrPackageDefinition`).
40
+ - **`Combiner`** — compatibility facade; bundling lives in `SchemaSet#bundle` (hoists definitions, adds a top-level `$ref` property per source schema, rewrites `#/definitions/` → `#/$defs/`).
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
+ - **`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`.
44
+ - **`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
+ ## JSON engine: yeptris
47
+
48
+ Two distinct paths, both engine-backed:
49
+
50
+ - **Direct parsing** in this gem (`Cli#combine`, `Cli#validate`, `Schema.serialize_additional_properties`) uses `Yeptris::JSON.load` — strict RFC 8259, exact-`JSON.parse` semantics, raises `Yeptris::JSON::ParseError`. JSON **generation** stays on the stdlib `json` gem (`JSON.pretty_generate`); yeptris's JSON surface is load-only.
51
+ - **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
+ - 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
+ - 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.
54
+
55
+ ## Repo notes
56
+
57
+ - `README.md` is unreplaced gem-template boilerplate — don't treat it as documentation.
58
+ - `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
+ - CI is metanorma/ci's `generic-rake` workflow; releases run through `.github/workflows/release.yml`.
data/Rakefile CHANGED
@@ -9,7 +9,17 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ task default: %i[spec rubocop perf]
13
+
14
+ namespace :perf do
15
+ desc "Pipeline performance harness with regression caps (TODO.sota/08)"
16
+ task :profile do
17
+ ruby "benchmark/pipeline_profile.rb"
18
+ end
19
+ end
20
+
21
+ desc "Run the pipeline performance regression gate"
22
+ task perf: "perf:profile"
13
23
 
14
24
  namespace :frontend do
15
25
  desc "Build the Vue frontend"
@@ -19,6 +29,11 @@ namespace :frontend do
19
29
  sh "npm run build"
20
30
  end
21
31
  end
32
+
33
+ desc "axe-core accessibility audit of the generated SPA (needs Chrome)"
34
+ task :audit do
35
+ ruby "script/axe_audit"
36
+ end
22
37
  end
23
38
 
24
39
  task :build_frontend do
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pipeline performance harness (TODO.sota/08).
4
+ #
5
+ # Measures the two budgeted paths on the real-world heroku.json fixture:
6
+ # - Schema.from_json (model construction)
7
+ # - end-to-end SPA generation (SchemaSet + SpaBuilder + inlined HTML)
8
+ #
9
+ # Metrics are reported both absolutely and as a ratio to a stdlib
10
+ # JSON.parse of the same document. The ratio is the machine-independent
11
+ # signal: CI runners vary wildly in absolute speed, but the C parser is
12
+ # a stable per-machine yardstick. Regression caps apply to the ratio.
13
+ #
14
+ # Usage: bundle exec ruby benchmark/pipeline_profile.rb [runs]
15
+
16
+ require "benchmark"
17
+ require "json"
18
+ require "lutaml/jsonschema"
19
+ require "tmpdir"
20
+
21
+ FIXTURES = Dir[File.expand_path("../spec/fixtures/*.json", __dir__)]
22
+ HEROKU = File.expand_path("../spec/fixtures/heroku.json", __dir__)
23
+ DATA = File.read(HEROKU)
24
+ RUNS = (ARGV[0] || 3).to_i
25
+
26
+ # Regression caps, calibrated per environment: the ratio looked
27
+ # machine-independent but is not — CI runners (2 vCPU, shared) inflate the
28
+ # Ruby-vs-C balance ~2x versus a local M-series machine (first real CI run:
29
+ # from_json 5901x, e2e 7853x, suite itself green). Caps catch real
30
+ # regressions (1.3x+ of the environment's own baseline) without flaking.
31
+ # Aspirational budgets are printed separately.
32
+ IN_CI = ["true", "1"].include?(ENV.fetch("CI", nil))
33
+ CAP_FROM_JSON_RATIO, CAP_E2E_RATIO =
34
+ if IN_CI
35
+ [7_500, 10_000]
36
+ else
37
+ [2_600, 5_000]
38
+ end
39
+
40
+ def median(values)
41
+ values.sort[values.length / 2]
42
+ end
43
+
44
+ def measure(runs, &)
45
+ Array.new(runs) { Benchmark.realtime(&) }
46
+ end
47
+
48
+ Schema = Lutaml::Jsonschema::Schema
49
+
50
+ # Warmup
51
+ Schema.from_json(DATA)
52
+ JSON.parse(DATA)
53
+
54
+ parse_times = measure(RUNS) { JSON.parse(DATA) }
55
+ from_json_times = measure(RUNS) { Schema.from_json(DATA) }
56
+
57
+ tmp = Dir.mktmpdir
58
+ frontend_built = File.exist?(
59
+ File.join(Lutaml::Jsonschema::Spa::VueInlinedStrategy::FRONTEND_DIST,
60
+ "app.iife.js"),
61
+ )
62
+ generate = lambda do
63
+ set = Lutaml::Jsonschema::SchemaSet.load_from_files(*FIXTURES)
64
+ Lutaml::Jsonschema::Spa::Generator.new(set, tmp).generate
65
+ end
66
+ if frontend_built
67
+ generate.call
68
+ e2e_times = measure(RUNS) { generate.call }
69
+ else
70
+ e2e_times = nil
71
+ end
72
+
73
+ parse_ms = median(parse_times) * 1000
74
+ from_json_ms = median(from_json_times) * 1000
75
+ e2e_ms = e2e_times && (median(e2e_times) * 1000)
76
+
77
+ from_json_ratio = from_json_ms / parse_ms
78
+ e2e_ratio = e2e_ms && (e2e_ms / parse_ms)
79
+
80
+ per_100kb = from_json_ms / (DATA.bytesize / 100_000.0)
81
+ html_bytes = if e2e_ms
82
+ File.size(File.join(tmp, "index.html"))
83
+ end
84
+
85
+ puts "heroku.json: #{DATA.bytesize} bytes, #{FIXTURES.length} fixtures, " \
86
+ "runs=#{RUNS}"
87
+ puts format("JSON.parse (reference): %<ms>10.3f ms", ms: parse_ms)
88
+ puts format("Schema.from_json: %<ms>10.3f ms " \
89
+ "(%<per>0f ms/100KB, %<ref>0fx ref)",
90
+ ms: from_json_ms, per: per_100kb, ref: from_json_ratio)
91
+ if e2e_ms
92
+ puts format("e2e spa generate: %<ms>10.3f ms (%<ref>0fx ref)",
93
+ ms: e2e_ms, ref: e2e_ratio)
94
+ puts format("index.html: %<bytes>10d bytes " \
95
+ "(%<ratio>0.2fx input)",
96
+ bytes: html_bytes, ratio: html_bytes.to_f / DATA.bytesize)
97
+ else
98
+ puts "e2e spa generate: skipped (frontend/dist not built)"
99
+ end
100
+ puts
101
+ puts "environment: #{IN_CI ? 'CI' : 'local'}"
102
+ puts "budgets (aspirational): from_json <= #{DATA.bytesize / 1000} ms " \
103
+ "(100 ms/100KB), e2e <= 1 s/MB"
104
+ puts "regression caps: from_json ratio <= #{CAP_FROM_JSON_RATIO}, " \
105
+ "e2e ratio <= #{CAP_E2E_RATIO}"
106
+
107
+ failures = []
108
+ failures << "from_json ratio #{from_json_ratio.round} > #{CAP_FROM_JSON_RATIO}" \
109
+ if from_json_ratio > CAP_FROM_JSON_RATIO
110
+ failures << "e2e ratio #{e2e_ratio.round} > #{CAP_E2E_RATIO}" \
111
+ if e2e_ratio && e2e_ratio > CAP_E2E_RATIO
112
+
113
+ if failures.empty?
114
+ puts "PERF OK"
115
+ else
116
+ warn "PERF REGRESSION: #{failures.join('; ')}"
117
+ exit 1
118
+ end