lutaml-model 0.8.22 → 0.8.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/performance.yml +1 -1
  3. data/Gemfile +6 -0
  4. data/README.adoc +16 -7
  5. data/docs/_guides/index.adoc +4 -0
  6. data/docs/_guides/native-engines.adoc +61 -0
  7. data/docs/_guides/xml-mapping.adoc +31 -0
  8. data/docs/_pages/configuration.adoc +11 -7
  9. data/docs/_pages/serialization_adapters.adoc +14 -5
  10. data/lib/compat/opal/lutaml_model_boot.rb +2 -0
  11. data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
  12. data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
  13. data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
  14. data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
  15. data/lib/lutaml/json/adapter.rb +1 -0
  16. data/lib/lutaml/json/format.rb +1 -1
  17. data/lib/lutaml/json/generator_options.rb +62 -0
  18. data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
  19. data/lib/lutaml/jsonld/adapter.rb +12 -4
  20. data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
  21. data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +7 -0
  22. data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +17 -5
  23. data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
  24. data/lib/lutaml/key_value/adapter/jsonl/standard_adapter.rb +1 -1
  25. data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
  26. data/lib/lutaml/key_value/adapter/toml.rb +1 -0
  27. data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
  28. data/lib/lutaml/key_value/transform.rb +82 -0
  29. data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
  30. data/lib/lutaml/key_value/transformation.rb +25 -1
  31. data/lib/lutaml/model/adapter_resolver.rb +23 -6
  32. data/lib/lutaml/model/attribute.rb +31 -6
  33. data/lib/lutaml/model/collection.rb +5 -0
  34. data/lib/lutaml/model/configuration.rb +5 -0
  35. data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
  36. data/lib/lutaml/model/serialization_adapter.rb +1 -1
  37. data/lib/lutaml/model/serialize/format_conversion.rb +75 -7
  38. data/lib/lutaml/model/serialize.rb +18 -0
  39. data/lib/lutaml/model/toml.rb +10 -6
  40. data/lib/lutaml/model/transformation.rb +5 -0
  41. data/lib/lutaml/model/type/date_time.rb +2 -2
  42. data/lib/lutaml/model/type/value.rb +24 -2
  43. data/lib/lutaml/model/utils.rb +6 -5
  44. data/lib/lutaml/model/version.rb +1 -1
  45. data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
  46. data/lib/lutaml/toml/adapter.rb +1 -0
  47. data/lib/lutaml/toml/format.rb +1 -1
  48. data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
  49. data/lib/lutaml/xml/adapter_element.rb +111 -3
  50. data/lib/lutaml/xml/builder/base.rb +3 -1
  51. data/lib/lutaml/xml/builder/leptris.rb +13 -0
  52. data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
  53. data/lib/lutaml/xml/builder.rb +1 -0
  54. data/lib/lutaml/xml/format.rb +4 -1
  55. data/lib/lutaml/xml/leptris/element.rb +34 -0
  56. data/lib/lutaml/xml/mapping.rb +24 -0
  57. data/lib/lutaml/xml/model_transform.rb +52 -1
  58. data/lib/lutaml/xml/plan_compiler.rb +139 -0
  59. data/lib/lutaml/xml/plan_hydrator.rb +80 -0
  60. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
  61. data/lib/lutaml/xml/transformation.rb +29 -0
  62. data/lib/lutaml/xml/xml_element.rb +6 -2
  63. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +40 -0
  64. data/lib/lutaml/yaml/adapter.rb +5 -0
  65. data/lib/lutaml/yaml/format.rb +1 -1
  66. data/lib/tasks/performance.rake +5 -0
  67. data/lib/tasks/performance_comparator.rb +19 -3
  68. data/lib/tasks/performance_probe.rake +33 -0
  69. data/lib/tasks/performance_probe.rb +106 -0
  70. data/lib/tasks/performance_probe_xml.rb +54 -0
  71. data/lutaml-model.gemspec +1 -1
  72. data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
  73. data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
  74. data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
  75. data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
  76. data/spec/lutaml/model/collection_spec.rb +1 -1
  77. data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
  78. data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
  79. data/spec/lutaml/model/json_adapter_spec.rb +6 -0
  80. data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
  81. data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
  82. data/spec/lutaml/model/toml_spec.rb +36 -1
  83. data/spec/lutaml/xml/plan_fast_path_spec.rb +96 -0
  84. data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
  85. data/spec/lutaml/xml/xml_spec.rb +13 -0
  86. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
  87. data/spec/spec_helper.rb +7 -1
  88. metadata +29 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a30fcd552dca587769736a81de33b66909c592cbc5ed40f696d34fbaf7de887f
