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
@@ -8,20 +8,45 @@ module Lutaml
8
8
  # This class maintains MECE responsibility: it only stores and organizes
9
9
  # namespace needs data. It does NOT make decisions or build XML.
10
10
  class NamespaceNeeds
11
- attr_reader :namespaces, :children, :type_namespaces,
12
- :type_namespace_classes, :type_attribute_namespaces,
13
- :type_element_namespaces, :type_refs,
14
- :namespace_scope_configs
15
-
16
- def initialize
17
- @namespaces = {} # Hash<String, NamespaceUsage>
18
- @children = {} # Hash<Symbol, NamespaceNeeds>
19
- @type_namespaces = {} # Hash<Symbol, Class>
20
- @type_namespace_classes = Set.new # Set<Class>
21
- @type_attribute_namespaces = Set.new # Set<Class>
22
- @type_element_namespaces = Set.new # Set<Class>
23
- @type_refs = [] # Array<TypeNamespace::Reference>
24
- @namespace_scope_configs = [] # Array<NamespaceScopeConfig>
11
+ # Lazy readers: leaf nodes never touch most containers; the
12
+ # eager 4-Sets + 3-Hashes + 2-Arrays was the serialize path's
13
+ # top allocation site (150k births per 200-paragraph to_xml).
14
+ # Readers get the frozen empty truth; writers build on first
15
+ # touch through the ensure_* helpers below.
16
+ EMPTY_HASH = {}.freeze
17
+ EMPTY_SET = ::Set.new.freeze
18
+ EMPTY_ARRAY = [].freeze
19
+
20
+ def namespaces
21
+ @namespaces || EMPTY_HASH
22
+ end
23
+
24
+ def children
25
+ @children || EMPTY_HASH
26
+ end
27
+
28
+ def type_namespaces
29
+ @type_namespaces || EMPTY_HASH
30
+ end
31
+
32
+ def type_namespace_classes
33
+ @type_namespace_classes || EMPTY_SET
34
+ end
35
+
36
+ def type_attribute_namespaces
37
+ @type_attribute_namespaces || EMPTY_SET
38
+ end
39
+
40
+ def type_element_namespaces
41
+ @type_element_namespaces || EMPTY_SET
42
+ end
43
+
44
+ def type_refs
45
+ @type_refs || EMPTY_ARRAY
46
+ end
47
+
48
+ def namespace_scope_configs
49
+ @namespace_scope_configs || EMPTY_ARRAY
25
50
  end
26
51
 
27
52
  # Add namespace usage for a specific namespace
@@ -32,29 +57,29 @@ module Lutaml
32
57
  raise ArgumentError, "Expected NamespaceUsage, got #{usage.class}"
33
58
  end
34
59
 
35
- @namespaces[key] = usage
60
+ (@namespaces ||= {})[key] = usage
36
61
  end
37
62
 
38
63
  # Track a type namespace for a specific attribute
39
64
  # @param attr_name [Symbol] Attribute name
40
65
  # @param ns_class [Class] XmlNamespace class
41
66
  def add_type_namespace(attr_name, ns_class)
42
- @type_namespaces[attr_name] = ns_class
43
- @type_namespace_classes << ns_class
67
+ (@type_namespaces ||= {})[attr_name] = ns_class
68
+ (@type_namespace_classes ||= ::Set.new) << ns_class
44
69
  end
45
70
 
46
71
  # Add type attribute namespace to tracking set
47
72
  # @param ns_class [Class] XmlNamespace class
48
73
  def add_type_attribute_namespace(ns_class)
49
- @type_attribute_namespaces << ns_class
50
- @type_namespace_classes << ns_class
74
+ (@type_attribute_namespaces ||= ::Set.new) << ns_class
75
+ (@type_namespace_classes ||= ::Set.new) << ns_class
51
76
  end
52
77
 
53
78
  # Add type element namespace to tracking set
54
79
  # @param ns_class [Class] XmlNamespace class
55
80
  def add_type_element_namespace(ns_class)
56
- @type_element_namespaces << ns_class
57
- @type_namespace_classes << ns_class
81
+ (@type_element_namespaces ||= ::Set.new) << ns_class
82
+ (@type_namespace_classes ||= ::Set.new) << ns_class
58
83
  end
