lutaml-model 0.8.23 → 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 (65) 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/_pages/configuration.adoc +11 -7
  8. data/docs/_pages/serialization_adapters.adoc +14 -5
  9. data/lib/compat/opal/lutaml_model_boot.rb +2 -0
  10. data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
  11. data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
  12. data/lib/lutaml/json/adapter.rb +1 -0
  13. data/lib/lutaml/json/format.rb +1 -1
  14. data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
  15. data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
  16. data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
  17. data/lib/lutaml/key_value/adapter/toml.rb +1 -0
  18. data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
  19. data/lib/lutaml/key_value/transform.rb +82 -0
  20. data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
  21. data/lib/lutaml/key_value/transformation.rb +25 -1
  22. data/lib/lutaml/model/adapter_resolver.rb +23 -6
  23. data/lib/lutaml/model/attribute.rb +31 -6
  24. data/lib/lutaml/model/configuration.rb +5 -0
  25. data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
  26. data/lib/lutaml/model/serialize/format_conversion.rb +37 -0
  27. data/lib/lutaml/model/toml.rb +10 -6
  28. data/lib/lutaml/model/type/date_time.rb +2 -2
  29. data/lib/lutaml/model/type/value.rb +24 -2
  30. data/lib/lutaml/model/utils.rb +6 -5
  31. data/lib/lutaml/model/version.rb +1 -1
  32. data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
  33. data/lib/lutaml/toml/adapter.rb +1 -0
  34. data/lib/lutaml/toml/format.rb +1 -1
  35. data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
  36. data/lib/lutaml/xml/adapter_element.rb +111 -3
  37. data/lib/lutaml/xml/builder/base.rb +3 -1
  38. data/lib/lutaml/xml/builder/leptris.rb +13 -0
  39. data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
  40. data/lib/lutaml/xml/builder.rb +1 -0
  41. data/lib/lutaml/xml/format.rb +4 -1
  42. data/lib/lutaml/xml/leptris/element.rb +34 -0
  43. data/lib/lutaml/xml/plan_compiler.rb +139 -0
  44. data/lib/lutaml/xml/plan_hydrator.rb +80 -0
  45. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
  46. data/lib/lutaml/xml/xml_element.rb +6 -2
  47. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +40 -0
  48. data/lib/lutaml/yaml/adapter.rb +5 -0
  49. data/lib/lutaml/yaml/format.rb +1 -1
  50. data/lib/tasks/performance.rake +5 -0
  51. data/lib/tasks/performance_comparator.rb +19 -3
  52. data/lib/tasks/performance_probe.rake +33 -0
  53. data/lib/tasks/performance_probe.rb +106 -0
  54. data/lib/tasks/performance_probe_xml.rb +54 -0
  55. data/lutaml-model.gemspec +1 -1
  56. data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
  57. data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
  58. data/spec/lutaml/model/json_adapter_spec.rb +6 -0
  59. data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
  60. data/spec/lutaml/model/toml_spec.rb +36 -1
  61. data/spec/lutaml/xml/plan_fast_path_spec.rb +96 -0
  62. data/spec/lutaml/xml/xml_spec.rb +13 -0
  63. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
  64. data/spec/spec_helper.rb +7 -1
  65. metadata +23 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22cdfc7334bab60076fc98d5153c5770789f2670dc41eeabfee4df3dc6265f99
4
- data.tar.gz: fcb7ed45d08960bc739e7b0401771c96cc295a79f0d5236d60c0b8c7edfc54bb
3
+ metadata.gz: c8aca94b467fe681429ee9863355555d3aa003d9deda8543a74fd1424087bcc8
4
+ data.tar.gz: 02db8dceef19fc7ef7abd3a54f0936e00ef162a1ee6477191a8b1d3477ba7860
5
5
  SHA512:
