lutaml-model 0.8.56 → 0.8.106

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/scripts/test_installed_gem.rb +58 -0
  3. data/.github/workflows/downstream-performance.yml +28 -3
  4. data/.github/workflows/js-pr-check.yml +4 -0
  5. data/.github/workflows/js-sync.yml +4 -0
  6. data/.github/workflows/opal.yml +9 -0
  7. data/.github/workflows/rake.yml +6 -41
  8. data/.github/workflows/release.yml +41 -0
  9. data/Gemfile +17 -3
  10. data/bench/bench_alloc_audit.rb +56 -0
  11. data/bench/bench_common.rb +11 -0
  12. data/bench/bench_merge_attempts.rb +49 -0
  13. data/docs/_guides/gem-integration.adoc +85 -0
  14. data/docs/_guides/grammar-backed-formats.adoc +69 -0
  15. data/docs/_guides/index.adoc +8 -1
  16. data/docs/_guides/native-engines.adoc +23 -5
  17. data/docs/_guides/schema-generation.adoc +25 -0
  18. data/docs/_guides/type-registers-contexts.adoc +126 -0
  19. data/lib/compat/opal/lutaml_model_boot.rb +7 -5
  20. data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
  21. data/lib/lutaml/key_value/data_model/element.rb +5 -1
  22. data/lib/lutaml/key_value/mapping_rule.rb +7 -0
  23. data/lib/lutaml/key_value/transform.rb +29 -24
  24. data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
  25. data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
  26. data/lib/lutaml/key_value/transformation.rb +64 -116
  27. data/lib/lutaml/model/adapter_resolver.rb +7 -3
  28. data/lib/lutaml/model/attribute.rb +74 -15
  29. data/lib/lutaml/model/cached_type_resolver.rb +13 -6
  30. data/lib/lutaml/model/configuration.rb +13 -0
  31. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
  33. data/lib/lutaml/model/render_policy.rb +26 -8
  34. data/lib/lutaml/model/schema/base_schema.rb +15 -0
  35. data/lib/lutaml/model/schema/generator/definition.rb +52 -8
  36. data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
  37. data/lib/lutaml/model/schema.rb +4 -2
  38. data/lib/lutaml/model/serializable.rb +33 -0
  39. data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
  40. data/lib/lutaml/model/serialize/builder.rb +22 -0
  41. data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
  42. data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
  43. data/lib/lutaml/model/serialize/initialization.rb +54 -7
  44. data/lib/lutaml/model/serialize/model_import.rb +60 -0
  45. data/lib/lutaml/model/serialize.rb +14 -6
  46. data/lib/lutaml/model/type/time.rb +6 -0
  47. data/lib/lutaml/model/type/value.rb +9 -7
  48. data/lib/lutaml/model/type_resolver.rb +7 -5
  49. data/lib/lutaml/model/utils.rb +18 -5
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/model.rb +7 -0
  52. data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
  53. data/lib/lutaml/xml/adapter_loader.rb +5 -2
  54. data/lib/lutaml/xml/format.rb +18 -16
  55. data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
  56. data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
  57. data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
  58. data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
  59. data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
  60. data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
  61. data/lib/lutaml/xml/leptris.rb +46 -0
  62. data/lib/lutaml/xml/mapping.rb +71 -18
  63. data/lib/lutaml/xml/model_transform.rb +231 -8
  64. data/lib/lutaml/xml/namespace.rb +18 -8
  65. data/lib/lutaml/xml/namespace_collector.rb +17 -1
  66. data/lib/lutaml/xml/namespace_needs.rb +77 -49
  67. data/lib/lutaml/xml/parse_session.rb +7 -19
  68. data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
  69. data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
  70. data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
  71. data/lib/lutaml/xml/transformation.rb +15 -1
  72. data/lib/lutaml/xml/type/serializers.rb +8 -1
  73. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
  74. data/lib/tasks/performance_probe_plan_walk.rb +1 -1
  75. data/lutaml-model.gemspec +7 -4
  76. data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
  77. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
  78. data/spec/lutaml/key_value/transformation_spec.rb +87 -21
  79. data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
  80. data/spec/lutaml/model/collection_validation_spec.rb +265 -0
  81. data/spec/lutaml/model/desc_spec.rb +50 -0
  82. data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
  83. data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
  84. data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
  85. data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
  86. data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
  87. data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
  88. data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
  89. data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
  90. data/spec/lutaml/model/store_spec.rb +6 -2
  91. data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
  92. data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
  93. data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
  94. data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
  95. data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
  96. data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
  97. data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
  98. data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
  99. data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
  100. data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
  101. data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
  102. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
  103. data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
  104. data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
  105. data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
  106. data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
  107. data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
  108. data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
  109. data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
  110. data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
  111. data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
  112. data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
  113. data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
  114. data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
  115. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
  116. data/spec/person_spec.rb +8 -0
  117. metadata +48 -17
  118. data/lib/lutaml/xml/plan_compiler.rb +0 -332
  119. data/lib/lutaml/xml/plan_hydrator.rb +0 -367
  120. data/lib/lutaml/xml/plan_order.rb +0 -53
  121. data/lib/lutaml/xml/plan_serializer.rb +0 -252
  122. data/lib/lutaml/xml/plan_walk.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8eaff3e9ef7cbf71d8cd5adbf05161b81c43399c92a1c741023005722c190025