59
84
 
60
85
  # Add a type reference for lazy resolution
@@ -65,13 +90,13 @@ module Lutaml
65
90
  "Expected TypeNamespace::Reference, got #{reference.class}"
66
91
  end
67
92
 
68
- @type_refs << reference
93
+ (@type_refs ||= []) << reference
69
94
  end
70
95
 
71
96
  # Clear type references after resolution
72
97
  # Used by TypeNamespaceResolver to prevent reprocessing
73
98
  def clear_type_refs
74
- @type_refs.clear
99
+ @type_refs&.clear
75
100
  end
76
101
 
77
102
  # Add namespace scope configuration
@@ -82,7 +107,7 @@ module Lutaml
82
107
  "Expected NamespaceScopeConfig, got #{config.class}"
83
108
  end
84
109
 
85
- @namespace_scope_configs << config
110
+ (@namespace_scope_configs ||= []) << config
86
111
  end
87
112
 
88
113
  # Add child needs
@@ -94,7 +119,7 @@ module Lutaml
94
119
  "Expected NamespaceNeeds, got #{child_needs.class}"
95
120
  end
96
121
 
97
- @children[name] = child_needs
122
+ (@children ||= {})[name] = child_needs
98
123
  end
99
124
 
100
125
  # Merge another NamespaceNeeds into this one
@@ -105,9 +130,9 @@ module Lutaml
105
130
  raise ArgumentError, "Expected NamespaceNeeds, got #{other.class}"
106
131
  end
107
132
 
108
- # Merge namespaces
133
+ # Merge namespaces (build containers on first touch)
109
134
  other.namespaces.each do |key, usage|
110
- if @namespaces.key?(key)
135
+ if (@namespaces ||= {}).key?(key)
111
136
  @namespaces[key].merge(usage)
112
137
  else
113
138
  @namespaces[key] = usage
@@ -116,7 +141,7 @@ module Lutaml
116
141
 
117
142
  # Merge children
118
143
  other.children.each do |name, child_needs|
119
- if @children.key?(name)
144
+ if (@children ||= {}).key?(name)
120
145
  @children[name].merge(child_needs)
121
146
  else
122
147
  @children[name] = child_needs
@@ -124,17 +149,20 @@ module Lutaml
124
149
  end
125
150
 
126
151
  # Merge type namespaces
127
- @type_namespaces.merge!(other.type_namespaces)
128
- @type_namespace_classes.merge(other.type_namespace_classes)
129
- @type_attribute_namespaces.merge(other.type_attribute_namespaces)
130
- @type_element_namespaces.merge(other.type_element_namespaces)
152
+ (@type_namespaces ||= {}).merge!(other.type_namespaces)
153
+ (@type_namespace_classes ||= ::Set.new)
154
+ .merge(other.type_namespace_classes)
155
+ (@type_attribute_namespaces ||= ::Set.new)
156
+ .merge(other.type_attribute_namespaces)
157
+ (@type_element_namespaces ||= ::Set.new)
158
+ .merge(other.type_element_namespaces)
131
159
 
132
160
  # Merge type refs
133
- @type_refs.concat(other.type_refs)
161
+ (@type_refs ||= []).concat(other.type_refs)
134
162
 
135
163
  # Merge namespace scope configs (avoiding duplicates)
136
164
  other.namespace_scope_configs.each do |config|
137
- unless @namespace_scope_configs.any? do |c|
165
+ unless (@namespace_scope_configs ||= []).any? do |c|
138
166
  c.namespace_class == config.namespace_class
139
167
  end
140
168
  @namespace_scope_configs << config
@@ -147,32 +175,32 @@ module Lutaml
147
175
  # Check if needs are empty
148
176
  # @return [Boolean]
149
177
  def empty?
150
- @namespaces.empty? &&
151
- @children.empty? &&
152
- @type_refs.empty? &&
153
- @type_namespaces.empty? &&
154
- @namespace_scope_configs.empty?
178
+ namespaces.empty? &&
179
+ children.empty? &&
180
+ type_refs.empty? &&
181
+ type_namespaces.empty? &&
182
+ namespace_scope_configs.empty?
155
183
  end
