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.
- checksums.yaml +4 -4
- data/.github/scripts/test_installed_gem.rb +58 -0
- data/.github/workflows/downstream-performance.yml +28 -3
- data/.github/workflows/js-pr-check.yml +4 -0
- data/.github/workflows/js-sync.yml +4 -0
- data/.github/workflows/opal.yml +9 -0
- data/.github/workflows/rake.yml +6 -41
- data/.github/workflows/release.yml +41 -0
- data/Gemfile +17 -3
- data/bench/bench_alloc_audit.rb +56 -0
- data/bench/bench_common.rb +11 -0
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/gem-integration.adoc +85 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +8 -1
- data/docs/_guides/native-engines.adoc +23 -5
- data/docs/_guides/schema-generation.adoc +25 -0
- data/docs/_guides/type-registers-contexts.adoc +126 -0
- data/lib/compat/opal/lutaml_model_boot.rb +7 -5
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
- data/lib/lutaml/key_value/data_model/element.rb +5 -1
- data/lib/lutaml/key_value/mapping_rule.rb +7 -0
- data/lib/lutaml/key_value/transform.rb +29 -24
- data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
- data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
- data/lib/lutaml/key_value/transformation.rb +64 -116
- data/lib/lutaml/model/adapter_resolver.rb +7 -3
- data/lib/lutaml/model/attribute.rb +74 -15
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
- data/lib/lutaml/model/render_policy.rb +26 -8
- data/lib/lutaml/model/schema/base_schema.rb +15 -0
- data/lib/lutaml/model/schema/generator/definition.rb +52 -8
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
- data/lib/lutaml/model/schema.rb +4 -2
- data/lib/lutaml/model/serializable.rb +33 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
- data/lib/lutaml/model/serialize/initialization.rb +54 -7
- data/lib/lutaml/model/serialize/model_import.rb +60 -0
- data/lib/lutaml/model/serialize.rb +14 -6
- data/lib/lutaml/model/type/time.rb +6 -0
- data/lib/lutaml/model/type/value.rb +9 -7
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/utils.rb +18 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
- data/lib/lutaml/xml/adapter_loader.rb +5 -2
- data/lib/lutaml/xml/format.rb +18 -16
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
- data/lib/lutaml/xml/leptris.rb +46 -0
- data/lib/lutaml/xml/mapping.rb +71 -18
- data/lib/lutaml/xml/model_transform.rb +231 -8
- data/lib/lutaml/xml/namespace.rb +18 -8
- data/lib/lutaml/xml/namespace_collector.rb +17 -1
- data/lib/lutaml/xml/namespace_needs.rb +77 -49
- data/lib/lutaml/xml/parse_session.rb +7 -19
- data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
- data/lib/lutaml/xml/transformation.rb +15 -1
- data/lib/lutaml/xml/type/serializers.rb +8 -1
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/lutaml-model.gemspec +7 -4
- data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
- data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
- data/spec/lutaml/key_value/transformation_spec.rb +87 -21
- data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
- data/spec/lutaml/model/collection_validation_spec.rb +265 -0
- data/spec/lutaml/model/desc_spec.rb +50 -0
- data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
- data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
- data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
- data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
- data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
- data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
- data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
- data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
- data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
- data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
- data/spec/person_spec.rb +8 -0
- metadata +48 -17
- data/lib/lutaml/xml/plan_compiler.rb +0 -332
- data/lib/lutaml/xml/plan_hydrator.rb +0 -367
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -252
- 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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.
|
|
129
|
-
|
|
130
|
-
@
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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 =
|
|
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
|
-
|
|
198
|
-
unless
|
|
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
|
-
|
|
204
|
-
unless
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
->(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 =
|
|
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/)
|