6
- metadata.gz: c2642f2aba35a1eff18d1cc69c3dd44c95f88c80ebb8b76937c10202f0f2527e9c8365be6730fdbd69ee34b44641707714e4d6e0bd2c1cb062176a8d735b7398
7
- data.tar.gz: 5cb5c0079ac4d8cb578cab080641e2d4f3d6a6445c1f212080aba556133b8b707afdf86a8508a4df488c859b3fabbe52adf26f324ddaecd1988f4d79338dc2b9
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.
@@ -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,
@@ -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
  )
@@ -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,
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../json/adapter/yeptris_adapter"
4
+
5
+ module Lutaml
6
+ module KeyValue
7
+ module Adapter
8
+ module Json
9
+ # Backward-compatibility entry for the adapter resolver's default
10
+ # load path; the implementation lives in Lutaml::Json::Adapter.
11
+ YeptrisAdapter = Lutaml::Json::Adapter::YeptrisAdapter
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TOML over the teptris engine (libteptris via FFI). teptris ships
4
+ # prebuilt platform gems for every platform we support — including
5
+ # mingw-ucrt — which retires the "no native TOML on Windows" gap that
6
+ # keeps tomlib off Windows. Value semantics mirror tomlib: offset and
7
+ # local datetimes become Time, dates become Date, local times stay
8
+ # String. Parse failures raise Teptris::ParseError (line/column).
9
+
10
+ module Lutaml
11
+ module KeyValue
12
+ module Adapter
13
+ module Toml
14
+ class TeptrisAdapter < Document
15
+ # teptris is required lazily (in parse/to_toml), NOT at file
16
+ # load: a platform gem whose native library is broken must
17
+ # degrade to detection-fallback + skipped specs, never crash
18
+ # unrelated loads. The gem's own require raises a clear
19
+ # LoadError ("no fallback by design") for that case.
20
+ def self.parse(toml, _options = {})
21
+ require "teptris"
22
+ Teptris::TOML.load(toml)
23
+ end
24
+
25
+ def to_toml(*)
26
+ require "teptris"
27
+ # Handle KeyValueElement input (new symmetric architecture)
28
+ attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
29
+ # Unwrap __root__ wrapper to get actual content
30
+ @attributes.to_hash["__root__"]
31
+ else
32
+ # Legacy Hash input (backward compatibility)
33
+ @attributes
34
+ end
35
+
36
+ Teptris::TOML.dump(attributes_to_serialize)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -12,6 +12,7 @@ module Lutaml
12
12
  autoload :Transform, "#{__dir__}/toml/transform"
13
13
  Lutaml::Model::RuntimeCompatibility.autoload_native(
14
14
  self,
15
+ TeptrisAdapter: "#{__dir__}/toml/teptris_adapter",
15
16
  TomlibAdapter: "#{__dir__}/toml/tomlib_adapter",
16
17
  TomlRbAdapter: "#{__dir__}/toml/toml_rb_adapter",
17
18
  )
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../yaml/adapter/yeptris_adapter"
4
+
5
+ module Lutaml
6
+ module KeyValue
7
+ module Adapter
8
+ module Yaml
9
+ # Backward-compatibility entry for the adapter resolver's default
10
+ # load path; the implementation lives in Lutaml::Yaml::Adapter.
11
+ YeptrisAdapter = Lutaml::Yaml::Adapter::YeptrisAdapter
12
+ end
13
+ end
14
+ end
15
+ end
@@ -244,6 +244,13 @@ format)
244
244
  rule, attr
245
245
  )
246
246
 
247
+ if (plan = kv_rule_plan(format, rule, attr)) &&
248
+ (value = kv_fast_extract(doc, plan))
249
+ rule.deserialize(instance, kv_fast_cast(value, plan, instance),
250
+ attributes, self)
251
+ return
252
+ end
253
+
247
254
  value = rule_value_extractor_class.call(rule, doc, format, attr,
248
255
  lutaml_register, options, instance)
249
256
  value = apply_value_map(value, rule.value_map(:from, options), attr)
@@ -269,6 +276,81 @@ format)
269
276
  rule.deserialize(instance, value, attributes, self)
270
277
  end
271
278
 