156
184
 
157
185
  # Get namespace usage by key
158
186
  # @param key [String] Namespace key
159
187
  # @return [NamespaceUsage, nil]
160
188
  def namespace(key)
161
- @namespaces[key]
189
+ namespaces[key]
162
190
  end
163
191
 
164
192
  # Get child needs by name
165
193
  # @param name [Symbol] Child attribute name
166
194
  # @return [NamespaceNeeds, nil]
167
195
  def child(name)
168
- @children[name]
196
+ children[name]
169
197
  end
170
198
 
171
199
  # Check if a namespace is in scope configuration
172
200
  # @param ns_class [Class] XmlNamespace class
173
201
  # @return [NamespaceScopeConfig, nil]
174
202
  def scope_config_for(ns_class)
175
- @namespace_scope_configs.find do |config|
203
+ namespace_scope_configs.find do |config|
176
204
  config.namespace_class == ns_class
177
205
  end
178
206
  end
@@ -180,28 +208,28 @@ module Lutaml
180
208
  # Get all namespace classes (from usage and type namespaces)
181
209
  # @return [Set<Class>]
182
210
  def all_namespace_classes
183
- namespace_classes = Set.new(@namespaces.values.map(&:namespace_class))
184
- namespace_classes.merge(@type_namespace_classes)
211
+ classes = Set.new(namespaces.values.map(&:namespace_class))
212
+ classes.merge(type_namespace_classes)
185
213
  end
186
214
 
187
215
  # Validate internal consistency
188
216
  # @raise [RuntimeError] if inconsistent state detected
189
217
  def validate!
190
218
  # Type attribute and element namespaces should be mutually exclusive
191
- overlap = @type_attribute_namespaces & @type_element_namespaces
219
+ overlap = type_attribute_namespaces & type_element_namespaces
192
220
  unless overlap.empty?
193
221
  raise "Type namespaces appear in both attribute and element contexts: #{overlap.to_a}"
194
222
  end
195
223
 
196
224
  # All type namespace classes should be in type_namespace_classes
197
- @type_attribute_namespaces.each do |ns_class|
198
- unless @type_namespace_classes.include?(ns_class)
225
+ type_attribute_namespaces.each do |ns_class|
226
+ unless type_namespace_classes.include?(ns_class)
199
227
  raise "Type attribute namespace #{ns_class} not in type_namespace_classes"
200
228
  end
201
229
  end
202
230
 
203
- @type_element_namespaces.each do |ns_class|
204
- unless @type_namespace_classes.include?(ns_class)
231
+ type_element_namespaces.each do |ns_class|
232
+ unless type_namespace_classes.include?(ns_class)
205
233
  raise "Type element namespace #{ns_class} not in type_namespace_classes"
206
234
  end
207
235
  end
@@ -24,6 +24,10 @@ module Lutaml
24
24
  @instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
25
25
  end
26
26
 
27
+ def mapping
28
+ @mapping ||= instance.class.mappings_for(:xml, register)
29
+ end
30
+
27
31
  # (element, rule name) -> resolved attribute name / matched
28
32
  # attribute VALUE for the lenient lookup paths. Both are pure
29
33
  # functions of the element and the name; ISO-13849-shaped
@@ -80,25 +84,9 @@ module Lutaml
80
84
  # Empty unless the mapping uses when_attribute at all; non-Serialize
81
85
  # custom models have no mappings to partition.
82
86
  def when_attribute_partition
83
- @when_attribute_partition ||=
84
- if instance_is_serialize && (mapping = xml_mapping)
85
- siblings = {}
86
- plain_names = {}
87
- mapping.mappings.each do |rule|
88
- next if rule.attribute? || rule.content_mapping? ||
89
- rule.raw_mapping? || rule.cdata
90
-
91
- pairs = rule.when_attribute
92
- if pairs && !pairs.empty?
93
- (siblings[rule.name.to_s] ||= []) << rule
94
- else
95
- plain_names[rule.name.to_s] = true
96
- end
97
- end
98
- [siblings, plain_names]
99
- else
100
- [{}, {}]
101
- end
87
+ return [{}, {}] unless instance_is_serialize && (mapping = xml_mapping)
88
+
89
+ mapping.when_attribute_partition
102
90
  end