4
- data.tar.gz: b15506fd1398cad525dcac0a624166cdc23dedf7faa3e282b1aae10c583b405a
3
+ metadata.gz: c8aca94b467fe681429ee9863355555d3aa003d9deda8543a74fd1424087bcc8
4
+ data.tar.gz: 02db8dceef19fc7ef7abd3a54f0936e00ef162a1ee6477191a8b1d3477ba7860
5
5
  SHA512:
6
- metadata.gz: 061ea9253cc7e425f9591e5f57407935eb6b52b1ee69fc5b289b672a0109f8aa3850097d0dd3971613b6010c5e8d83b5425e3447bebe0ae6724916fbdc7a3173
7
- data.tar.gz: 9280bc8adfdeaa2e6167c57379d0d40a7c8c27a4dffb420c8f8b6468b893df8ae72207d5fddef2885d4954ba4c45918a1f9cd3746ef2194aed3b09bae46b19f9
6
+ metadata.gz: 4da87513ed839ba070c32b28de27411ffe12ccc92f879a0dbe433aad85ea3a32a76759b36112958a131b09a747b1618c0ff8a95e92238aca40f45b736dd46950
7
+ data.tar.gz: ff488be2b79ef92c3017d63ee0a9bf4ac3b183549be5b17a59efcb42ea31ae00cacb2b63995f9b69fbe3628af9bd65f30dfedb2fb5d2722fe15189c7e964a93b
@@ -16,7 +16,7 @@ permissions:
16
16
  jobs:
17
17
  performance:
18
18
  runs-on: ubuntu-latest
19
- timeout-minutes: 20
19
+ timeout-minutes: 30
20
20
  steps:
21
21
  - uses: actions/checkout@v6
22
22
  with:
data/Gemfile CHANGED
@@ -19,6 +19,7 @@ gem "benchmark-ips"
19
19
  gem "bigdecimal"
20
20
  gem "canon" # , path: "../canon"
21
21
  gem "json-ld"
22
+ gem "leptris"
22
23
  gem "liquid", "~> 5"
23
24
  gem "multi_json"
24
25
  gem "nokogiri"
@@ -26,8 +27,11 @@ gem "oj"
26
27
  gem "openssl", "~> 3.0"
27
28
  gem "ox"
28
29
  gem "rake"
30
+ # yeptris ships prebuilt platform gems for linux/darwin only; a source
31
+ # build is not wired for windows runners.
29
32
  gem "rdf-turtle"
30
33
  gem "rexml"
34
+ gem "yeptris" unless Gem.win_platform?
31
35
  # TODO: revert rng branch to main when lutaml/rng#32 is merged
32
36
  gem "rng", git: "https://github.com/lutaml/rng", branch: "main"
33
37
  gem "rspec"
@@ -35,6 +39,8 @@ gem "rubocop"
35
39
  gem "rubocop-performance", require: false
36
40
  gem "rubocop-rake", require: false
37
41
  gem "rubocop-rspec", require: false
42
+ # teptris ships prebuilt gems for every platform (incl. mingw-ucrt)
43
+ gem "teptris"
38
44
  gem "tomlib"
39
45
  gem "toml-rb"
40
46
 
data/README.adoc CHANGED
@@ -16223,7 +16223,7 @@ Adapters are auto-detected on first use. The defaults when gems are available:
16223
16223
  * YAML: `:standard` (alias: `:standard_yaml`)
16224
16224
  * JSON: `:standard` (alias: `:standard_json`)
16225
16225
  * Hash: `:standard` (alias: `:standard_hash`)
16226
- * TOML: `:tomlib` on non-Windows, `:toml_rb` on Windows
16226
+ * TOML: `:teptris` if available, else `:tomlib` on non-Windows, `:toml_rb` on Windows
16227
16227
 
16228
16228
 
16229
16229
  ==== Configure adapters through symbol choices
@@ -16246,7 +16246,7 @@ Lutaml::Model::Config.configure do |config|
16246
16246
  config.hash_adapter = :standard