279
+ # Compiled rule plans (TODO.perf/07): for plain scalar rules the
280
+ # per-value interpretation (extraction dispatch, type resolution,
281
+ # per-element probes, validation inside casts) collapses to a
282
+ # precomputed wire name, target class, and identity-guard cast.
283
+ # Guard + cached target — an inline cache per rule. Complex rules
284
+ # (custom methods, transforms, value maps, delegates, hash mappings,
285
+ # unions, polymorphism) return nil and keep the interpretive path.
286
+ KvRulePlan = ::Struct.new(:wire, :klass, :collection)
287
+
288
+ def kv_rule_plan(format, rule, attr)
289
+ @kv_rule_plans ||= {}
290
+ @kv_rule_plans[[format, rule]] ||= build_kv_rule_plan(format, rule,
291
+ attr)
292
+ end
293
+
294
+ # `rule.polymorphic` defaults to an empty Hash, which is truthy —
295
+ # presence is emptiness-based, mirroring MappingRule#polymorphic_mapping?.
296
+ def polymorphic_rule?(rule)
297
+ poly = rule.polymorphic
298
+ poly.respond_to?(:empty?) ? !poly.empty? : !!poly
299
+ end
300
+
301
+ def build_kv_rule_plan(_format, rule, attr)
302
+ return nil if rule.raw_mapping? ||
303
+ rule.has_custom_method_for_deserialization? ||
304
+ rule.hash_mappings || rule.multiple_mappings? ||
305
+ polymorphic_rule?(rule) || rule.delegate ||
306
+ (rule.transform.is_a?(Hash) && !rule.transform.empty?) ||
307
+ rule.transform.is_a?(Class) ||
308
+ attr.derived? ||
309
+ attr.union? || attr.polymorphic? ||
310
+ attr.custom_collection?
311
+
312
+ type = attr.type(lutaml_register)
313
+ return nil unless type.is_a?(Class) &&
314
+ type < ::Lutaml::Model::Type::Value &&
315
+ !attr.value_policy.whole_value?(type) &&
316
+ !Lutaml::Model::Attribute.custom_from_probe?(type)
317
+
318
+ KvRulePlan.new(rule.name.to_s, type, attr.collection?)
319
+ end
320
+
321
+ # Extraction fast path: plain hash fetch for a present, non-blank
322
+ # value. nil/blank values keep the interpretive path — their
323
+ # value-map/missing-value semantics must not be duplicated here.
324
+ def kv_fast_extract(doc, plan)
325
+ return nil unless doc.is_a?(::Hash)
326
+
327
+ v = doc[plan.wire]
328
+ return nil if v.nil?
329
+ return nil if v.is_a?(::String) && v.empty?
330
+ return nil if v.is_a?(::Array) && (v.empty? || !plan.collection)
331
+ # Structured values on scalar plans keep the interpretive path —
332
+ # its collection guidance error and cast semantics own them.
333
+ return nil if v.is_a?(::Hash)
334
+
335
+ v
336
+ end
337
+
338
+ # Cast fast path: identity when the value already has the target
339
+ # class (the overwhelming case for engine-typed scalars), the type's
340
+ # cast otherwise. Collections cast per element.
341
+ def kv_fast_cast(value, plan, _instance)
342
+ if plan.collection
343
+ unless value.is_a?(::Array)
344
+ return value.is_a?(plan.klass) ? value : plan.klass.cast(value)
345
+ end
346
+
347
+ klass = plan.klass
348
+ value.map { |e| e.is_a?(klass) ? e : klass.cast(e) }
349
+ else
350
+ value.is_a?(plan.klass) ? value : plan.klass.cast(value)
351
+ end
352
+ end
353
+
272
354
  def cast_value(value, attr, format, rule, instance)
273
355
  cast_options = rule.polymorphic ? { polymorphic: rule.polymorphic } : {}
274
356
  cast_options[:lutaml_parent] = instance if instance
@@ -57,6 +57,14 @@ model_class: nil)
57
57
  return nil if value.nil?
58
58
  return nil if Lutaml::Model::Utils.uninitialized?(value)