103
91
 
104
92
  def when_attribute_siblings_by_name
@@ -148,7 +148,8 @@ module Lutaml
148
148
  def target_namespace_from(model, value)
149
149
  model.target_namespace = value
150
150
  model.target_namespace_prefix =
151
- namespace_prefix_for(model.pending_plan_root_element, value)
151
+ model.import_declaration_plan
152
+ &.global_prefix_registry&.[](value)
152
153
  end
153
154
 
154
155
  # Find a type definition by local name
@@ -63,7 +63,11 @@ module Lutaml
63
63
  #
64
64
  # @return [Array<String>]
65
65
  def xml_schema_paths
66
- inherited = if superclass.respond_to?(:xml_schema_paths)
66
+ # Type check, not a probe: every Serializable carries the
67
+ # class method; a plain (non-Serializable) superclass ends
68
+ # the chain.
69
+ inherited = if superclass.is_a?(Class) &&
70
+ superclass.include?(Lutaml::Model::Serialize)
67
71
  superclass.xml_schema_paths
68
72
  else
69
73
  []
@@ -101,20 +101,26 @@ model_class, register_id)
101
101
  return
102
102
  end
103
103
 
104
- # Extract parent's namespace info for element_form_default inheritance
105
- parent_ns_class = parent.namespace_class
106
- # Only pass element_form_default VALUE if it was explicitly set
107
- # When not set (defaults to :unqualified), pass nil to avoid incorrect blank namespace treatment
108
- parent_element_form_default = if parent_ns_class&.element_form_default_set?
109
- parent_ns_class.element_form_default
110
- end
111
-
112
- # Merge parent context into options
113
- child_options = options.merge(
114
- parent_namespace_class: parent_ns_class,
115
- parent_element_form_default: parent_element_form_default,
116
- parent_element: parent,
117
- )
104
+ # Fast path: the ordered walk pre-sets the parent context keys
105
+ # for every value it emits (one hash per walk, not per value).
106
+ # The three keys are always written together, so a parent
107
+ # identity match implies the namespace context matches too.
108
+ if options[:parent_element] == parent
109
+ child_options = options
110
+ else
111
+ parent_ns_class = parent.namespace_class
112
+ # Only pass element_form_default VALUE if it was explicitly set
113
+ # When not set (defaults to :unqualified), pass nil to avoid incorrect blank namespace treatment
114
+ parent_element_form_default = if parent_ns_class&.element_form_default_set?
115
+ parent_ns_class.element_form_default
116
+ end
117
+
118
+ child_options = options.dup
119
+ child_options[:parent_namespace_class] = parent_ns_class
120
+ child_options[:parent_element_form_default] =
121
+ parent_element_form_default
122
+ child_options[:parent_element] = parent
123
+ end
118
124
 
119
125
  element = yield(rule, value, child_options)
120
126
  parent.add_child(element) if element
@@ -428,7 +434,7 @@ text_node_count = 0, use_content_index = false)
428
434
  # reconciliation could not place
429
435
  def apply_remaining_rules(model_instance, options, compiled_rules,
430
436
  mapping, processed_text_nodes, fallback_rules)
431
- attr_order = model_instance.respond_to?(:attribute_order) &&
437
+ attr_order = model_instance.is_a?(Lutaml::Model::Serialize) &&
432
438
  model_instance.attribute_order
433
439
 
434
440
  rules_to_apply = if attr_order
@@ -265,6 +265,20 @@ module Lutaml
265
265
  # Loop-invariant for the whole ordered walk (see
266
266
  # apply_standard_rules): one merge per model, not per rule.
267
267
  rule_options = options.merge(current_model: model_instance)