16247
16247
  config.yaml_adapter = :standard
16248
16248
  config.json_adapter = :standard # can be one of [:standard, :multi_json, :oj]
16249
- config.toml_adapter = :toml_rb # can be one of [:toml_rb, :tomlib]
16249
+ config.toml_adapter = :toml_rb # can be one of [:teptris, :toml_rb, :tomlib]
16250
16250
  end
16251
16251
  ----
16252
16252
 
@@ -16388,22 +16388,29 @@ end
16388
16388
 
16389
16389
  Lutaml::Model supports the following TOML adapters:
16390
16390
 
16391
+ Teptris::
16392
+ (default when the `teptris` gem is in the bundle — every platform)
16393
+ The native TOML engine (libteptris via FFI). Value semantics mirror
16394
+ tomlib: offset and local datetimes become `Time`, dates become `Date`,
16395
+ local times stay `String`.
16396
+ Requires the `teptris` gem.
16397
+
16391
16398
  Toml-rb::
16392
- (default on Windows)
16399
+ (default on Windows without teptris)
16393
16400
  A TOML parser and serializer for Ruby that is compatible with the TOML v1.0.0
16394
16401
  specification.
16395
16402
  Requires the `toml-rb` gem.
16396
16403
 
16397
16404
  Tomlib::
16398
- (default on non-Windows platforms)
16405
+ (default on non-Windows platforms without teptris)
16399
16406
  Toml-rb fork that is compatible with the TOML v1.0.0 specification, but with
16400
16407
  additional features.
16401
16408
  Requires the `tomlib` gem.
16402
16409
 
16403
16410
  IMPORTANT: The Tomlib adapter is not available on Windows due to segmentation
16404
- fault issues. On Windows, the `toml-rb` adapter is used automatically as the
16405
- default. If you explicitly configure `tomlib` on Windows, a
16406
- `ConfigurationError` will be raised.
16411
+ fault issues. On Windows, the `teptris` adapter (when bundled) or the
16412
+ `toml-rb` adapter is used automatically as the default. If you explicitly
16413
+ configure `tomlib` on Windows, a `ConfigurationError` will be raised.
16407
16414
 
16408
16415
 
16409
16416
  .Using a TOML adapter
@@ -16412,6 +16419,8 @@ default. If you explicitly configure `tomlib` on Windows, a
16412
16419
  require 'lutaml/model'
16413
16420
 
16414
16421
  Lutaml::Model::Config.configure do |config|
16422
+ config.toml_adapter = :teptris
16423
+ # or
16415
16424
  config.toml_adapter = :toml_rb
16416
16425
  # or (not available on Windows)
16417
16426
  config.toml_adapter = :tomlib
@@ -22,6 +22,10 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
22
22
  * link:../keyvalue-serialization[Key-Value Serialization] - JSON/YAML/TOML/Hash
23
23
  * link:../collection-serialization[Collection Serialization] - JSONL and YAML Stream
24
24
 
25
+ == Performance
26
+
27
+ * link:../native-engines[Native engines (leptris / yeptris / teptris)] - Drop-in XML/YAML/JSON/TOML engine upgrades via bundle opt-in
28
+
25
29
  == Linked Data serialization
26
30
 
27
31
  * link:../rdf-serialization[Unified RDF Serialization] - One `rdf` block for both JSON-LD and Turtle
