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.
- checksums.yaml +4 -4
- data/.github/workflows/performance.yml +1 -1
- data/Gemfile +6 -0
- data/README.adoc +16 -7
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/native-engines.adoc +61 -0
- data/docs/_pages/configuration.adoc +11 -7
- data/docs/_pages/serialization_adapters.adoc +14 -5
- data/lib/compat/opal/lutaml_model_boot.rb +2 -0
- data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
- data/lib/lutaml/json/adapter.rb +1 -0
- data/lib/lutaml/json/format.rb +1 -1
- data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
- data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
- data/lib/lutaml/key_value/adapter/toml.rb +1 -0
- data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/transform.rb +82 -0
- data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
- data/lib/lutaml/key_value/transformation.rb +25 -1
- data/lib/lutaml/model/adapter_resolver.rb +23 -6
- data/lib/lutaml/model/attribute.rb +31 -6
- data/lib/lutaml/model/configuration.rb +5 -0
- data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
- data/lib/lutaml/model/serialize/format_conversion.rb +37 -0
- data/lib/lutaml/model/toml.rb +10 -6
- data/lib/lutaml/model/type/date_time.rb +2 -2
- data/lib/lutaml/model/type/value.rb +24 -2
- data/lib/lutaml/model/utils.rb +6 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
- data/lib/lutaml/toml/adapter.rb +1 -0
- data/lib/lutaml/toml/format.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
- data/lib/lutaml/xml/adapter_element.rb +111 -3
- data/lib/lutaml/xml/builder/base.rb +3 -1
- data/lib/lutaml/xml/builder/leptris.rb +13 -0
- data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
- data/lib/lutaml/xml/builder.rb +1 -0
- data/lib/lutaml/xml/format.rb +4 -1
- data/lib/lutaml/xml/leptris/element.rb +34 -0
- data/lib/lutaml/xml/plan_compiler.rb +139 -0
- data/lib/lutaml/xml/plan_hydrator.rb +80 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
- data/lib/lutaml/xml/xml_element.rb +6 -2
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +40 -0
- data/lib/lutaml/yaml/adapter.rb +5 -0
- data/lib/lutaml/yaml/format.rb +1 -1
- data/lib/tasks/performance.rake +5 -0
- data/lib/tasks/performance_comparator.rb +19 -3
- data/lib/tasks/performance_probe.rake +33 -0
- data/lib/tasks/performance_probe.rb +106 -0
- data/lib/tasks/performance_probe_xml.rb +54 -0
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
- data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
- data/spec/lutaml/model/json_adapter_spec.rb +6 -0
- data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
- data/spec/lutaml/model/toml_spec.rb +36 -1
- data/spec/lutaml/xml/plan_fast_path_spec.rb +96 -0
- data/spec/lutaml/xml/xml_spec.rb +13 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
- data/spec/spec_helper.rb +7 -1
- metadata +23 -4
|
@@ -286,7 +286,7 @@ module Lutaml
|
|
|
286
286
|
if format == :toml && type_name == :tomlib && RuntimeCompatibility.windows?
|
|
287
287
|
raise ArgumentError,
|
|
288
288
|
"The `:tomlib` adapter is not supported on Windows due to " \
|
|
289
|
-
"segmentation fault issues. Please use `:toml_rb` instead."
|
|
289
|
+
"segmentation fault issues. Please use `:teptris` or `:toml_rb` instead."
|
|
290
290
|
end
|
|
291
291
|
|
|
292
292
|
available = adapter_config[:available]
|
|
@@ -363,11 +363,23 @@ module Lutaml
|
|
|
363
363
|
detect_xml_adapter
|
|
364
364
|
when :toml
|
|
365
365
|
detect_toml_adapter
|
|
366
|
+
when :yaml, :json
|
|
367
|
+
detect_yeptris_kv_adapter(format)
|
|
366
368
|
else
|
|
367
369
|
metadata.dig(format, :default)
|
|
368
370
|
end
|
|
369
371
|
end
|
|
370
372
|
|
|
373
|
+
# Native key-value engine preference: yeptris (libyeptris C11)
|
|
374
|
+
# when its gem is in the bundle, else the format's standard
|
|
375
|
+
# adapter. Opt-in by bundle contents — nothing is required of
|
|
376
|
+
# consumers that do not add yeptris.
|
|
377
|
+
def detect_yeptris_kv_adapter(format)
|
|
378
|
+
return metadata.dig(format, :default) if Lutaml::Model.opal?
|
|
379
|
+
|
|
380
|
+
Utils.safe_load("yeptris", :Yeptris) ? :yeptris : metadata.dig(format, :default)
|
|
381
|
+
end
|
|
382
|
+
|
|
371
383
|
# Detect available XML adapter.
|
|
372
384
|
#
|
|
373
385
|
# Delegates to moxml which is the authority on XML adapter
|
|
@@ -380,17 +392,22 @@ module Lutaml
|
|
|
380
392
|
|
|
381
393
|
# Detect available TOML adapter.
|
|
382
394
|
#
|
|
383
|
-
# @return [Symbol, nil] :tomlib, :toml_rb, or nil
|
|
395
|
+
# @return [Symbol, nil] :teptris, :tomlib, :toml_rb, or nil
|
|
384
396
|
def detect_toml_adapter
|
|
385
397
|
return nil if Lutaml::Model.opal?
|
|
386
398
|
|
|
387
|
-
|
|
388
|
-
|
|
399
|
+
# teptris 0.2.12+ parses ~3x and dumps ~8x faster than
|
|
400
|
+
# tomlib and ships prebuilts for every platform (mingw-ucrt
|
|
401
|
+
# restored in 0.2.4+) — native TOML on Windows retires the
|
|
402
|
+
# tomlib segfault workaround (previously pure-Ruby toml-rb
|
|
403
|
+
# only).
|
|
404
|
+
return :teptris if Utils.safe_load("teptris", :Teptris)
|
|
389
405
|
|
|
390
|
-
|
|
406
|
+
if !RuntimeCompatibility.windows? &&
|
|
407
|
+
Utils.safe_load("tomlib", :Tomlib)
|
|
408
|
+
return :tomlib
|
|
391
409
|
end
|
|
392
410
|
|
|
393
|
-
return :tomlib if Utils.safe_load("tomlib", :Tomlib)
|
|
394
411
|
return :toml_rb if Utils.safe_load("toml-rb", :TomlRb)
|
|
395
412
|
|
|
396
413
|
nil
|
|
@@ -31,6 +31,29 @@ module Lutaml
|
|
|
31
31
|
union_member_types
|
|
32
32
|
].freeze
|
|
33
33
|
|
|
34
|
+
# Per-type-class memo of the custom from_xml/from_json probe used by #cast.
|
|
35
|
+
# Isolated cache holder: enclosing-class freezes (some suites freeze
|
|
36
|
+
# Lutaml::Model::Attribute) would freeze a constant Hash with it.
|
|
37
|
+
TypeProbeCache = ::Class.new do
|
|
38
|
+
class << self
|
|
39
|
+
def cache
|
|
40
|
+
@cache ||= {}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# Public reader for compiled rule plans: does this type class carry a
|
|
47
|
+
# custom from_xml/from_json (answer static, probed once)?
|
|
48
|
+
def custom_from_probe?(type)
|
|
49
|
+
TypeProbeCache.cache[type] ||= begin
|
|
50
|
+
base = Lutaml::Model::Type::Value.singleton_class
|
|
51
|
+
type.method(:from_xml).owner != base ||
|
|
52
|
+
type.method(:from_json).owner != base
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
34
57
|
MODEL_STRINGS = [
|
|
35
58
|
Lutaml::Model::Type::String,
|
|
36
59
|
"String",
|
|
@@ -252,7 +275,9 @@ module Lutaml
|
|
|
252
275
|
end
|
|
253
276
|
|
|
254
277
|
def union?
|
|
255
|
-
|
|
278
|
+
return @union unless @union.nil?
|
|
279
|
+
|
|
280
|
+
@union = unresolved_type == Lutaml::Model::Type::Union
|
|
256
281
|
end
|
|
257
282
|
|
|
258
283
|
def union_member_types
|
|
@@ -687,11 +712,11 @@ instance_object = nil)
|
|
|
687
712
|
|
|
688
713
|
# Fast path for Type::Value subclasses (String, Integer, Boolean, etc.)
|
|
689
714
|
# These are never Serializable, so skip expensive can_serialize? and needs_conversion? checks
|
|
690
|
-
# Skip if type has custom from_xml/from_json methods (defined on the class itself, not inherited)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
return resolved_type.cast(value)
|
|
715
|
+
# Skip if type has custom from_xml/from_json methods (defined on the class itself, not inherited).
|
|
716
|
+
# The probe allocated two Method objects per cast call; the answer is
|
|
717
|
+
# static per type class, so it is resolved once.
|
|
718
|
+
if resolved_type.is_a?(Class) && resolved_type < Lutaml::Model::Type::Value && !self.class.custom_from_probe?(resolved_type)
|
|
719
|
+
return resolved_type.cast(value)
|
|
695
720
|
end
|
|
696
721
|
|
|
697
722
|
klass = resolve_polymorphic_class(resolved_type, value, options)
|
|
@@ -15,10 +15,15 @@ module Lutaml
|
|
|
15
15
|
#
|
|
16
16
|
class Configuration
|
|
17
17
|
attr_reader :default_register
|
|
18
|
+
# Experimental: whole-document native materialization for plan-
|
|
19
|
+
# compilable XML models (Leptris::XML::Descriptor walk). Opt-in
|
|
20
|
+
# while the full semantics audit completes.
|
|
21
|
+
attr_accessor :xml_plan_fast_path
|
|
18
22
|
|
|
19
23
|
def initialize
|
|
20
24
|
@default_register = :default
|
|
21
25
|
@configured = false
|
|
26
|
+
@xml_plan_fast_path = false
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def configure
|
|
@@ -3,10 +3,10 @@ module Lutaml
|
|
|
3
3
|
class UnknownAdapterTypeError < Error
|
|
4
4
|
# Available adapters by format
|
|
5
5
|
AVAILABLE_ADAPTERS = {
|
|
6
|
-
xml: %w[nokogiri ox oga rexml],
|
|
7
|
-
json: %w[standard multi_json oj],
|
|
8
|
-
yaml: %w[standard],
|
|
9
|
-
toml: %w[tomlib toml_rb],
|
|
6
|
+
xml: %w[nokogiri ox oga rexml leptris],
|
|
7
|
+
json: %w[standard multi_json oj yeptris],
|
|
8
|
+
yaml: %w[standard yeptris],
|
|
9
|
+
toml: %w[teptris tomlib toml_rb],
|
|
10
10
|
hash: %w[standard],
|
|
11
11
|
jsonl: %w[standard],
|
|
12
12
|
yamls: %w[standard],
|
|
@@ -45,6 +45,11 @@ module Lutaml
|
|
|
45
45
|
# @param options [Hash] Additional options
|
|
46
46
|
# @return [Object] The deserialized model instance
|
|
47
47
|
def from(format, data, options = {})
|
|
48
|
+
if format == :xml && Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
49
|
+
fast = xml_plan_fast_path(data, options)
|
|
50
|
+
return fast if fast
|
|
51
|
+
end
|
|
52
|
+
|
|
48
53
|
Instrumentation.instrument(:from, model: name, format: format) do
|
|
49
54
|
adapter = resolve_adapter(format, options.delete(:adapter))
|
|
50
55
|
|
|
@@ -76,6 +81,36 @@ module Lutaml
|
|
|
76
81
|
# No-op by default; XML overrides via prepend
|
|
77
82
|
end
|
|
78
83
|
|
|
84
|
+
# Whole-document native materialization (Phase 5): compile the
|
|
85
|
+
# mapping into a Leptris descriptor plan and hydrate from one
|
|
86
|
+
# plan walk. Only when the resolved XML adapter is leptris-
|
|
87
|
+
# backed, the model fully compiles, and no path-affecting
|
|
88
|
+
# options are present; anything else falls back to the
|
|
89
|
+
# interpretive pipeline.
|
|
90
|
+
def xml_plan_fast_path(data, options)
|
|
91
|
+
return nil unless defined?(::Leptris::XML::Descriptor)
|
|
92
|
+
return nil if options.key?(:adapter) || options.key?(:only) ||
|
|
93
|
+
options.key?(:except) || options.key?(:mappings) ||
|
|
94
|
+
options.key?(:register)
|
|
95
|
+
|
|
96
|
+
adapter_name = Lutaml::Model::Config.adapter_for(:xml)
|
|
97
|
+
adapter_name = adapter_name&.name
|
|
98
|
+
return nil unless adapter_name.to_s.end_with?("LeptrisAdapter")
|
|
99
|
+
|
|
100
|
+
register = Lutaml::Model::Config.default_register
|
|
101
|
+
plan = Lutaml::Xml::PlanCompiler.compile(self, register)
|
|
102
|
+
return nil unless plan
|
|
103
|
+
|
|
104
|
+
root = ::Leptris::XML.parse(data.to_s).root
|
|
105
|
+
return nil if root.nil?
|
|
106
|
+
return nil unless root.name == plan[:tree][:name]
|
|
107
|
+
|
|
108
|
+
Lutaml::Xml::PlanHydrator.call(self, plan,
|
|
109
|
+
plan[:descriptor].walk(root))
|
|
110
|
+
rescue ::Leptris::XML::ParseError => e
|
|
111
|
+
raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
|
|
112
|
+
end
|
|
113
|
+
|
|
79
114
|
# Get list of error types that can be raised during format parsing.
|
|
80
115
|
# Core errors are always included; format-specific errors come from
|
|
81
116
|
# FormatRegistry registrations.
|
|
@@ -119,6 +154,8 @@ module Lutaml
|
|
|
119
154
|
toml_errors = Array(toml_errors)
|
|
120
155
|
tomllib_err = compatibility.safe_constantize("Tomlib::ParseError")
|
|
121
156
|
toml_errors << tomllib_err if tomllib_err
|
|
157
|
+
teptris_err = compatibility.safe_constantize("Teptris::ParseError")
|
|
158
|
+
toml_errors << teptris_err if teptris_err
|
|
122
159
|
|
|
123
160
|
@format_error_types_base + toml_errors
|
|
124
161
|
end
|
data/lib/lutaml/model/toml.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Lutaml
|
|
|
7
7
|
module Toml
|
|
8
8
|
Lutaml::Model::RuntimeCompatibility.define_native_aliases(
|
|
9
9
|
self,
|
|
10
|
+
TeptrisAdapter: "::Lutaml::Toml::Adapter::TeptrisAdapter",
|
|
10
11
|
TomlibAdapter: "::Lutaml::Toml::Adapter::TomlibAdapter",
|
|
11
12
|
TomlRbAdapter: "::Lutaml::Toml::Adapter::TomlRbAdapter",
|
|
12
13
|
)
|
|
@@ -18,15 +19,18 @@ module Lutaml
|
|
|
18
19
|
def self.detect_toml_adapter
|
|
19
20
|
return nil if Lutaml::Model.opal?
|
|
20
21
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# teptris 0.2.12+ parses ~3x and dumps ~8x faster than tomlib
|
|
23
|
+
# and ships prebuilts for every platform (mingw-ucrt restored
|
|
24
|
+
# in 0.2.4+) — native TOML on Windows retires the tomlib
|
|
25
|
+
# segfault workaround (previously pure-Ruby toml-rb only).
|
|
26
|
+
return :teptris if Lutaml::Model::Utils.safe_load("teptris", :Teptris)
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
# tomlib segfaults on Windows when parsing invalid TOML
|
|
29
|
+
if !Lutaml::Model::RuntimeCompatibility.windows? &&
|
|
30
|
+
Lutaml::Model::Utils.safe_load("tomlib", :Tomlib)
|
|
31
|
+
return :tomlib
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
# On non-Windows, prefer tomlib
|
|
29
|
-
return :tomlib if Lutaml::Model::Utils.safe_load("tomlib", :Tomlib)
|
|
30
34
|
return :toml_rb if Lutaml::Model::Utils.safe_load("toml-rb", :TomlRb)
|
|
31
35
|
|
|
32
36
|
nil
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
|
+
# Isolated holder for format_type_serializer_for's memo: Type::Value
|
|
6
|
+
# can be frozen by suites, which would freeze an ivar Hash with it.
|
|
7
|
+
SERIALIZER_LOOKUP_CACHE = ::Class.new do
|
|
8
|
+
class << self
|
|
9
|
+
def cache
|
|
10
|
+
@cache ||= {}
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end.cache
|
|
14
|
+
|
|
5
15
|
module Type
|
|
6
16
|
# Base class for all value types
|
|
7
17
|
class Value
|
|
@@ -35,14 +45,26 @@ from: nil)
|
|
|
35
45
|
# @param type_class [Class] the type class to look up
|
|
36
46
|
# @return [Hash, nil] { to: Proc, from: Proc } or nil
|
|
37
47
|
def format_type_serializer_for(format, type_class)
|
|
48
|
+
# Hot path: memoize per [format, resolved class] — the hierarchy
|
|
49
|
+
# walk below only runs on a miss. Format serializers register at
|
|
50
|
+
# load time, so a hit is stable.
|
|
51
|
+
cache = SERIALIZER_LOOKUP_CACHE
|
|
52
|
+
key = [format, type_class]
|
|
53
|
+
return cache[key] if cache.key?(key)
|
|
54
|
+
|
|
38
55
|
klass = type_class
|
|
56
|
+
found = nil
|
|
39
57
|
while klass && klass <= Value
|
|
40
58
|
s = @format_type_serializers[[format, klass]]
|
|
41
|
-
|
|
59
|
+
if s
|
|
60
|
+
found = s
|
|
61
|
+
break
|
|
62
|
+
end
|
|
42
63
|
|
|
43
64
|
klass = klass.superclass
|
|
44
65
|
end
|
|
45
|
-
|
|
66
|
+
cache[key] = found
|
|
67
|
+
found
|
|
46
68
|
end
|
|
47
69
|
end
|
|
48
70
|
|
data/lib/lutaml/model/utils.rb
CHANGED
|
@@ -121,11 +121,12 @@ module Lutaml
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def blank?(value)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
# Hot path: the nil test is a pointer compare, and nil dominates
|
|
125
|
+
# in optional-field-heavy documents.
|
|
126
|
+
return true if value.nil?
|
|
127
|
+
return false unless value.is_a?(::String) || value.is_a?(::Array) || value.is_a?(::Hash)
|
|
128
|
+
|
|
129
|
+
value.empty?
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def empty_collection?(collection)
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../key_value/adapter/toml/teptris_adapter"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Toml
|
|
7
|
+
module Adapter
|
|
8
|
+
# The implementation lives in the KeyValue namespace (the live
|
|
9
|
+
# surface for adapter resolution); this alias keeps the legacy
|
|
10
|
+
# Lutaml::Toml::Adapter surface complete.
|
|
11
|
+
TeptrisAdapter = Lutaml::KeyValue::Adapter::Toml::TeptrisAdapter
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/lutaml/toml/adapter.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Lutaml
|
|
|
11
11
|
autoload :Transform, "#{__dir__}/adapter/transform"
|
|
12
12
|
Lutaml::Model::RuntimeCompatibility.autoload_native(
|
|
13
13
|
self,
|
|
14
|
+
TeptrisAdapter: "#{__dir__}/adapter/teptris_adapter",
|
|
14
15
|
TomlRbAdapter: "#{__dir__}/adapter/toml_rb_adapter",
|
|
15
16
|
TomlibAdapter: "#{__dir__}/adapter/tomlib_adapter",
|
|
16
17
|
)
|
data/lib/lutaml/toml/format.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "moxml/adapter/leptris"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Xml
|
|
7
|
+
module Adapter
|
|
8
|
+
class LeptrisAdapter < BaseAdapter
|
|
9
|
+
MOXML_ADAPTER = Moxml::Adapter::Leptris
|
|
10
|
+
BUILDER_CLASS = Builder::Leptris
|
|
11
|
+
PARSED_ELEMENT_CLASS = Lutaml::Xml::LeptrisElement
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -22,7 +22,11 @@ module Lutaml
|
|
|
22
22
|
text = case node
|
|
23
23
|
when Moxml::Element
|
|
24
24
|
namespace_name = node.namespace&.prefix
|
|
25
|
-
|
|
25
|
+
# moxml >= 0.5.33 contract: #namespaces is the full
|
|
26
|
+
# in-scope map (parity with Nokogiri, #198/#201); the
|
|
27
|
+
# own-declarations shape this parse path needs is
|
|
28
|
+
# #namespace_definitions.
|
|
29
|
+
ns_defs = node.namespace_definitions
|
|
26
30
|
|
|
27
31
|
has_empty_xmlns = ns_defs.any? do |ns|
|
|
28
32
|
ns.prefix.nil? && ns.uri == ""
|
|
@@ -172,14 +176,118 @@ module Lutaml
|
|
|
172
176
|
def parse_children(node, default_namespace: nil)
|
|
173
177
|
return [] unless node.children
|
|
174
178
|
|
|
179
|
+
# Non-element children (text/cdata/comment/PI) stay RAW moxml
|
|
180
|
+
# nodes in the list — the hot scalar parse path reads text off
|
|
181
|
+
# the element (inner_text) and never touches their wrappers, so
|
|
182
|
+
# wrapping every text node eagerly paid half the parse tree for
|
|
183
|
+
# nothing. #children wraps them on first access; document order
|
|
184
|
+
# is preserved because they sit at their original positions.
|
|
175
185
|
node.children.filter_map do |child|
|
|
176
186
|
next if (child.is_a?(Moxml::Text) || child.is_a?(Moxml::Cdata)) && child.content.empty?
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
if child.is_a?(Moxml::Element)
|
|
189
|
+
self.class.new(child, parent: self,
|
|
190
|
+
default_namespace: default_namespace)
|
|
191
|
+
else
|
|
192
|
+
child
|
|
193
|
+
end
|
|
180
194
|
end
|
|
181
195
|
end
|
|
182
196
|
|
|
197
|
+
public
|
|
198
|
+
|
|
199
|
+
def children
|
|
200
|
+
unless @non_element_children_wrapped
|
|
201
|
+
@children.map! do |child|
|
|
202
|
+
if child.is_a?(Moxml::Node) && !child.is_a?(Moxml::Element)
|
|
203
|
+
self.class.new(child, parent: self)
|
|
204
|
+
else
|
|
205
|
+
child
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
@non_element_children_wrapped = true
|
|
209
|
+
end
|
|
210
|
+
@children
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def children=(new_children)
|
|
214
|
+
@non_element_children_wrapped = true
|
|
215
|
+
super
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def element_children
|
|
219
|
+
return @element_children if defined?(@element_children)
|
|
220
|
+
|
|
221
|
+
@element_children = @children.reject do |child|
|
|
222
|
+
raw_non_element_child?(child) ||
|
|
223
|
+
(child.is_a?(XmlElement) &&
|
|
224
|
+
(child.text? || child.processing_instruction?))
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def raw_non_element_child?(child)
|
|
229
|
+
child.is_a?(Moxml::Node) && !child.is_a?(Moxml::Element)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Order consumes raw non-element children inline: wrapping them
|
|
233
|
+
# just to read .text/.content would materialize the whole lazy
|
|
234
|
+
# layer on every parse.
|
|
235
|
+
def order
|
|
236
|
+
return @order_cache if @order_cache
|
|
237
|
+
|
|
238
|
+
@order_cache = @children.filter_map do |child|
|
|
239
|
+
case child
|
|
240
|
+
when Moxml::Cdata
|
|
241
|
+
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
242
|
+
text_content: child.content,
|
|
243
|
+
node_type: :cdata)
|
|
244
|
+
when Moxml::Text
|
|
245
|
+
next if child.content.nil?
|
|
246
|
+
|
|
247
|
+
Lutaml::Xml::Element.new("Text", "text",
|
|
248
|
+
text_content: child.content,
|
|
249
|
+
node_type: :text)
|
|
250
|
+
when Moxml::Comment
|
|
251
|
+
Lutaml::Xml::Element.new("Comment", "comment",
|
|
252
|
+
text_content: child.content,
|
|
253
|
+
node_type: :comment)
|
|
254
|
+
when Moxml::ProcessingInstruction
|
|
255
|
+
Lutaml::Xml::Element.new("ProcessingInstruction",
|
|
256
|
+
child.target,
|
|
257
|
+
text_content: child.content.to_s.sub(/\A\s+/, ""),
|
|
258
|
+
node_type: :processing_instruction)
|
|
259
|
+
else
|
|
260
|
+
next if child.is_a?(Moxml::Node)
|
|
261
|
+
|
|
262
|
+
if child.cdata?
|
|
263
|
+
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
264
|
+
text_content: child.text,
|
|
265
|
+
node_type: :cdata)
|
|
266
|
+
elsif child.text?
|
|
267
|
+
next if child.text.nil?
|
|
268
|
+
|
|
269
|
+
Lutaml::Xml::Element.new("Text", "text",
|
|
270
|
+
text_content: child.text,
|
|
271
|
+
node_type: :text)
|
|
272
|
+
elsif child.comment?
|
|
273
|
+
Lutaml::Xml::Element.new("Comment", "comment",
|
|
274
|
+
text_content: child.text,
|
|
275
|
+
node_type: :comment)
|
|
276
|
+
elsif child.processing_instruction?
|
|
277
|
+
Lutaml::Xml::Element.new("ProcessingInstruction",
|
|
278
|
+
child.unprefixed_name,
|
|
279
|
+
text_content: child.text,
|
|
280
|
+
node_type: :processing_instruction)
|
|
281
|
+
else
|
|
282
|
+
Lutaml::Xml::Element.new("Element", child.unprefixed_name,
|
|
283
|
+
node_type: :element,
|
|
284
|
+
namespace_uri: child.namespace_uri,
|
|
285
|
+
namespace_prefix: child.namespace_prefix)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end.each(&:freeze).freeze
|
|
289
|
+
end
|
|
290
|
+
|
|
183
291
|
def add_namespaces_from_defs(ns_defs, is_root: false)
|
|
184
292
|
has_default_xmlns = is_root || ns_defs.any? { |ns| ns.prefix.nil? }
|
|
185
293
|
|
|
@@ -223,7 +223,9 @@ module Lutaml
|
|
|
223
223
|
ns = new_el.in_scope_namespaces.find { |n| n.prefix == prefix }
|
|
224
224
|
new_el.namespace = ns if ns
|
|
225
225
|
elsif !prefix_unset && prefix.nil?
|
|
226
|
-
|
|
226
|
+
# Skip the no-op clear: namespace-less elements pay the
|
|
227
|
+
# undeclare path on some engines (leptris) for nothing.
|
|
228
|
+
new_el.namespace = nil unless new_el.namespace.nil?
|
|
227
229
|
end
|
|
228
230
|
end
|
|
229
231
|
|
data/lib/lutaml/xml/builder.rb
CHANGED
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -21,6 +21,8 @@ end
|
|
|
21
21
|
|
|
22
22
|
module Lutaml
|
|
23
23
|
module Xml
|
|
24
|
+
autoload :PlanCompiler, "#{__dir__}/plan_compiler"
|
|
25
|
+
autoload :PlanHydrator, "#{__dir__}/plan_hydrator"
|
|
24
26
|
# Error module for XML-specific errors
|
|
25
27
|
module Error
|
|
26
28
|
autoload :XmlError, "#{__dir__}/error/xml_error"
|
|
@@ -161,6 +163,7 @@ module Lutaml
|
|
|
161
163
|
NokogiriElement: "#{__dir__}/nokogiri/element",
|
|
162
164
|
Nokogiri: "#{__dir__}/nokogiri",
|
|
163
165
|
Ox: "#{__dir__}/ox",
|
|
166
|
+
LeptrisElement: "#{__dir__}/leptris/element",
|
|
164
167
|
)
|
|
165
168
|
end
|
|
166
169
|
end
|
|
@@ -192,7 +195,7 @@ Lutaml::Model::FormatRegistry.register(
|
|
|
192
195
|
}
|
|
193
196
|
else
|
|
194
197
|
{
|
|
195
|
-
available: %i[nokogiri ox oga rexml],
|
|
198
|
+
available: %i[nokogiri ox oga rexml leptris],
|
|
196
199
|
default: :nokogiri,
|
|
197
200
|
}
|
|
198
201
|
end,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
class LeptrisElement < AdapterElement
|
|
6
|
+
def initialize(node, parent: nil, default_namespace: nil)
|
|
7
|
+
@raw_text = node.raw_content if node.is_a?(Moxml::Text)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def adapter_class
|
|
14
|
+
Lutaml::Xml::Adapter::LeptrisAdapter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_text_for_xml
|
|
18
|
+
@raw_text || @text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_element_xml(builder)
|
|
22
|
+
builder.create_and_add_element(name,
|
|
23
|
+
prefix: namespace_prefix,
|
|
24
|
+
attributes: build_attributes(self)) do |xml|
|
|
25
|
+
children.each { |child| child.build_xml(xml) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attribute_value_for_build(attr)
|
|
30
|
+
attr.raw_value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|