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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "XML plan fast path: (URI, local) dispatch and W3C xml:* attributes" do
|
|
6
|
+
# Same-name element rows in different namespaces must dispatch by
|
|
7
|
+
# the child type's exact URI — the interpretive matcher's
|
|
8
|
+
# typed-element identity. The w14/w15 docId shape (OOXML
|
|
9
|
+
# settings.xml) collapsed to name-only binding: one row captured
|
|
10
|
+
# both elements and the other stayed empty, so round-trips lost a
|
|
11
|
+
# docId and downstream reconcilers regenerated it.
|
|
12
|
+
let(:w14_ns_class) do
|
|
13
|
+
klass = Class.new(Lutaml::Xml::Namespace) do
|
|
14
|
+
uri "http://schemas.microsoft.com/office/word/2010/wordml"
|
|
15
|
+
prefix_default "w14"
|
|
16
|
+
attribute_form_default :qualified
|
|
17
|
+
end
|
|
18
|
+
stub_const("PlanUriLocal::W14Ns", klass)
|
|
19
|
+
klass
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:w15_ns_class) do
|
|
23
|
+
klass = Class.new(Lutaml::Xml::Namespace) do
|
|
24
|
+
uri "http://schemas.microsoft.com/office/word/2012/wordml"
|
|
25
|
+
prefix_default "w15"
|
|
26
|
+
attribute_form_default :qualified
|
|
27
|
+
end
|
|
28
|
+
stub_const("PlanUriLocal::W15Ns", klass)
|
|
29
|
+
klass
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:w_ns_class) do
|
|
33
|
+
klass = Class.new(Lutaml::Xml::Namespace) do
|
|
34
|
+
uri "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
35
|
+
prefix_default "w"
|
|
36
|
+
end
|
|
37
|
+
stub_const("PlanUriLocal::WNs", klass)
|
|
38
|
+
klass
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
let(:w14_doc_id_class) do
|
|
42
|
+
w14_ns_class
|
|
43
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
44
|
+
attribute :val, :string
|
|
45
|
+
|
|
46
|
+
xml do
|
|
47
|
+
element "docId"
|
|
48
|
+
namespace PlanUriLocal::W14Ns
|
|
49
|
+
map_attribute "val", to: :val
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
stub_const("PlanUriLocal::W14DocId", klass)
|
|
53
|
+
klass
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
let(:w15_doc_id_class) do
|
|
57
|
+
w15_ns_class
|
|
58
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
59
|
+
attribute :val, :string
|
|
60
|
+
|
|
61
|
+
xml do
|
|
62
|
+
element "docId"
|
|
63
|
+
namespace PlanUriLocal::W15Ns
|
|
64
|
+
map_attribute "val", to: :val
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
stub_const("PlanUriLocal::W15DocId", klass)
|
|
68
|
+
klass
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def define_settings_class
|
|
72
|
+
w_ns_class
|
|
73
|
+
w14_doc_id_class
|
|
74
|
+
w15_doc_id_class
|
|
75
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
76
|
+
attribute :w14_doc_id, PlanUriLocal::W14DocId
|
|
77
|
+
attribute :w15_doc_id, PlanUriLocal::W15DocId
|
|
78
|
+
|
|
79
|
+
xml do
|
|
80
|
+
element "settings"
|
|
81
|
+
namespace PlanUriLocal::WNs
|
|
82
|
+
namespace_scope [
|
|
83
|
+
{ namespace: PlanUriLocal::W14Ns, declare: :always },
|
|
84
|
+
{ namespace: PlanUriLocal::W15Ns, declare: :always },
|
|
85
|
+
]
|
|
86
|
+
map_element "docId", to: :w14_doc_id, render_nil: false
|
|
87
|
+
map_element "docId", to: :w15_doc_id, render_nil: false
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def settings_xml(body)
|
|
93
|
+
"<w:settings xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" " \
|
|
94
|
+
"xmlns:w14=\"http://schemas.microsoft.com/office/word/2010/wordml\" " \
|
|
95
|
+
"xmlns:w15=\"http://schemas.microsoft.com/office/word/2012/wordml\">#{body}</w:settings>"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
around do |example|
|
|
99
|
+
old = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
100
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
101
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
102
|
+
ensure
|
|
103
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = old
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "binds same-name rows by exact URI regardless of document order" do
|
|
107
|
+
settings_class = define_settings_class
|
|
108
|
+
|
|
109
|
+
[%(<w14:docId w14:val="{A}"/><w15:docId w15:val="{B}"/>),
|
|
110
|
+
%(<w15:docId w15:val="{B}"/><w14:docId w14:val="{A}"/>)].each do |body|
|
|
111
|
+
settings = settings_class.from_xml(settings_xml(body))
|
|
112
|
+
|
|
113
|
+
expect(settings.w14_doc_id&.val).to eq("{A}")
|
|
114
|
+
expect(settings.w15_doc_id&.val).to eq("{B}")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "leaves the absent-namespace row unset for single-docId documents" do
|
|
119
|
+
settings_class = define_settings_class
|
|
120
|
+
|
|
121
|
+
only_w14 = settings_class.from_xml(
|
|
122
|
+
settings_xml(%(<w14:docId w14:val="{A}"/>)),
|
|
123
|
+
)
|
|
124
|
+
expect(only_w14.w14_doc_id&.val).to eq("{A}")
|
|
125
|
+
expect(only_w14.w15_doc_id).to be_nil
|
|
126
|
+
|
|
127
|
+
only_w15 = settings_class.from_xml(
|
|
128
|
+
settings_xml(%(<w15:docId w15:val="{B}"/>)),
|
|
129
|
+
)
|
|
130
|
+
expect(only_w15.w14_doc_id).to be_nil
|
|
131
|
+
expect(only_w15.w15_doc_id&.val).to eq("{B}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "emits each docId under its own namespace on serialize" do
|
|
135
|
+
settings_class = define_settings_class
|
|
136
|
+
settings = settings_class.from_xml(
|
|
137
|
+
settings_xml(%(<w14:docId w14:val="{A}"/><w15:docId w15:val="{B}"/>)),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
output = settings.to_xml
|
|
141
|
+
expect(output).to include("<w14:docId w14:val=\"{A}\"/>")
|
|
142
|
+
expect(output).to include("<w15:docId w15:val=\"{B}\"/>")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Exact-URI attribute rows capture by (URI, local): a prefixed rule
|
|
146
|
+
# spelling with a namespaced type (xlink:href over XlinkHrefType)
|
|
147
|
+
# binds its local part under the row's URI — the full raw name
|
|
148
|
+
# matched nothing and the attribute was dropped.
|
|
149
|
+
it "binds prefixed attribute rules with namespaced types by (URI, local)" do
|
|
150
|
+
text_class = Class.new(Lutaml::Model::Serializable) do
|
|
151
|
+
attribute :xlink_href, Lutaml::Xml::W3c::XlinkHrefType
|
|
152
|
+
|
|
153
|
+
xml do
|
|
154
|
+
element "link"
|
|
155
|
+
map_attribute "xlink:href", to: :xlink_href
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
stub_const("PlanUriLocal::Xlink", text_class)
|
|
159
|
+
|
|
160
|
+
source = '<link xmlns:xlink="http://www.w3.org/1999/xlink" ' \
|
|
161
|
+
'xlink:href="https://example.com"/>'
|
|
162
|
+
link = text_class.from_xml(source)
|
|
163
|
+
|
|
164
|
+
expect(link.xlink_href).to eq("https://example.com")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The xml prefix is implicit (W3C-reserved): xml:* attribute rules
|
|
168
|
+
# bind by raw name and must not be qualified under the model's
|
|
169
|
+
# namespace by attribute form defaults (w:t's xml:space under a
|
|
170
|
+
# :qualified WML mapping dropped the attribute entirely).
|
|
171
|
+
it "round-trips xml:space through the plan lane" do
|
|
172
|
+
w_ns_class
|
|
173
|
+
text_class = Class.new(Lutaml::Model::Serializable) do
|
|
174
|
+
attribute :content, :string
|
|
175
|
+
attribute :xml_space, :string
|
|
176
|
+
|
|
177
|
+
xml do
|
|
178
|
+
element "t"
|
|
179
|
+
namespace PlanUriLocal::WNs
|
|
180
|
+
map_content to: :content
|
|
181
|
+
map_attribute "xml:space", to: :xml_space
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
stub_const("PlanUriLocal::Text", text_class)
|
|
185
|
+
|
|
186
|
+
source = '<w:t xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ' \
|
|
187
|
+
'xml:space="preserve">hi </w:t>'
|
|
188
|
+
text = text_class.from_xml(source)
|
|
189
|
+
|
|
190
|
+
expect(text.xml_space).to eq("preserve")
|
|
191
|
+
expect(text.content).to eq("hi ")
|
|
192
|
+
expect(text.to_xml).to include('xml:space="preserve"')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Writer-added values sit outside the parse-built element_order.
|
|
196
|
+
# The ordered serialize lane reconciles them at the interpretive
|
|
197
|
+
# anchors — but only for instances a writer has touched; pristine
|
|
198
|
+
# parsed instances skip the reconciliation scan entirely.
|
|
199
|
+
it "emits writer-added children after parse and skips the scan when pristine" do
|
|
200
|
+
w_ns_class
|
|
201
|
+
w14_doc_id_class
|
|
202
|
+
zoom_class = Class.new(Lutaml::Model::Serializable) do
|
|
203
|
+
attribute :percent, :string
|
|
204
|
+
|
|
205
|
+
xml do
|
|
206
|
+
element "zoom"
|
|
207
|
+
namespace PlanUriLocal::WNs
|
|
208
|
+
map_attribute "percent", to: :percent
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
stub_const("PlanUriLocal::Zoom", zoom_class)
|
|
212
|
+
settings_class = Class.new(Lutaml::Model::Serializable) do
|
|
213
|
+
attribute :zoom, PlanUriLocal::Zoom
|
|
214
|
+
attribute :w14_doc_id, PlanUriLocal::W14DocId
|
|
215
|
+
|
|
216
|
+
xml do
|
|
217
|
+
element "settings"
|
|
218
|
+
namespace PlanUriLocal::WNs
|
|
219
|
+
map_element "zoom", to: :zoom
|
|
220
|
+
map_element "docId", to: :w14_doc_id
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
stub_const("PlanUriLocal::Settings", settings_class)
|
|
224
|
+
|
|
225
|
+
source = settings_xml(%(<w:zoom w:percent="100"/>))
|
|
226
|
+
pristine = settings_class.from_xml(source)
|
|
227
|
+
expect(pristine.zoom.percent).to eq("100")
|
|
228
|
+
expect(pristine.to_xml).not_to include("docId")
|
|
229
|
+
|
|
230
|
+
mutated = settings_class.from_xml(source)
|
|
231
|
+
mutated.w14_doc_id = w14_doc_id_class.new(val: "{A}")
|
|
232
|
+
output = mutated.to_xml
|
|
233
|
+
expect(output).to include("<docId")
|
|
234
|
+
expect(output).to include('val="{A}"')
|
|
235
|
+
expect(output.index("zoom")).to be < output.index("docId")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Root declaration-plan parity: plan-hydrated roots carry the same
|
|
239
|
+
# import_declaration_plan state the interpretive parse records, so
|
|
240
|
+
# consumers and the interpretive serialize fallback see in-scope
|
|
241
|
+
# declarations (#872).
|
|
242
|
+
it "records the source declaration plan on plan-hydrated roots" do
|
|
243
|
+
settings_class = define_settings_class
|
|
244
|
+
settings = settings_class.from_xml(
|
|
245
|
+
settings_xml(%(<w14:docId w14:val="{A}"/>)),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
plan = settings.import_declaration_plan
|
|
249
|
+
expect(plan).to be_a(Lutaml::Xml::DeclarationPlan)
|
|
250
|
+
hoisted = plan.root_node.hoisted_declarations
|
|
251
|
+
expect(hoisted["w14"]).to eq("http://schemas.microsoft.com/office/word/2010/wordml")
|
|
252
|
+
|
|
253
|
+
skipped = settings_class.from_xml(
|
|
254
|
+
settings_xml(%(<w14:docId w14:val="{A}"/>)),
|
|
255
|
+
import_declaration_plan: :skip,
|
|
256
|
+
)
|
|
257
|
+
expect(skipped.import_declaration_plan).to be_nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Processing-instruction mappings (rfcxml's <?rfc toc?>) have no
|
|
261
|
+
# plan-row spelling: compiling the model silently dropped
|
|
262
|
+
# pi_settings. The compiler refuses such models — the interpretive
|
|
263
|
+
# path owns PIs.
|
|
264
|
+
it "opts models with processing-instruction mappings out of the plan path" do
|
|
265
|
+
rfc_class = Class.new(Lutaml::Model::Serializable) do
|
|
266
|
+
attribute :pi_settings, :hash
|
|
267
|
+
attribute :version, :string
|
|
268
|
+
|
|
269
|
+
xml do
|
|
270
|
+
element "rfc"
|
|
271
|
+
map_processing_instruction "rfc", to: :pi_settings
|
|
272
|
+
map_attribute "version", to: :version
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
stub_const("PlanUriLocal::Rfc", rfc_class)
|
|
276
|
+
|
|
277
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
|
|
278
|
+
rfc_class, Lutaml::Model::Config.default_register
|
|
279
|
+
)
|
|
280
|
+
expect(plan).to be_nil
|
|
281
|
+
|
|
282
|
+
source = %(<?rfc toc="yes"?>\n<rfc version="3"/>)
|
|
283
|
+
rfc = rfc_class.from_xml(source)
|
|
284
|
+
expect(rfc.version).to eq("3")
|
|
285
|
+
expect(rfc.pi_settings["toc"]).to eq("yes")
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
-
require "lutaml/xml/plan_walk"
|
|
4
|
+
require "lutaml/xml/leptris/plan_walk"
|
|
5
5
|
|
|
6
6
|
RSpec.describe "XML PlanWalk consumer surface" do
|
|
7
7
|
around do |example|
|
|
@@ -25,7 +25,7 @@ RSpec.describe "XML PlanWalk consumer surface" do
|
|
|
25
25
|
|
|
26
26
|
it "hydrates equal to the interpretive path on a single-row document" do
|
|
27
27
|
xml = %(<row id="7"><name>Alice</name><tags>x</tags><tags>y</tags></row>)
|
|
28
|
-
walked = Lutaml::Xml::PlanWalk.call(item_class, xml)
|
|
28
|
+
walked = Lutaml::Xml::Leptris::PlanWalk.call(item_class, xml)
|
|
29
29
|
expected = item_class.from_xml(xml)
|
|
30
30
|
|
|
31
31
|
expect(walked).to be_a(item_class)
|
|
@@ -46,7 +46,7 @@ RSpec.describe "XML PlanWalk consumer surface" do
|
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
walked = Lutaml::Xml::PlanWalk.call(root_class, xml)
|
|
49
|
+
walked = Lutaml::Xml::Leptris::PlanWalk.call(root_class, xml)
|
|
50
50
|
expected = root_class.from_xml(xml)
|
|
51
51
|
|
|
52
52
|
expect(walked.item).to be_an(Array)
|
|
@@ -71,7 +71,7 @@ RSpec.describe "XML PlanWalk consumer surface" do
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
walked = Lutaml::Xml::PlanWalk.call(root_class, xml)
|
|
74
|
+
walked = Lutaml::Xml::Leptris::PlanWalk.call(root_class, xml)
|
|
75
75
|
interpretive = root_class.from_xml(xml)
|
|
76
76
|
expect(walked.item.size).to eq(interpretive.item.size)
|
|
77
77
|
expect(walked.item.map(&:id)).to eq(interpretive.item.map(&:id))
|
|
@@ -94,9 +94,9 @@ RSpec.describe "XML PlanWalk consumer surface" do
|
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
97
|
+
expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
|
|
98
98
|
Lutaml::Model::Config.default_register)).not_to be_nil
|
|
99
|
-
expect(Lutaml::Xml::PlanWalk.call(klass, "<doc><note>x</note></doc>").tgt.note)
|
|
99
|
+
expect(Lutaml::Xml::Leptris::PlanWalk.call(klass, "<doc><note>x</note></doc>").tgt.note)
|
|
100
100
|
.to eq("x")
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "XML plan fast path: map_any_element catch-all" do
|
|
6
|
+
# leptris#1552: the wildcard child row rides a two-pass walk where
|
|
7
|
+
# named rows take precedence; the hydrator bridges the unclaimed
|
|
8
|
+
# remainder in document order and casts it through the attribute —
|
|
9
|
+
# the interpretive pass's contract for map_any_element.
|
|
10
|
+
|
|
11
|
+
let(:host_class) do
|
|
12
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :title, :string
|
|
14
|
+
attribute :others, :string, collection: true
|
|
15
|
+
|
|
16
|
+
xml do
|
|
17
|
+
element "host"
|
|
18
|
+
map_element "title", to: :title
|
|
19
|
+
map_any_element to: :others
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
stub_const("Gate4Any::Host", klass)
|
|
23
|
+
klass
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:typed_class) do
|
|
27
|
+
item = Class.new(Lutaml::Model::Serializable) do
|
|
28
|
+
attribute :content, :string
|
|
29
|
+
|
|
30
|
+
xml do
|
|
31
|
+
element "item"
|
|
32
|
+
map_content to: :content
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
stub_const("Gate4Any::Item", item)
|
|
36
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
37
|
+
attribute :label, :string
|
|
38
|
+
attribute :items, Gate4Any::Item, collection: true
|
|
39
|
+
|
|
40
|
+
xml do
|
|
41
|
+
element "bag"
|
|
42
|
+
map_element "label", to: :label
|
|
43
|
+
map_any_element to: :items
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
stub_const("Gate4Any::Bag", model)
|
|
47
|
+
model
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
around do |example|
|
|
51
|
+
old = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
52
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
53
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
54
|
+
ensure
|
|
55
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = old
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Members flow through the attribute's cast exactly as the
|
|
59
|
+
# interpretive pass does: String attributes stringify the wrapper
|
|
60
|
+
# (both paths identically), Serializable attributes build instances.
|
|
61
|
+
def member_shapes(elements)
|
|
62
|
+
elements.map { |e| e.class.name.split("::").last }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "captures unclaimed children in document order" do
|
|
66
|
+
doc = "<host><title>t</title><extra1>a</extra1>" \
|
|
67
|
+
"<extra2>b</extra2></host>"
|
|
68
|
+
|
|
69
|
+
parsed = host_class.from_xml(doc)
|
|
70
|
+
expect(parsed.title).to eq("t")
|
|
71
|
+
expect(parsed.others.size).to eq(2)
|
|
72
|
+
expect(member_shapes(parsed.others)).to eq(%w[String String])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "captures typed members as model instances" do
|
|
76
|
+
doc = "<bag><label>L</label><item>one</item><item>two</item></bag>"
|
|
77
|
+
|
|
78
|
+
parsed = typed_class.from_xml(doc)
|
|
79
|
+
expect(parsed.label).to eq("L")
|
|
80
|
+
expect(parsed.items.map(&:content)).to eq(%w[one two])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "matches the interpretive path's capture" do
|
|
84
|
+
doc = "<host><title>t</title><extra1>a</extra1><extra2>b</extra2></host>"
|
|
85
|
+
|
|
86
|
+
interpretive = Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
|
|
87
|
+
host_class.from_xml(doc)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
parsed = host_class.from_xml(doc)
|
|
91
|
+
expect(parsed.others.size).to eq(interpretive.others.size)
|
|
92
|
+
expect(member_shapes(parsed.others)).to eq(
|
|
93
|
+
member_shapes(interpretive.others).map { |n| n.sub("Nokogiri", "Leptris") },
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -30,6 +30,30 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Objec
|
|
|
30
30
|
expect(described_class.parse("d: 2020-01-01\n"))
|
|
31
31
|
.to eq("d" => Date.new(2020, 1, 1))
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
it "raises Psych::SyntaxError for invalid YAML, like the standard adapter" do
|
|
35
|
+
expect { described_class.parse("name: test\n invalid: [unclosed\n") }
|
|
36
|
+
.to raise_error(Psych::SyntaxError)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "raises Psych::SyntaxError for a mapping value inside a plain scalar" do
|
|
40
|
+
# The dependent-CI shape: one line, a second "key:" colon after a
|
|
41
|
+
# plain scalar run. Psych rejects it with "mapping values are not
|
|
42
|
+
# allowed in this context"; the engine must surface under the
|
|
43
|
+
# same class so downstream rescue ladders hold.
|
|
44
|
+
expect { described_class.parse('name: test\n invalid: [unclosed') }
|
|
45
|
+
.to raise_error(Psych::SyntaxError)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "carries the engine's position on the syntax error" do
|
|
49
|
+
expect { described_class.parse("name: test\n invalid: [unclosed\n") }
|
|
50
|
+
.to raise_error(Psych::SyntaxError) do |error|
|
|
51
|
+
expect(error.line).to be >= 1
|
|
52
|
+
expect(error.problem).to be_a(String)
|
|
53
|
+
expect(error.problem).not_to be_empty
|
|
54
|
+
expect(error.message).to include("at line #{error.line}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
33
57
|
end
|
|
34
58
|
|
|
35
59
|
describe "#to_yaml" do
|
|
@@ -59,5 +83,10 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Objec
|
|
|
59
83
|
expect(model.roles).to eq(%w[admin dev])
|
|
60
84
|
expect(model_class.from_yaml(model.to_yaml).roles).to eq(%w[admin dev])
|
|
61
85
|
end
|
|
86
|
+
|
|
87
|
+
it "surfaces invalid YAML as InvalidFormatError" do
|
|
88
|
+
expect { model_class.from_yaml("name: test\n invalid: [unclosed\n") }
|
|
89
|
+
.to raise_error(Lutaml::Model::InvalidFormatError)
|
|
90
|
+
end
|
|
62
91
|
end
|
|
63
92
|
end
|
data/spec/person_spec.rb
CHANGED
|
@@ -115,6 +115,13 @@ RSpec.describe Person do
|
|
|
115
115
|
expect(model.to_yaml).to eq(attributes_yaml.to_yaml)
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
# Root cause of the long-pended time-only misparse: psych resolves an
|
|
119
|
+
# unquoted hh:mm:ss scalar as a YAML 1.1 sexagesimal integer (25200
|
|
120
|
+
# for "07:00:00"), and the :time cast fed that integer through
|
|
121
|
+
# Time.parse, which reads "25200" as a date (the fixed 2025-07-19
|
|
122
|
+
# garbage). The cast now reads sexagesimal integers as seconds since
|
|
123
|
+
# midnight — both the psych and yeptris paths answer today-07:00.
|
|
124
|
+
# Evidence chain: moxml#336, yeptris-ruby#265.
|
|
118
125
|
it "deserializes from YAML" do
|
|
119
126
|
yaml = attributes_yaml.to_yaml
|
|
120
127
|
person = described_class.from_yaml(yaml)
|
|
@@ -127,6 +134,7 @@ RSpec.describe Person do
|
|
|
127
134
|
expect(person.active).to be true
|
|
128
135
|
end
|
|
129
136
|
|
|
137
|
+
# Same sexagesimal time-only scalar as above.
|
|
130
138
|
it "deserializes from YAML array" do
|
|
131
139
|
yaml = [attributes_yaml.dup, attributes_yaml.dup].to_yaml
|
|
132
140
|
|