@@ -0,0 +1,61 @@
1
+ = Native engines (leptris / yeptris / teptris)
2
+
3
+ By default, `lutaml-model` serializes through the same engines every Ruby
4
+ install already has: Nokogiri (via moxml) for XML, Psych for YAML, the
5
+ `json` gem for JSON.
6
+
7
+ The leptris family of native engines is a drop-in performance upgrade:
8
+
9
+ * **leptris** — the XML engine moxml drives. When the `leptris` gem is in
10
+ your bundle, moxml prefers it over Nokogiri for parsing and
11
+ serialization.
12
+ * **yeptris** — the YAML/JSON engine. When the `yeptris` gem is in your
13
+ bundle, lutaml-model's YAML and JSON adapters resolve to the yeptris
14
+ engine.
15
+ * **teptris** — the TOML engine. When the `teptris` gem is in your
16
+ bundle it becomes the TOML default on every platform (0.2.12+ parses
17
+ ~3x and dumps ~8x faster than tomlib, and the Windows mingw-ucrt
18
+ prebuilts are back), giving Windows native TOML and retiring the
19
+ tomlib-segfault workaround (previously pure-Ruby toml-rb only).
20
+
21
+ Opting in is purely a bundle decision — no code changes, no gemspec
22
+ requirements:
23
+
24
+ [source,sh]
25
+ ----
26
+ bundle add leptris yeptris teptris
27
+ ----
28
+
29
+ leptris and yeptris ship prebuilt platform gems for Linux and macOS
30
+ (yeptris has no Windows prebuilts). teptris ships prebuilt gems for
31
+ every platform, including Windows. When a gem is absent, lutaml-model
32
+ automatically falls back to the standard engine for that format.
33
+
34
+ == Selecting adapters explicitly
35
+
36
+ The engines register as ordinary adapter types, so explicit selection and
37
+ the availability error messages work as usual:
38
+
39
+ [source,ruby]
40
+ ----
41
+ Lutaml::Model::Config.xml_adapter_type = :leptris
42
+ Lutaml::Model::Config.yaml_adapter_type = :yeptris
43
+ Lutaml::Model::Config.json_adapter_type = :yeptris
44
+ Lutaml::Model::Config.toml_adapter_type = :teptris
45
+ ----
46
+
47
+ == Semantics notes
48
+
49
+ * **YAML**: the yeptris adapter uses the engine's `compat_11` schema, so
50
+ implicit typing matches Psych (parity-verified across the scalar
51
+ battery: dates, timestamps, octal, sexagesimal, `yes`/`no`, `.inf`).
52
+ Two deliberate differences: anchors and aliases always resolve, and a
53
+ tagged value without a core type materializes as plain data instead of
54
+ raising `Psych::DisallowedClass`.
55
+ * **JSON**: `yeptris` parsing targets exact `JSON.parse` semantics;
56
+ generation stays on the `json` gem.
57
+ * **TOML**: the teptris adapter mirrors tomlib value semantics — offset
58
+ and local datetimes become `Time`, dates become `Date`, local times
59
+ stay `String`. Parse failures raise `Teptris::ParseError` with line
60
+ and column, wrapped as `InvalidFormatError` like the other TOML
61
+ adapters.
@@ -717,6 +717,37 @@ end
717
717
  ----
718
718
  ====
719
719
 
720
+ ===== Mapping several element names to one attribute
721
+
722
+ Several `map_element` rules may target the same collection attribute to
723
+ accept multiple spellings of one element (e.g. a legacy and a modern
724
+ name). Parsing merges the content of every spelling **in document
725
+ order**; serialization emits the value once, under the **first declared**
726
+ spelling.
727
+
728
+ [source,ruby]
729
+ ----
730
+ class Editorial < Lutaml::Model::Serializable
731
+ attribute :groups, Group, collection: true
732
+
733
+ xml do
734
+ element "metadata"
735
+ map_element "editorial-group", to: :groups
736
+ map_element "editorialgroup", to: :groups # legacy spelling
737
+ end
738
+ end
739
+
740
+ Editorial.from_xml(
741
+ "<metadata><editorialgroup>…</editorialgroup><editorial-group>…</editorial-group></metadata>"
742
+ ).groups
743
+ # => [group(legacy), group(modern)] — document order
744
+ ----
745
+
746
+ NOTE: The merge applies to plain element rules on collection attributes.
747
+ Rules with custom deserialization methods, `raw`, `content`, or `cdata`
748
+ are not merged — each still assigns separately, last rule winning as
749
+ before.
750
+
720
751
  ===== Using elements in different namespaces
721
752
 
722
753
  ====== General
@@ -23,7 +23,7 @@ The auto-detection order for each format:
23
23
  |===
24
24
  | Format | Detection order
25
25
  | XML | `:nokogiri` → `:ox` → `:oga` → `:rexml`
26
- | TOML | `:tomlib` → `:toml_rb` (Windows: `:toml_rb` only)
26
+ | TOML | `:teptris` → `:tomlib` → `:toml_rb` (Windows: `:teptris` → `:toml_rb`)
27
27
  | JSON | `:standard` (always available)
28
28
  | YAML | `:standard` (always available)
29
29
  | Hash | `:standard` (always available)
@@ -109,14 +109,17 @@ When an adapter is needed for a format, `AdapterResolver` follows this chain:
109
109
  |===