268
+ # `root` is the parent of every element value this walk emits,
269
+ # so the parent context keys are walk-invariant too: set them
270
+ # once here and apply_element_rule_single's fast path reuses
271
+ # this hash for every value instead of merging per value.
272
+ # rule_options itself keeps the caller's parent context for
273
+ # the attribute/content/raw rules dispatched via apply_rule.
274
+ element_value_options = rule_options.dup
275
+ root_ns_class = root.namespace_class
276
+ element_value_options[:parent_namespace_class] = root_ns_class
277
+ element_value_options[:parent_element_form_default] =
278
+ if root_ns_class&.element_form_default_set?
279
+ root_ns_class.element_form_default
280
+ end
281
+ element_value_options[:parent_element] = root
268
282
  apply_rules_in_order(
269
283
  root, model_instance, options,
270
284
  compiled_rules, model_class, register_id
@@ -281,7 +295,7 @@ module Lutaml
281
295
  parent: root,
282
296
  rule: rule,
283
297
  value: value,
284
- options: rule_options,
298
+ options: element_value_options,
285
299
  ) do |r, v, child_opts|
286
300
  element = create_element_for_value(r, v, child_opts, model_class,
287
301
  register_id, register)
@@ -8,13 +8,20 @@ module Lutaml
8
8
  module Serializers
9
9
  module_function
10
10
 
11
+ # The default :xml String serializer is the identity for String
12
+ # values. Attribute#serialize_value fast-paths String→String
13
+ # through an identity comparison with this exact proc, so a
14
+ # consumer override of the serializer disables that fast path
15
+ # automatically.
16
+ STRING_XML_TO = ->(inst) { inst.value&.to_s }.freeze
17
+
11
18
  def register_all!
12
19
  v = Lutaml::Model::Type::Value
13
20
 
14
21
  # String — value&.to_s
15
22
  v.register_format_type_serializer(
16
23
  :xml, Lutaml::Model::Type::String,
17
- to: ->(inst) { inst.value&.to_s },
24
+ to: STRING_XML_TO,
18
25
  from: ->(val) { Lutaml::Model::Type::String.cast(val) }
19
26
  )
20
27
 
@@ -14,18 +14,50 @@ module Lutaml
14
14
  # Semantic notes vs the standard adapter's Psych.safe_load:
15
15
  # implicit typing is parity-verified (compat_11 schema); anchors
16
16
  # and aliases always resolve; a tagged value without a core type
17
- # materializes as plain data instead of raising DisallowedClass.
17
+ # materializes as plain data instead of raising DisallowedClass;
18
+ # parse failures surface as Psych::SyntaxError like the standard
19
+ # adapter's (the engine's own ParseError is re-raised under the
20
+ # standard class).
18
21
  class YeptrisAdapter < StandardAdapter
19
22
  # yeptris is required lazily: the ruby-platform variant installs
20
23
  # everywhere but only loads where libyeptris exists (no Windows
21
24
  # prebuilts) — a broken install must degrade to detection
22
25
  # fallback, never crash loads.
23
26
  def self.parse(yaml, _options = {})
24
- require "yeptris"
27
+ require_engine
25
28
  require "yeptris/yaml"
26
29
  # rubocop:disable-next Naming/VariableNumber -- upstream schema literal
27
30
  Yeptris::YAML.load(yaml, schema: :compat_11)
31
+ rescue Yeptris::ParseError => e
32
+ raise syntax_error(e)
33
+ end
34
+
35
+ # The engine require sits outside parse's rescue so a broken
36
+ # install still surfaces its LoadError (the detection fallback's
37
+ # contract) instead of a NameError from the rescue clause.
38
+ def self.require_engine
39
+ require "yeptris"
40
+ end
41
+ private_class_method :require_engine
42
+
43
+ # The parse-error contract is the standard adapter's: invalid
44
+ # input raises Psych::SyntaxError regardless of the engine, so
45
+ # consumers' rescue ladders (and this gem's own
46
+ # format_error_types conversion to InvalidFormatError) hold on
47
+ # every adapter. The C parser reports the position inside the
48
+ # error message ("... at line L, column C"); lift it into the
49
+ # structured fields. Without psych (Opal) the engine error
50
+ # propagates unchanged.
51
+ def self.syntax_error(error)
52
+ return error unless defined?(::Psych::SyntaxError)
53
+
54
+ match = /\bline (\d+),? column (\d+)/.match(error.message)
55
+ line = match ? match[1].to_i : 0
56
+ column = match ? match[2].to_i : 0
57
+ problem = error.message.sub(/\s+at\s+line\s+\d+,?\s+column\s+\d+\z/, "")
58
+ ::Psych::SyntaxError.new(nil, line, column, 0, problem, nil)
28
59
  end