4
- data.tar.gz: bd4383a644f8878226034c191215780c38b5f1beee2cdf196e26d31067908878
3
+ metadata.gz: cc1bb0f95876011fac44764d7ea55c334d24ea2a20911e46f4200448cf50cc1e
4
+ data.tar.gz: 23f02dbcf3dac25a19a50f4a8bf75b81d642b64514c0aeb9a820d727be63406f
5
5
  SHA512:
6
- metadata.gz: 00453571fbb8c6b2540f3d545fe53d0c664fa18301797e5192699a6dded982ca9280a4ac39dcc2a7c60f03746ee0dc986e9571c05f9f387f503ba6ab2b522d5c
7
- data.tar.gz: bdf133791492a00bb00babcc2b1037366918e6eba7bb05001d4b5748152249c7e45c47c0dbd099c72e0f99acf949febf8ae36f43bb99b43b35874112da3f6312
6
+ metadata.gz: 8d649b8eef84ef0cfa60ec5fec9e70b0fa45cdc44508325f12c7a17d66e3ca437535c3bf56bc725217f73d305046c9102d7dba067148f4efdec8996d2f2f8ec2
7
+ data.tar.gz: d81707bac3fb9bf9ebc3091c421253e3b9b8abd085891e03debcd19674af75ae5e6dc5f1b724fbb05e160847694dcc8752e9eb1fb17425a394c1babc67777273
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Smoke test for the INSTALLED lutaml-model gem (parsanol-ruby
4
+ # pattern, lutaml-model#833): runs against the gem's own load paths —
5
+ # never the repo — and exercises the boot surface a packaging
6
+ # regression would break: every format round-trip and the Opal boot
7
+ # manifest's autoload coverage.
8
+
9
+ gem_root = Gem::Specification.find_by_name("lutaml-model").full_gem_path
10
+ raise "installed lutaml-model not found" unless gem_root
11
+
12
+ $LOAD_PATH.unshift(File.join(gem_root, "lib"))
13
+ require "lutaml/model"
14
+
15
+ # Boot manifest: every autoload in lib/compat/opal/lutaml_model_boot.rb
16
+ # must resolve from the installed gem alone.
17
+ boot = File.join(gem_root, "lib", "compat", "opal", "lutaml_model_boot.rb")
18
+ if File.exist?(boot)
19
+ manifest = File.read(boot)
20
+ missing = manifest.scan(/autoload[^\n]*["']([^"']+)["']/).flatten.select do |path|
21
+ !File.exist?(File.join(gem_root, "lib", path))
22
+ end
23
+ raise "boot manifest references missing files: #{missing.inspect}" unless missing.empty?
24
+ end
25
+
26
+ class Smoke < Lutaml::Model::Serializable
27
+ attribute :name, :string
28
+ attribute :count, :integer
29
+ attribute :tags, :string, collection: true
30
+
31
+ xml do
32
+ root "smoke"
33
+ map_attribute "name", to: :name
34
+ map_element "count", to: :count
35
+ map_element "tag", to: :tags
36
+ end
37
+ end
38
+
39
+ require "tempfile"
40
+ model = Smoke.new(name: "smoke", count: 3, tags: %w[a b])
41
+
42
+ round_trips = {
43
+ xml: -> { Smoke.from_xml(model.to_xml) },
44
+ json: -> { Smoke.from_json(model.to_json) },
45
+ yaml: -> { Smoke.from_yaml(model.to_yaml) },
46
+ toml: -> { Smoke.from_toml(model.to_toml) },
47
+ hash: -> { Smoke.from_hash(model.to_hash) },
48
+ }
49
+ round_trips.each do |format, parse|
50
+ round = parse.call
51
+ raise "#{format}: name lost" if round.name != "smoke"
52
+ raise "#{format}: count lost" if round.count != 3
53
+ raise "#{format}: tags lost" if Array(round.tags).sort != %w[a b]
54
+ end
55
+
56
+ puts "installed-gem smoke OK (#{gem_root}, " \
57
+ "lutaml-model #{Lutaml::Model::VERSION}, " \
58
+ "#{round_trips.keys.join("/")})"
@@ -17,9 +17,16 @@ permissions:
17
17
  jobs:
18
18
  benchmark:
19
19
  runs-on: ubuntu-latest
20
- timeout-minutes: 30
20
+ timeout-minutes: 60
21
21
  strategy:
22
22
  fail-fast: false
23
+ # Serializes the corpus legs: concurrent legs on shared runners
24
+ # produce contention measurements that misfire the absolute gates
25
+ # (lutaml-model#834: niso 0.921s vs a 0.25s main-passing gate).
26
+ # max-parallel, NOT a job-level concurrency group: with a
27
+ # matrix, every leg lands in the same group and GitHub cancels
28
+ # four of the five at start.
29
+ max-parallel: 1
23
30
  matrix:
24
31
  include:
25
32
  - downstream: xmi
@@ -109,7 +116,16 @@ jobs:
109
116
  File.write("Gemfile", out.join)
110
117
  ' /tmp/main-checkout
111
118
  bundle install --jobs=4
112
- bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb
119
+ # lutaml-model#834: min-of-N across attempts — contention
120
+ # inflates single-shot timings, so run three attempts and let
121
+ # the merge keep the least-interfered measurement per fixture.
122
+ for i in 1 2 3; do
123
+ bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb || \
124
+ echo "::warning::baseline attempt $i errored (#834) — continuing with remaining attempts"
125
+ cp "$BENCH_JSON" "$BENCH_JSON.attempt$i" 2>/dev/null || true
126
+ done
127
+ ruby ${{ github.workspace }}/bench/bench_merge_attempts.rb \
128
+ "$BENCH_JSON" "$BENCH_JSON.attempt1" "$BENCH_JSON.attempt2" "$BENCH_JSON.attempt3"
113
129
 
114
130
  - name: Run current benchmark (PR branch)
115
131
  env:
@@ -117,6 +133,9 @@ jobs:
117
133
  ${{ matrix.env_key }}: ${{ github.workspace }}/downstream
118
134
  LUTAML_MODEL_DIR: ${{ github.workspace }}
119
135
  BENCH_JSON: /tmp/bench_current_${{ matrix.downstream }}.json
136
+ # TODO.perf 17: corpus opt-out histogram in the job log —
137
+ # which clauses (if any) keep this corpus interpretive.
138
+ PLAN_COMPILE_STATS: "1"
120
139
  run: |
121
140
  cd downstream
122
141
  ruby -e '
@@ -134,7 +153,13 @@ jobs:
134
153
  File.write("Gemfile", out.join)
135
154
  ' ${{ github.workspace }}
136
155
  bundle install --jobs=4
137
- bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb
156
+ for i in 1 2 3; do
157
+ bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb || \
158
+ echo "::warning::current attempt $i errored (#834) — continuing with remaining attempts"
159
+ cp "$BENCH_JSON" "$BENCH_JSON.attempt$i" 2>/dev/null || true
160
+ done
161
+ ruby ${{ github.workspace }}/bench/bench_merge_attempts.rb \
162
+ "$BENCH_JSON" "$BENCH_JSON.attempt1" "$BENCH_JSON.attempt2" "$BENCH_JSON.attempt3"
138
163
 
139
164
  - name: Compare baseline vs current
140
165
  run: |
@@ -31,6 +31,10 @@ permissions:
31
31
  contents: read
32
32
  pull-requests: write
33
33
 
34
+ concurrency:
35
+ group: js-pr-check-${{ github.ref }}
36
+ cancel-in-progress: true
37
+
34
38
  jobs:
35
39
  trigger:
36
40
  runs-on: ubuntu-latest
@@ -20,6 +20,10 @@ on:
20
20
  permissions:
21
21
  contents: read
22
22
 
23
+ concurrency:
24
+ group: js-sync-${{ github.ref }}
25
+ cancel-in-progress: true
26
+
23
27
  jobs:
24
28
  trigger:
25
29
  runs-on: ubuntu-latest
@@ -9,9 +9,18 @@ on:
9
9
  permissions:
10
10
  contents: read
11
11
 
12
+ concurrency:
13
+ group: opal-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
12
16
  jobs:
13
17
  test:
14
18
  runs-on: ubuntu-latest
19
+ env:
20
+ # The Opal leg is the ONLY consumer of the vendored
21
+ # Opal-compatibility forks (#835 step 2): MRI legs run released
22
+ # oga/ruby-ll.
23
+ LUTAML_OPAL_FORKS: "1"
15
24
  steps:
16
25
  - uses: actions/checkout@v6
17
26
  with:
@@ -23,10 +23,13 @@ jobs:
23
23
  os: [ubuntu-latest, macos-latest, windows-latest]
24
24
  ruby: ["3.3", "3.4", "4.0"]
25
25
  experimental: [false]
26
- include:
26
+ # ruby head runs on ubuntu/macos only: windows-head cannot build
27
+ # the released ruby-ll C extension (and the vendored forks exist
28
+ # for Opal), so that cell was permanently red with zero signal
29
+ # (#832 follow-up).
30
+ exclude:
27
31
  - os: windows-latest
28
32
  ruby: head
29
- experimental: true
30
33
  steps:
31
34
  - uses: actions/checkout@v6
32
35
  with:
@@ -36,50 +39,12 @@ jobs:
36
39
  uses: ruby/setup-ruby@v1
37
40
  with:
38
41
  ruby-version: ${{ matrix.ruby }}
39
- # bundler-cache disabled: the path-source oga and ruby-ll forks
40
- # need ragel + ruby-ll outputs generated (rake vendor:prepare)
41
- # before their C extensions can compile.
42
+ # bundler-cache disabled: adapter pins resolve per run.
42
43
  bundler-cache: false
43
44
 
44
- - name: Install ragel (Linux)
45
- if: runner.os == 'Linux'
46
- run: sudo apt-get update && sudo apt-get install -y ragel
47
-
48
- - name: Install ragel (macOS)
49
- if: runner.os == 'macOS'
50
- run: brew install ragel
51
-
52
- - name: Install ragel (Windows)
53
- if: runner.os == 'Windows'
54
- run: choco install -y ragel
55
- continue-on-error: true
56
-
57
- - name: Install ruby-ll (for rake vendor:prepare)
58
- run: gem install ruby-ll --no-document
59
-
60
- - name: Generate ragel / ruby-ll outputs in vendored forks
61
- # Run before bundle install: the path-source oga/ruby-ll forks
62
- # gitignore their generated .rb/.c files. bundle install compiles
63
- # the C extensions via each fork's extconf.rb, which needs
64
- # ext/c/lexer.c to exist.
65
- run: rake vendor:prepare
66
-
67
- - name: Configure bundler
68
- # ruby-ll's C extension needs an older C standard on some compilers.
69
- run: |
70
- mkdir -p .bundle
71
- echo '---' > .bundle/config
72
- echo 'BUNDLE_BUILD__RUBY___LL: "--with-cflags=-std=gnu17"' >> .bundle/config
73
-
74
45
  - name: Install dependencies
75
46
  run: bundle install
76
47
 
77
- - name: Compile liboga / libll native extensions
78
- # Bundler does not reliably build path-source gems' native
79
- # extensions; compile them explicitly so `require "oga"` (which
80
- # requires libll) resolves under CRuby.
81
- run: rake vendor:compile
82
-
83
48
  - name: Run tests
84
49
  run: bundle exec rake spec
85
50
 
@@ -20,6 +20,12 @@ permissions:
20
20
  contents: write
21
21
  id-token: write
22
22
 
23
+ # One release at a time: concurrent bumps race the version commit
24
+ # and half-published gems are worse than a queued release.
25
+ concurrency:
26
+ group: release
27
+ cancel-in-progress: false
28
+
23
29
  jobs:
24
30
  release:
25
31
  uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
@@ -33,3 +39,38 @@ jobs:
33
39
  # pat_token is optional (private-gem checkout + GH Packages). Keep if
34
40
  # the org still uses private deps; drop once pure public.
35
41
  pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}