110
110
  | Adapter | Description
111
111
 
112
+ | `:teptris` (default when bundled)
113
+ | Native engine (libteptris via FFI), all platforms including Windows. tomlib-compatible value semantics.
114
+
112
115
  | `:tomlib` (default on non-Windows)
113
116
  | Enhanced fork. Better performance on non-Windows platforms.
114
117
 
115
- | `:toml_rb` (default on Windows)
116
- | Pure Ruby. TOML v1.0.0 compatible. Required on Windows.
118
+ | `:toml_rb` (default on Windows without teptris)
119
+ | Pure Ruby. TOML v1.0.0 compatible.
117
120
  |===
118
121
 
119
- NOTE: The `:tomlib` adapter is not available on Windows due to segmentation fault issues. On Windows, `:toml_rb` is automatically used as the default.
122
+ NOTE: The `:tomlib` adapter is not available on Windows due to segmentation fault issues. On Windows, `:teptris` (when bundled) or `:toml_rb` is used as the default.
120
123
 
121
124
  == Per-operation adapter override
122
125
 
@@ -226,7 +229,7 @@ If not explicitly configured, Lutaml::Model auto-detects on first use:
226
229
  * JSON: `:standard` (alias: `:standard_json`)
227
230
  * YAML: `:standard` (alias: `:standard_yaml`)
228
231
  * Hash: `:standard` (alias: `:standard_hash`)
229
- * TOML: `:tomlib` on non-Windows, `:toml_rb` on Windows
232
+ * TOML: `:teptris` if available, else `:tomlib` on non-Windows, `:toml_rb` on Windows
230
233
 
231
234
  == When to configure
232
235
 
@@ -272,8 +275,9 @@ end
272
275
 
273
276
  === Choose TOML adapter based on
274
277
 
275
- * **Tomlib**: Most projects on non-Windows (better performance)
276
- * **TomlRb**: Windows platforms (required due to tomlib incompatibility)
278
+ * **Teptris**: Bundled native engine ~3x faster parse, ~8x faster dump than tomlib, every platform
279
+ * **Tomlib**: Non-Windows projects without teptris (better performance)
280
+ * **TomlRb**: Windows platforms without teptris (required due to tomlib incompatibility)
277
281
 
278
282
  == Opal runtime compatibility
279
283
 
@@ -48,7 +48,7 @@ more information.
48
48
  Adapters are resolved lazily on first use. If no adapter is explicitly configured, `AdapterResolver` probes for available gems in a preferred order:
49
49
 
50
50
  * **XML**: `:nokogiri` → `:ox` → `:oga` → `:rexml`
51
- * **TOML**: `:tomlib` → `:toml_rb` (Windows: `:toml_rb` only)
51
+ * **TOML**: `:teptris` → `:tomlib` → `:toml_rb` (Windows: `:teptris` → `:toml_rb`)
52
52
  * **JSON/YAML/Hash**: `:standard` (always available)
53
53
 
54
54
  The detection result is cached after the first probe, so it only runs once per format.
@@ -84,7 +84,7 @@ There is a default configuration for adapters for commonly used formats:
84
84
  * YAML: `:standard` (alias: `:standard_yaml`)
85
85
  * JSON: `:standard` (alias: `:standard_json`)
86
86
  * Hash: `:standard` (alias: `:standard_hash`)
87
- * TOML: `:tomlib` on non-Windows, `:toml_rb` on Windows
87
+ * TOML: `:teptris` if available, else `:tomlib` on non-Windows, `:toml_rb` on Windows
88
88
 
89
89
 
90
90
  ==== Configure adapters through symbol choices
@@ -107,7 +107,7 @@ Lutaml::Model::Config.configure do |config|
107
107
  config.hash_adapter = :standard
108
108
  config.yaml_adapter = :standard
109
109
  config.json_adapter = :standard # can be one of [:standard, :multi_json, :oj]
110
- config.toml_adapter = :toml_rb # can be one of [:toml_rb, :tomlib] (tomlib not available on Windows)
110
+ config.toml_adapter = :toml_rb # can be one of [:teptris, :toml_rb, :tomlib] (tomlib not available on Windows)
111
111
  end
112
112
  ----
113
113
 
@@ -285,8 +285,16 @@ end
285
285
 
286
286
  Lutaml::Model supports the following TOML adapters:
287
287
 
288
+ Teptris::
289
+ (default when the `teptris` gem is in the bundle — every platform)
290
+ The native TOML engine (libteptris via FFI). Value semantics mirror
291
+ tomlib: offset and local datetimes become `Time`, dates become `Date`,
292
+ local times stay `String`. Parse failures raise `Teptris::ParseError`
293
+ with line and column.
294
+ Requires the `teptris` gem.
295
+
288
296
  Tomlib::
289
- (default on non-Windows)
297
+ (default on non-Windows when teptris is absent)
290
298
  Toml-rb fork that is compatible with the TOML v1.0.0 specification, with
291
299
  additional features and better performance.
292
300
  Requires the `tomlib` gem.
@@ -348,4 +356,5 @@ raised to indicate that the input format is malformed and cannot be parsed.
348
356
 
349
357
  NOTE: The `:tomlib` TOML adapter is disabled on Windows due to segmentation
350
358
  fault issues. Attempting to configure `:tomlib` on Windows will raise an
351
- `ArgumentError`. Use `:toml_rb` on Windows instead.
359
+ `ArgumentError`. Use `:teptris` (native, all platforms) or `:toml_rb` on
360
+ Windows instead.
@@ -167,6 +167,8 @@ require "lutaml/xml/oga"
167
167
  require "lutaml/xml/parse_session"
168
168
  require "lutaml/xml/parsed_namespace_declaration"
169
169
  require "lutaml/xml/parsed_namespace_set"
170
+ require "lutaml/xml/plan_compiler"
171
+ require "lutaml/xml/plan_hydrator"
170
172
  require "lutaml/xml/polymorphic_value_handler"
171
173
  require "lutaml/xml/qualified_inheritance_strategy"
172
174
  require "lutaml/xml/rexml"
@@ -16,6 +16,8 @@ module Lutaml
16
16
  "multi_json gem is not available. Please add 'multi_json' to your Gemfile."
17
17
  end
18
18
 
19
+ INTERNAL_LUTAML_KEYS = %i[register adapter _adapter_override].freeze
20
+
19
21
  # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
20
22
  def to_json(*args)
21
23
  require "multi_json"
@@ -28,7 +30,16 @@ module Lutaml
28
30
  @attributes
29
31
  end
30
32
 
31
- MultiJson.dump(attributes_to_serialize, *args)
33
+ # LutaML's own threading keys must not reach the backend; the
34
+ # rest (e.g. :pretty) are meaningful to multi_json. The json
35
+ # gem 3.x and Oj raise on unknown generator options, unlike
36
+ # json 2.x which ignored them.
37
+ dump_args = args.map do |arg|
38
+ next arg unless arg.is_a?(::Hash)
39
+
40
+ arg.except(*INTERNAL_LUTAML_KEYS)
41
+ end
42
+ MultiJson.dump(attributes_to_serialize, *dump_args)
32
43
  # rubocop:enable Style/ArgumentsForwarding
33
44
  rescue LoadError
34
45
  raise LoadError,
@@ -17,6 +17,13 @@ module Lutaml
17
17
  end
18
18
 
19
19
  # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
20
+ # Oj has its own option namespace and its own escape_mode. Handing it
21
+ # the stdlib generator's options overrides that configuration, so it
22
+ # takes none of them -- which is what it received before.
23
+ def ignores_generator_options?
24
+ true
25
+ end
26
+
20
27
  def to_json(*args)
21
28
  require "oj"
22
29
  # Handle KeyValueElement input (new symmetric architecture)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../generator_options"
4
5
 
5
6
  module Lutaml
6
7
  module Json
@@ -9,12 +10,16 @@ module Lutaml
9
10
  FORMAT_SYMBOL = :json
10
11
 
11
12
  def self.parse(json, _options = {})
12
- JSON.parse(json, create_additions: false)
13
+ JSON.parse(json)
13
14
  end
14
15
 
15
- def to_json(*args)
16
- options = args.first || {}
16
+ # This adapter hands its payload to the stdlib generator, so a
17
+ # JSON::State carrying the outer formatting is meaningful here.
18
+ def accepts_generator_state?
19
+ true
20
+ end
17
21
 
22
+ def to_json(*args)
18
23
  # Handle KeyValueElement input (new symmetric architecture)