60
+ private_class_method :syntax_error
29
61
 
30
62
  def to_yaml(_options = {})
31
63
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
@@ -47,7 +47,7 @@ end
47
47
 
48
48
  Lutaml::Model::Config.instance.xml_plan_fast_path = true
49
49
  measure.call("PlanWalk parse + 5000 model.new") do
50
- result = Lutaml::Xml::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
50
+ result = Lutaml::Xml::Leptris::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
51
51
  raise "PlanWalk returned nil" unless result&.item&.size == 5000
52
52
  end
53
53
 
data/lutaml-model.gemspec CHANGED
@@ -33,15 +33,18 @@ Gem::Specification.new do |spec|
33
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
34
 
35
35
  # required for liquid
36
- spec.add_dependency "base64"
36
+ spec.add_dependency "base64", ">= 0.1.1"
37
37
  spec.add_dependency "bigdecimal"
38
38
  spec.add_dependency "canon"
39
39
  spec.add_dependency "concurrent-ruby"
40
40
  spec.add_dependency "liquid", ">= 4.0", "< 6.0"
41
- spec.add_dependency "moxml", "~> 0.5.56"
41
+ # 0.5.84 always passes XML_PARSE_HUGE to libxml2: very large documents
42
+ # parse completely instead of being silently truncated at the 10 MB
43
+ # input-buffer cap (lutaml-model#871).
44
+ spec.add_dependency "moxml", "~> 0.5.84"
42
45
  spec.add_dependency "ostruct"
43
- spec.add_dependency "rubyzip", "~> 3.4"
44
- spec.add_dependency "thor"
46
+ spec.add_dependency "rubyzip", "~> 3.7"
47
+ spec.add_dependency "thor", "~> 1.4"
45
48
 
46
49
  spec.metadata["rubygems_mfa_required"] = "true"
47
50
  end
@@ -25,9 +25,9 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Objec
25
25
  .to eq("a" => 1.5, "b" => nil, "c" => true, "d" => "1e3")
26
26
  end
27
27
 
28
- it "raises on invalid JSON" do
28
+ it "raises JSON::ParserError on invalid JSON, like the standard adapter" do
29
29
  expect { described_class.parse("{nope") }
30
- .to raise_error(Yeptris::JSON::ParseError)
30
+ .to raise_error(JSON::ParserError)
31
31
  end
32
32
  end
33
33
 
@@ -59,5 +59,10 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Objec
59
59
  expect(model.roles).to eq(%w[admin dev])
60
60
  expect(model_class.from_json(model.to_json).roles).to eq(%w[admin dev])
61
61
  end
62
+
63
+ it "surfaces invalid JSON as InvalidFormatError" do
64
+ expect { model_class.from_json("{nope") }
65
+ .to raise_error(Lutaml::Model::InvalidFormatError)
66
+ end
62
67
  end
63
68
  end
@@ -5,11 +5,15 @@ require "spec_helper"
5
5
  RSpec.describe Lutaml::KeyValue::Transformation::ValueSerializer do
6
6
  let(:format) { :json }
7
7
  let(:register_id) { :default }
8
+ # No doubles (house rule): a real transformation factory. Under
9
+ # hash-direct emission (TODO.perf 19) transform returns the content
10
+ # hash itself.
8
11
  let(:transformation_factory) do
9
- ->(_type_class) do
10
- double("Transformation",
11
- transform: double("Element",
12
- to_hash: { "__root__" => { "name" => "test" } }))
12
+ ->(type_class) do
13
+ Lutaml::KeyValue::Transformation.new(
14
+ type_class, type_class.mappings_for(format, register_id),
15
+ format, register_id
16
+ )
13
17
  end
14
18
  end
15
19
 
@@ -106,7 +110,7 @@ RSpec.describe Lutaml::KeyValue::Transformation::ValueSerializer do
106
110
  end
107
111
 
108
112
  it "validates type mismatch" do
109
- wrong_value = double("wrong_type")
113
+ wrong_value = Object.new
110
114
  expect do
111
115
  serializer.serialize_item(wrong_value, rule)
112
116
  end.to raise_error(Lutaml::Model::IncorrectModelError, /but should be/)