59
59
 
60
+ # Compiled serialize plan (TODO.perf/07): builtin scalars whose
61
+ # type registers no custom to_<format> serializer serialize as the
62
+ # value itself — Type::Value#to_<format>'s default — without the
63
+ # wrap-allocate-cast ceremony per field. Computed once per rule.
64
+ if (fast = serialize_plan(rule)) && value.instance_of?(fast)
65
+ return value
66
+ end
67
+
60
68
  # Check for Reference type first - even if value is a Serializable,
61
69
  # it should be serialized as a key, not as a nested model
62
70
  if reference_type?(rule)
@@ -151,6 +159,23 @@ model_class: nil)
151
159
  end
152
160
  end
153
161
 
162
+ # Per-rule serialize plan: the type class for builtin scalars with
163
+ # no custom to_<format> serializer (identity serialization), nil
164
+ # otherwise. Memoized per [rule].
165
+ def serialize_plan(rule)
166
+ @serialize_plans ||= {}
167
+ @serialize_plans[rule] ||= begin
168
+ type = rule.attribute_type
169
+ if type.is_a?(::Class) && type < ::Lutaml::Model::Type::Value
170
+ serializer = ::Lutaml::Model::Type::Value
171
+ .format_type_serializer_for(@format, type)
172
+ has_custom_to = serializer && serializer[:to]
173
+ has_custom_from = ::Lutaml::Model::Attribute.custom_from_probe?(type)
174
+ type unless has_custom_to || has_custom_from
175
+ end
176
+ end
177
+ end
178
+
154
179
  private
155
180
 
156
181
  # Validate that a nested model value matches the expected type.
@@ -16,6 +16,18 @@ module Lutaml
16
16
  # This is a critical step toward symmetric OOP architecture
17
17
  # across all serialization formats.
18
18
  class Transformation < Lutaml::Model::Transformation
19
+ # Builtin Value types whose cast of an already-native value is the
20
+ # identity — used by the serialize_value fast path.
21
+ NATIVE_VALUE_CLASS = {
22
+ ::Lutaml::Model::Type::String => ::String,
23
+ ::Lutaml::Model::Type::Integer => ::Integer,
24
+ ::Lutaml::Model::Type::Float => ::Float,
25
+ ::Lutaml::Model::Type::Date => ::Date,
26
+ ::Lutaml::Model::Type::Time => ::Time,
27
+ ::Lutaml::Model::Type::DateTime => ::DateTime,
28
+ ::Lutaml::Model::Type::Symbol => ::Symbol,
29
+ }.freeze
30
+
19
31
  include Lutaml::Model::RenderPolicy
20
32
 
21
33
  autoload :RuleCompiler, "#{__dir__}/transformation/rule_compiler"
@@ -1082,7 +1094,19 @@ child_mappings, options)
1082
1094
  # Wrap value in type and call to_#{format} instance method (like legacy Attribute#serialize_value)
1083
1095
  # This allows custom type subclasses to override to_json, to_yaml, etc.
1084
1096
  if rule.attribute_type.is_a?(Class) && rule.attribute_type < Lutaml::Model::Type::Value
1085
- wrapped_value = rule.attribute_type.new(value)
1097
+ type = rule.attribute_type
1098
+ # Identity fast path (TODO.perf/07): a builtin type with no custom
1099
+ # to_<format>/from_<format> behavior emits an already-native value
1100
+ # unchanged — the wrap only re-casts and re-emits it.
1101
+ native = NATIVE_VALUE_CLASS[type]
1102
+ if native && value.instance_of?(native) &&
1103
+ !::Lutaml::Model::Type::Value
1104
+ .format_type_serializer_for(format, type)&.fetch(:to, nil) &&
1105
+ !::Lutaml::Model::Attribute.custom_from_probe?(type)
1106
+ return value
1107
+ end
1108
+
1109
+ wrapped_value = type.new(value)
1086
1110
  wrapped_value.public_send(:"to_#{format}")
1087
1111
  else
1088
1112
  value