19
24
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
20
25
  # Unwrap __root__ wrapper to get actual content
@@ -24,10 +29,17 @@ module Lutaml
24
29
  @attributes
25
30
  end
26
31
 
32
+ unless GeneratorOptions.lutaml_options?(args.first)
33
+ return JSON.generate(attributes_to_serialize, args.first)
34
+ end
35
+
36
+ options = args.first || {}
37
+ generator_options = GeneratorOptions.filter(options)
38
+
27
39
  if options[:pretty]
28
- JSON.pretty_generate(attributes_to_serialize, *args)
40
+ JSON.pretty_generate(attributes_to_serialize, generator_options)
29
41
  else
30
- JSON.generate(attributes_to_serialize, *args)
42
+ JSON.generate(attributes_to_serialize, generator_options)
31
43
  end
32
44
  end
33
45
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yeptris"
4
+ require_relative "standard_adapter"
5
+
6
+ module Lutaml
7
+ module Json
8
+ module Adapter
9
+ # JSON parsing over the yeptris engine: Yeptris::JSON.load targets
10
+ # exact JSON.parse semantics (spec-pinned upstream), with a fused
11
+ # native materializer when the loaded libyeptris build carries it.
12
+ # Generation stays on the json gem (the yeptris JSON surface is
13
+ # load-only), so everything else inherits from the standard adapter.
14
+ class YeptrisAdapter < StandardAdapter
15
+ def self.parse(json, _options = {})
16
+ ::Yeptris::JSON.load(json)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -14,6 +14,7 @@ module Lutaml
14
14
  self,
15
15
  OjAdapter: "#{__dir__}/adapter/oj_adapter",
16
16
  MultiJsonAdapter: "#{__dir__}/adapter/multi_json_adapter",
17
+ YeptrisAdapter: "#{__dir__}/adapter/yeptris_adapter",
17
18
  )
18
19
  end
19
20
  end
@@ -52,7 +52,7 @@ Lutaml::Model::FormatRegistry.register(
52
52
  transformer: Lutaml::Json::Adapter::Transform,
53
53
  key_value: true,
54
54
  adapter_options: {
55
- available: %i[standard standard_json multi_json oj],
55
+ available: %i[standard standard_json multi_json oj yeptris],
56
56
  default: :standard,
57
57
  },
58
58
  )
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lutaml
6
+ module Json
7
+ # Filters the options LutaML threads through serialization down to the
8
+ # subset the JSON generator accepts.
9
+ #
10
+ # json 2.x silently ignored unknown generator options; json 3.0 raises
11
+ # ArgumentError on them, so LutaML's own options (:register, :pretty and
12
+ # any caller-supplied extras) have to be stripped before they reach
13
+ # JSON.generate.
14
+ module GeneratorOptions
15
+ # Options the generator accepts on both json 2.x and 3.x. Measured
16
+ # against 2.9.1, 2.15.2, 2.19.9, 2.20.0, 2.21.1, 2.21.2 and 3.0.0.
17
+ # :escape_slash is NOT here because json 3.0 removed it; on 2.x it still
18
+ # arrives via DERIVED_PERMITTED. :allow_duplicate_key IS here because the
19
+ # generator accepts it on every version while JSON::State never exposed
20
+ # it as an accessor, so the derived half alone would miss it.
21
+ BASE_PERMITTED = %i[
22
+ allow_duplicate_key allow_nan array_nl as_json ascii_only
23
+ buffer_initial_length depth indent max_nesting object_nl script_safe
24
+ sort_keys space space_before strict
25
+ ].freeze
26
+
27
+ # Additive, so a json release that adds an option needs no change here.
28
+ # Opal's JSON shim has no State class, hence the guard.
29
+ DERIVED_PERMITTED =
30
+ if defined?(::JSON::State)
31
+ accessors = ::JSON::State.instance_methods(false).grep(/\A[a-z_][a-z0-9_]*=\z/)
32
+ accessors.map { |name| name.to_s.chomp("=").to_sym }
33
+ else
34
+ []
35
+ end.freeze
36
+
37
+ PERMITTED = (BASE_PERMITTED | DERIVED_PERMITTED).freeze
38
+
39
+ # json 3.0 removed :escape_slash, which was only ever an alias of
40
+ # :script_safe. Dropping it would silently stop escaping slashes, so it
41
+ # is translated instead of discarded.
42
+ RENAMED = { escape_slash: :script_safe }.freeze
43
+
44
+ def self.filter(options)
45
+ return {} unless options.is_a?(::Hash)
46
+
47
+ options.each_with_object({}) do |(key, value), kept|
48
+ key = RENAMED.fetch(key, key) unless PERMITTED.include?(key)
49
+ kept[key] = value if PERMITTED.include?(key)
50
+ end
51
+ end
52
+
53
+ # Ruby's JSON generator calls #to_json with a JSON::State whenever a
54
+ # document is nested inside another JSON.generate call. That is the
55
+ # generator's own state, not a LutaML options hash, and json 3.0 removed
56
+ # JSON::State#[], so it must never be read like one.
57
+ def self.lutaml_options?(argument)
58
+ argument.nil? || argument.is_a?(::Hash)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -14,7 +14,7 @@ module Lutaml
14
14
  next if line.strip.empty?