42
+
43
+ # lutaml-model#833: the reusable release pushes the gem without ever
44
+ # installing it — packaging regressions (gemspec globs, autoload
45
+ # drift, fresh-bundler resolution) pass in-repo and break consumers.
46
+ # Install the just-published version from rubygems.org and exercise
47
+ # the boot surface from the gem's own load paths.
48
+ smoke:
49
+ needs: release
50
+ runs-on: ubuntu-latest
51
+ steps:
52
+ - uses: actions/checkout@v6
53
+ # The release job commits the version bump to main while this job
54
+ # runs; a checkout of the dispatch-time HEAD reads the previous
55
+ # version and smoke-tests the wrong gem (0.8.60 was smoke-checked
56
+ # for the 0.8.61 release). Pull post-bump main before resolving.
57
+ with:
58
+ fetch-depth: 5
59
+ - uses: ruby/setup-ruby@v1
60
+ with:
61
+ ruby-version: "3.4"
62
+ - name: Resolve the released version
63
+ id: version
64
+ # From the version file, not git describe: the default checkout
65
+ # is shallow with no tags, so git describe failed and the empty
66
+ # version produced `gem install "lutaml-model:"`.
67
+ run: |
68
+ git pull --ff-only origin main
69
+ echo "version=$(ruby -e 'print File.read(%q{lib/lutaml/model/version.rb})[/VERSION\s*=\s*"([^"]+)"/, 1]')" >> "$GITHUB_OUTPUT"
70
+ - name: Install and smoke the released gem
71
+ run: |
72
+ gem install "lutaml-model:${{ steps.version.outputs.version }}" --no-document
73
+ # The round-trip script exercises all five formats; the TOML
74
+ # legs require an adapter gem to be present.
75
+ gem install tomlib toml-rb --no-document
76
+ ruby .github/scripts/test_installed_gem.rb
data/Gemfile CHANGED
@@ -10,8 +10,17 @@ gemspec
10
10
  # in lib/oga.rb / lib/ll/setup.rb that selects the pure-Ruby implementation
11
11
  # when RUBY_PLATFORM == 'opal'. Under CRuby/JRuby the forks behave
12
12
  # identically to upstream (the conditional falls through to liboga/libll).
13
- gem "oga", path: "vendor/opal-oga"
14
- gem "ruby-ll", path: "vendor/opal-ruby-ll"
13
+ # The vendored forks are Opal-compatibility shims (pure-Ruby lexer)
14
+ # used by the Opal CI leg ONLY (LUTAML_OPAL_FORKS=1 in opal.yml) —
15
+ # #835 step 2. Every MRI leg runs against released oga/ruby-ll; the
16
+ # forks' C extension is upstream's and adds nothing under MRI.
17
+ if ENV["LUTAML_OPAL_FORKS"]
18
+ gem "oga", path: "vendor/opal-oga"
19
+ gem "ruby-ll", path: "vendor/opal-ruby-ll"
20
+ else
21
+ gem "oga"
22
+ gem "ruby-ll"
23
+ end
15
24
 
16
25
  # needed for liquid with ruby 3.4
17
26
  gem "base64"
@@ -35,7 +44,12 @@ gem "rexml"
35
44
  # Native engines stay development-only: every adapter detects and
36
45
  # degrades gracefully at runtime (safe requires + prefer-if-available).
37
46
  # yeptris ships no Windows prebuilt platform gem.
38
- gem "leptris"
47
+ # leptris floor: the single capability line for the plan fast path
48
+ # (see Lutaml::Xml::Leptris::MIN_LEPTRIS_VERSION) — child snapshots,
49
+ # exact-URI attribute rows, nested-attribute capture, wildcard rows,
50
+ # walk-side ns retention and leniency, :unqualified rows, and the
51
+ # address-based text attach all ride it.
52
+ gem "leptris", ">= 1.9.336.0"
39
53
  gem "teptris"
40
54
  gem "yeptris" unless Gem.win_platform?
41
55
  # TODO: revert rng branch to main when lutaml/rng#32 is merged
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module AllocAudit
6
+ class Row < Lutaml::Model::Serializable
7
+ attribute :id, :integer
8
+ attribute :name, :string
9
+
10
+ xml do
11
+ root "row"
12
+ map_element "id", to: :id
13
+ map_element "name", to: :name
14
+ end
15
+
16
+ json do
17
+ map "id", to: :id
18
+ map "name", to: :name
19
+ end
20
+ end
21
+
22
+ class Doc < Lutaml::Model::Serializable
23
+ attribute :rows, AllocAudit::Row, collection: true
24
+
25
+ xml do
26
+ root "doc"
27
+ map_element "row", to: :rows
28
+ end
29
+
30
+ json do
31
+ map "rows", to: :rows
32
+ end
33
+ end
34
+
35
+ DOC = Doc.new(rows: Array.new(500) { |i| Row.new(id: i, name: "n#{i}") })
36
+
37
+ def self.alloc
38
+ GC.start
39
+ before = GC.stat(:total_allocated_objects)
40
+ yield
41
+ GC.stat(:total_allocated_objects) - before
42
+ end
43
+
44
+ 3.times do
45
+ DOC.to_xml
46
+ DOC.to_json
47
+ DOC.to_hash
48
+ end
49
+
50
+ puts "rows=500"
51
+ puts "to_xml_alloc=#{alloc { DOC.to_xml }}"
52
+ puts "to_json_alloc=#{alloc { DOC.to_json }}"
53
+ puts "to_hash_alloc=#{alloc { DOC.to_hash }}"
54
+ puts "from_xml_alloc=#{alloc { Doc.from_xml(DOC.to_xml) }}"
55
+ puts "from_json_alloc=#{alloc { Doc.from_json(DOC.to_json) }}"
56
+ end
@@ -12,6 +12,17 @@
12
12
  # bundle exec ruby bench/bench_uniword.rb
13
13
 
14
14
  require "bundler/setup"
15
+
16
+ # TODO.perf/15: run any benchmark with PLAN_COMPILE_STATS=1 to append
17
+ # the plan-compiler opt-out histogram (which clauses keep a corpus on
18
+ # the interpretive path) after the benchmark output.
19
+ if ENV["PLAN_COMPILE_STATS"]
20
+ at_exit do
21
+ require "pp"
22
+ puts "\nPlanCompiler opt-out histogram:"
23
+ pp(Lutaml::Xml::Leptris::PlanCompiler.plan_stats)
24
+ end
25
+ end
15
26
  require "benchmark"
16
27
  require "fileutils"
17
28
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lutaml-model#834: merge N benchmark-attempt JSONs into one result set,
4
+ # taking the per-fixture minimum of every metric. Contention inflates
5
+ # timings; the minimum across attempts is the least-interfered
6
+ # measurement, so gates evaluated on merged output misfire only when
7
+ # EVERY attempt was slow.
8
+ #
9
+ # Usage: ruby bench_merge_attempts.rb <merged.json> <attempt1.json> [attempt2.json ...]
10
+ #
11
+ # Attempt files may be missing (crashed attempt): missing files are
12
+ # skipped; if none exist, nothing is written and the caller's compare
13
+ # step fails on the missing merged file.
14
+ if ARGV.length < 3
15
+ warn "Usage: #{$PROGRAM_NAME} <merged.json> <attempt1.json> [attempt2.json ...]"
16
+ exit 1
17
+ end
18
+
19
+ merged_path = ARGV[0]
20
+ attempt_paths = ARGV[1..]
21
+
22
+ require "json"
23
+
24
+ best = {}
25
+ attempt_paths.each do |path|
26
+ next unless File.exist?(path)
27
+
28
+ results = JSON.parse(File.read(path))
29
+ results.each do |label, metrics|
30
+ cur = best[label]
31
+ if cur
32
+ cur["avg_time"] = [cur["avg_time"], metrics["avg_time"]].min
33
+ cur["min_time"] = [cur["min_time"], metrics["min_time"]].min
34
+ cur["max_time"] = [cur["max_time"], metrics["max_time"]].min
35
+ cur["allocations"] = [cur["allocations"], metrics["allocations"]].min
36
+ cur["ips"] = [cur["ips"], metrics["ips"]].max
37
+ else
38
+ best[label] = metrics.dup
39
+ end
40
+ end
41
+ end
42
+
43
+ if best.nil? || best.empty?
44
+ warn "No attempt JSONs found among: #{attempt_paths.join(', ')}"
45
+ exit 1
46
+ end
47
+
48
+ File.write(merged_path, JSON.pretty_generate(best))
49
+ puts "Merged #{attempt_paths.size} attempt file(s) into #{merged_path} (#{best.size} fixtures)"
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: Integrating lutaml-model in a Gem
3
+ nav_order: 21
4
+ ---
5
+
6
+ = Integrating lutaml-model in a gem
7
+
8
+ For gem authors, the integration contract is simple: **depend on
9
+ `lutaml-model` and nothing else format-specific.** Every XML, YAML,
10
+ JSON, and TOML engine is resolved at runtime, and the standard
11
+ adapters (Nokogiri through moxml, Psych, the `json` gem, toml-rb
12
+ compatible engines) cover a working default everywhere.
13
+
14
+ == Declaring the dependency
15
+
16
+ [source,ruby]
17
+ ----
18
+ spec.add_dependency "lutaml-model", "~> 0.8"
19
+ ----
20
+
21
+ Do not add Nokogiri, Ox, Oga, leptris, yeptris, or teptris to your
22
+ gemspec. All engines are optional at runtime:
23
+
24
+ * when an engine gem is in the end user's bundle, the matching
25
+ adapters prefer it (see the
26
+ link:../native-engines[native engines guide]);
27
+ * when it is not, adapters fall back to the standard engine for
28
+ that format — parsing and generation semantics are identical,
29
+ error classes are re-raised as their standard-library equivalents.
30
+
31
+ There is no code path to write, no require order to respect, and no
32
+ capability probe to perform in your gem.
33
+
34
+ == Testing across adapters
35
+
36
+ Your test suite should exercise the adapters your users may run.
37
+ Lutaml::Model's own suite parameterizes the XML adapter through an
38
+ environment variable; the same shape works downstream:
39
+
40
+ [source,ruby]
41
+ ----
42
+ ADAPTER = ENV.fetch("XML_ADAPTER", "nokogiri").to_sym
43
+
44
+ RSpec.configure do |config|
45
+ config.before(:suite) do
46
+ Lutaml::Model::Config.xml_adapter_type = ADAPTER
47
+ end
48
+ end
49
+ ----
50
+
51
+ Run the matrix in CI — one leg per adapter the standard runners can
52
+ install:
53
+
54
+ [source,sh]
55
+ ----
56
+ XML_ADAPTER=nokogiri bundle exec rspec
57
+ XML_ADAPTER=ox bundle exec rspec
58
+ XML_ADAPTER=leptris bundle exec rspec # requires the leptris gem in that leg's bundle
59
+ ----
60
+
61
+ Engine gems belong in the CI *Gemfile* (a `group :test` entry or a
62
+ separate CI gemfile), never in the gemspec. Start with the primary
63
+ adapter (Nokogiri) and enable further legs incrementally as they
64
+ pass — the same "adapter-first" strategy this library's own suite
65
+ uses.
66
+
67
+ == Semantics your users can rely on
68
+
69
+ * Adapter choice never changes results: the plan fast path and the
70
+ interpretive pipeline produce identical output; on engines below
71
+ a capability floor the affected models parse interpretively
72
+ rather than degrade (see the version-floor note in the
73
+ link:../native-engines[native engines guide]).
74
+ * Parse failures raise the standard error classes wrapped as
75
+ `Lutaml::Model::InvalidFormatError` on every engine — code that
76
+ rescues those classes works regardless of the bundle.
77
+
78
+ == Rules of thumb
79
+
80
+ * Gemspec: `lutaml-model` only. CI gemfiles: the engines you test.
81
+ * Expose format options you want to forward (pretty printing,
82
+ encoding) — never the adapter itself.
83
+ * If you ship models for multiple schema versions, see
84
+ link:../type-registers-contexts[type registers and contexts] for
85
+ per-version trees and substitution.
@@ -0,0 +1,69 @@
1
+ = Grammar-backed string formats
2
+
3
+ A serialization format whose input is a *grammar*, not a data structure:
4
+ identifier strings, query languages, DSLs — text that is parsed, not
5
+ decoded. lutaml-model supports registering these as first-class formats
6
+ through the same `FormatRegistry` extension point as xml/json/yaml; the
7
+ parse side is implemented by a grammar engine (the reference
8
+ implementation is https://github.com/parsanol/parsanol-ruby[Parsanol]'s
9
+ `Parsanol::PARG::Lutaml.register`, which consumes a compiled PARG
10
+ artifact plus its bindings).
11
+
12
+ == The contract
13
+
14
+ A grammar-backed format needs three things:
15
+
16
+ * a *parse* — text → attribute hash (the grammar engine's job)
17
+ * *bindings* — capture names → model attributes (the artifact's
18
+ bindings section)
19
+ * a *model class* — a regular `Serializable` with attributes; no
20
+ hand-written `from_h`/`to_h` anywhere
21
+
22
+ == Reference implementation (Parsanol)
23
+
24
+ [source,ruby]
25
+ ----
26
+ require "parsanol/parg/lutaml"
27
+
28
+ Parsanol::PARG::Lutaml.register(
29
+ IsoIdentifier,
30
+ format_name: :pubid_iso,
31
+ artifact: "artifacts/iso.json", # compiled grammar + bindings + tests
32
+ entry: "identifier",
33
+ )
34
+
35
+ IsoIdentifier.from_pubid_iso("ISO/IEC 12345-1:2020")
36
+ # => #<IsoIdentifier number: "12345-1", year: 2020, ...>
37
+ ----
38
+
39
+ What `register` does:
40
+
41
+ . loads the checksummed artifact (parse failures raise the engine's
42
+ error type — register it via `error_types:` so users can rescue it);
43
+ . registers the format through `FormatRegistry.register` with an
44
+ adapter whose `parse` is artifact-parse-then-bind;
45
+ . defines `from_<format>` on the model class.
46
+
47
+ The artifact is the parser of record: it carries the grammar, the
48
+ bindings, and the conformance tests (the grammar's own test block is
49
+ baked in and can be re-run by consumers), and its checksum pins the
50
+ pinned version of the language.
51
+
52
+ == Writing the grammar side
53
+
54
+ Grammars are authored in PARG (`.parg` — ABNF-flavoured surface, PEG
55
+ semantics, one rule per line) and compiled with the `parsanol` CLI:
56
+
57
+ ----
58
+ parsanol compile grammars/iso.parg --tables tables
59
+ ----
60
+
61
+ The compile-time lint rejects left recursion and shadowed
62
+ alternatives; the embedded test block runs at compile time, so a
63
+ grammar that stops accepting its documented inputs fails the build
64
+ rather than the consumer.
65
+
66
+ == See also
67
+
68
+ * parsanol-ruby `docs/pg-language-guide.md` — the PARG language
69
+ * `Parsanol::PARG::Lutaml.register` — the bridge (reference implementation)
@@ -26,9 +26,15 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
26
26
  == Advanced mapping
27
27
 
28
28
  * link:../advanced-mapping[Advanced Attribute Mapping] - Same-name dispatch on attribute values (`when_attribute`), grouped declarations, custom methods, delegation
29
+ * link:../grammar-backed-formats[Grammar-Backed String Formats] - Register a grammar engine as a serialization format (identifier strings, DSLs)
29
30
 
30
- == Performance
31
+ == Model architecture and integration
32
+
33
+ * link:../type-registers-contexts[Type Registers and Contexts] - Multi-version class trees, derived contexts, and model substitution
34
+ * link:../gem-integration[Integrating lutaml-model in a Gem] - Dependency contract, adapter deferral, cross-adapter testing
31
35
 
36
+ == Performance
37
+ /
32
38
  * link:../native-engines[Native engines (leptris / yeptris / teptris)] - Drop-in XML/YAML/JSON/TOML engine upgrades via bundle opt-in
33
39
 
34
40
  == Linked Data serialization
@@ -47,6 +53,7 @@ See link:../references/rdf-namespaces[RDF Namespaces] for namespace classes,
47
53
  * link:../value-transformations[Value Transformations] - Transform values during serialization
48
54
  * link:../missing-values-handling[Missing Values Handling] - Handle nil/empty/omitted
49
55
  * link:../advanced-mapping[Advanced Mapping] - Custom methods and delegation
56
+ * link:../grammar-backed-formats[Grammar-Backed String Formats] - Grammar engines as serialization formats
50
57
  * link:../schema-generation[Schema Generation] - Generate schemas from models
51
58
  * link:../creating-xsd[Creating XSD Schemas] - Complete reference for XSD generation from LutaML models
52
59
  * link:../schema-import[Schema Import] - Import schemas to create models