15
15
 
16
16
  begin
17
- results << JSON.parse(line, create_additions: false)
17
+ results << JSON.parse(line)
18
18
  rescue JSON::ParserError => e
19
19
  warn "Skipping invalid line: #{e.message}"
20
20
  end
@@ -1,21 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "../json/generator_options"
4
5
 
5
6
  module Lutaml
6
7
  module JsonLd
7
8
  class Adapter < Lutaml::KeyValue::Document
8
9
  def self.parse(jsonld_string, _options = {})
9
- JSON.parse(jsonld_string, create_additions: false)
10
+ JSON.parse(jsonld_string)
10
11
  end
11
12
 
12
13
  def to_jsonld(*args)
13
- options = args.first || {}
14
14
  data = @attributes
15
+
16
+ unless Lutaml::Json::GeneratorOptions.lutaml_options?(args.first)
17
+ return JSON.generate(data, args.first)
18
+ end
19
+
20
+ options = args.first || {}
21
+ generator_options = Lutaml::Json::GeneratorOptions.filter(options)
22
+
15
23
  if options[:pretty]
16
- JSON.pretty_generate(data, *args)
24
+ JSON.pretty_generate(data, generator_options)
17
25
  else
18
- JSON.generate(data, *args)
26
+ JSON.generate(data, generator_options)
19
27
  end
20
28
  end
21
29
  end
@@ -20,6 +20,8 @@ module Lutaml
20
20
  "multi_json gem is not available. Please add 'multi_json' to your Gemfile."
21
21
  end
22
22
 
23
+ INTERNAL_LUTAML_KEYS = %i[register adapter _adapter_override].freeze
24
+
23
25
  # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
24
26
  def to_json(*args)
25
27
  require "multi_json"
@@ -32,7 +34,20 @@ module Lutaml
32
34
  @attributes
33
35
  end
34
36
 
35
- MultiJson.dump(attributes_to_serialize, *args)
37
+ # LutaML's own options must not reach the backend: the json
38
+ # gem 3.x and Oj both raise on unknown generator options
39
+ # (multi_json's backend choice shifts with the resolved json
40
+ # version), unlike json 2.x which ignored them.
41
+ # LutaML's own threading keys must not reach the backend; the
42
+ # rest (e.g. :pretty) are meaningful to multi_json. The json
43
+ # gem 3.x and Oj raise on unknown generator options, unlike
44
+ # json 2.x which ignored them.
45
+ dump_args = args.map do |arg|
46
+ next arg unless arg.is_a?(::Hash)
47
+
48
+ arg.except(*INTERNAL_LUTAML_KEYS)
49
+ end
50
+ MultiJson.dump(attributes_to_serialize, *dump_args)
36
51
  # rubocop:enable Style/ArgumentsForwarding
37
52
  rescue LoadError
38
53
  raise LoadError,
@@ -21,6 +21,13 @@ module Lutaml
21
21
  end
22
22
 
23
23
  # rubocop:disable Style/ArgumentsForwarding -- anonymous * requires Ruby 3.2+
24
+ # Oj has its own option namespace and its own escape_mode. Handing it
25
+ # the stdlib generator's options overrides that configuration, so it
26
+ # takes none of them -- which is what it received before.
27
+ def ignores_generator_options?
28
+ true
29
+ end
30
+
24
31
  def to_json(*args)
25
32
  require "oj"
26
33
  # Handle KeyValueElement input (new symmetric architecture)