lutaml-model 0.8.17 → 0.8.18
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/downstream-performance.yml +27 -1
- data/.github/workflows/js-pr-check.yml +102 -0
- data/.github/workflows/js-sync.yml +51 -0
- data/.github/workflows/opal.yml +26 -6
- data/.github/workflows/performance.yml +32 -7
- data/.github/workflows/rake.yml +78 -32
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +8 -0
- data/Gemfile +16 -1
- data/README.adoc +267 -0
- data/Rakefile +166 -4
- data/docs/_guides/schema-import.adoc +42 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_pages/validation.adoc +65 -0
- data/lib/compat/opal/generate_boot.rb +123 -0
- data/lib/compat/opal/js_bundle_entry.rb +42 -0
- data/lib/compat/opal/lutaml_model_boot.rb +497 -0
- data/lib/compat/opal/moxml_boot.rb +68 -0
- data/lib/compat/opal/yaml_compat.rb +32 -0
- data/lib/lutaml/json.rb +1 -1
- data/lib/lutaml/key_value/transform.rb +3 -8
- data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
- data/lib/lutaml/key_value/transformation.rb +17 -5
- data/lib/lutaml/model/adapter_resolver.rb +2 -2
- data/lib/lutaml/model/attribute.rb +88 -11
- data/lib/lutaml/model/cached_type_resolver.rb +1 -1
- data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
- data/lib/lutaml/model/global_context.rb +11 -8
- data/lib/lutaml/model/instrumentation.rb +2 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
- data/lib/lutaml/model/register.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +50 -0
- data/lib/lutaml/model/schema/class_loader.rb +58 -0
- data/lib/lutaml/model/schema/compiled_output.rb +83 -0
- data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
- data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
- data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
- data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
- data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
- data/lib/lutaml/model/schema/definitions/model.rb +40 -0
- data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
- data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
- data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
- data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
- data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
- data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
- data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
- data/lib/lutaml/model/schema/definitions.rb +24 -0
- data/lib/lutaml/model/schema/file_writer.rb +70 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
- data/lib/lutaml/model/schema/generator/property.rb +21 -4
- data/lib/lutaml/model/schema/module_nesting.rb +31 -0
- data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
- data/lib/lutaml/model/schema/registry_generator.rb +115 -0
- data/lib/lutaml/model/schema/renderers/base.rb +40 -0
- data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
- data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
- data/lib/lutaml/model/schema/renderers/model.rb +121 -0
- data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
- data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
- data/lib/lutaml/model/schema/renderers/union.rb +83 -0
- data/lib/lutaml/model/schema/renderers.rb +19 -0
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
- data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
- data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
- data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
- data/lib/lutaml/model/schema/templates.rb +162 -0
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
- data/lib/lutaml/model/schema.rb +19 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
- data/lib/lutaml/model/serialize/builder.rb +15 -2
- data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
- data/lib/lutaml/model/serialize.rb +6 -9
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
- data/lib/lutaml/model/services.rb +0 -2
- data/lib/lutaml/model/store.rb +2 -1
- data/lib/lutaml/model/toml.rb +1 -1
- data/lib/lutaml/model/transform.rb +17 -39
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type.rb +1 -0
- data/lib/lutaml/model/union.rb +320 -0
- data/lib/lutaml/model/utils.rb +6 -0
- data/lib/lutaml/model/validation.rb +1 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +18 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +1 -1
- data/lib/lutaml/xml/builder/base.rb +1 -1
- data/lib/lutaml/xml/builder.rb +1 -1
- data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +5 -1
- data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
- data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
- data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
- data/lib/lutaml/xml/xsd_validator.rb +66 -0
- data/lib/lutaml/xml.rb +53 -12
- data/lutaml-model.gemspec +4 -2
- data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
- data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
- data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
- data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
- data/spec/fixtures/xml/schema/rng/book.rng +27 -0
- data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
- data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
- data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
- data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
- data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
- data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
- data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
- data/spec/fixtures/xml/schema/rng/person.rng +23 -0
- data/spec/fixtures/xml/schema/rng/union.rng +14 -0
- data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
- data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
- data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +169 -12
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/mixed_content_spec.rb +171 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +30 -0
- data/spec/lutaml/model/raw_element_spec.rb +125 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
- data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
- data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
- data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
- data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
- data/spec/lutaml/model/union_attribute_spec.rb +658 -0
- data/spec/lutaml/model/union_spec.rb +257 -0
- data/spec/lutaml/model/utils_spec.rb +14 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
- data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
- data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +97 -31
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
- data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
- data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
- data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
- data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
- data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
- data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
- data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
- data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
- data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
- data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
- data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
- data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
- data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
- data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
- data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
- data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
- data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
# Guard: the generated Opal boot manifest must stay in sync with the autoloads
|
|
7
|
+
# in lib/lutaml/**. Opal ignores `autoload`, so lutaml_model_boot.rb explicitly
|
|
8
|
+
# requires every autoload target; a deleted/renamed file leaves a stale require
|
|
9
|
+
# that only fails the Opal CI job (`rake spec:opal`) — MRI's autoload hides it.
|
|
10
|
+
# Regenerating here and comparing catches that drift in the fast MRI suite.
|
|
11
|
+
RSpec.describe "Opal boot manifest" do
|
|
12
|
+
let(:generator) do
|
|
13
|
+
File.expand_path("../../../lib/compat/opal/generate_boot.rb", __dir__)
|
|
14
|
+
end
|
|
15
|
+
let(:manifest) do
|
|
16
|
+
File.expand_path("../../../lib/compat/opal/lutaml_model_boot.rb", __dir__)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "matches the generator output (regenerate: ruby lib/compat/opal/generate_boot.rb)" do
|
|
20
|
+
Dir.mktmpdir do |dir|
|
|
21
|
+
out = File.join(dir, "boot.rb")
|
|
22
|
+
ok = system({ "OPAL_BOOT_OUTPUT" => out }, RbConfig.ruby, generator,
|
|
23
|
+
out: File::NULL, err: File::NULL)
|
|
24
|
+
expect(ok).to be(true), "generate_boot.rb failed to run"
|
|
25
|
+
expect(File.read(manifest)).to eq(File.read(out)),
|
|
26
|
+
"lib/compat/opal/lutaml_model_boot.rb is out of date — run " \
|
|
27
|
+
"`ruby lib/compat/opal/generate_boot.rb` after adding/removing autoloads"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -323,6 +323,24 @@ RSpec.describe Lutaml::Model::Collection do
|
|
|
323
323
|
end
|
|
324
324
|
end
|
|
325
325
|
|
|
326
|
+
context "when a range collection allows zero elements" do
|
|
327
|
+
it "validates when the collection is given as empty" do
|
|
328
|
+
kiln = CollectionTests::Kiln.new(attributes.merge(pots: []))
|
|
329
|
+
|
|
330
|
+
expect { kiln.validate! }.not_to raise_error
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it "validates when the collection is omitted entirely" do
|
|
334
|
+
kiln = CollectionTests::Kiln.new(attributes.except(:pots))
|
|
335
|
+
|
|
336
|
+
expect { kiln.validate! }.not_to raise_error
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "does not flag the empty min-zero collection when built with no arguments" do
|
|
340
|
+
expect { CollectionTests::Kiln.new.validate! }.not_to raise_error
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
326
344
|
context "when collection with unbounded maximum exceeds minimum" do
|
|
327
345
|
let(:valid_attributes) do
|
|
328
346
|
attributes.merge(operators: ["John", "Jane", "Jim", "Jessica"])
|
|
@@ -275,6 +275,88 @@ module MixedContentSpec
|
|
|
275
275
|
map_element "b", to: :bold
|
|
276
276
|
end
|
|
277
277
|
end
|
|
278
|
+
|
|
279
|
+
# Regression: nested mixed_content text ownership
|
|
280
|
+
# Bug report claimed child's direct text was captured by the parent.
|
|
281
|
+
# Not reproducible with DOM-based adapters — text nodes are parented
|
|
282
|
+
# by the parser, not by a shared accumulator. These specs lock in
|
|
283
|
+
# correct behavior.
|
|
284
|
+
class NestedTextInner < Lutaml::Model::Serializable
|
|
285
|
+
attribute :text, :string, collection: true
|
|
286
|
+
attribute :child, :string, collection: true
|
|
287
|
+
|
|
288
|
+
xml do
|
|
289
|
+
element "inner"
|
|
290
|
+
mixed_content
|
|
291
|
+
map_content to: :text
|
|
292
|
+
map_element "child", to: :child
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
class NestedTextOuter < Lutaml::Model::Serializable
|
|
297
|
+
attribute :text, :string, collection: true
|
|
298
|
+
attribute :inner, NestedTextInner, collection: true
|
|
299
|
+
|
|
300
|
+
xml do
|
|
301
|
+
element "outer"
|
|
302
|
+
mixed_content
|
|
303
|
+
map_content to: :text
|
|
304
|
+
map_element "inner", to: :inner
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
class NestedTextInnerOrdered < Lutaml::Model::Serializable
|
|
309
|
+
attribute :text, :string, collection: true
|
|
310
|
+
attribute :child, :string, collection: true
|
|
311
|
+
|
|
312
|
+
xml do
|
|
313
|
+
element "inner"
|
|
314
|
+
ordered
|
|
315
|
+
mixed_content
|
|
316
|
+
map_content to: :text
|
|
317
|
+
map_element "child", to: :child
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
class NestedTextOuterOrdered < Lutaml::Model::Serializable
|
|
322
|
+
attribute :text, :string, collection: true
|
|
323
|
+
attribute :inner, NestedTextInnerOrdered, collection: true
|
|
324
|
+
|
|
325
|
+
xml do
|
|
326
|
+
element "outer"
|
|
327
|
+
ordered
|
|
328
|
+
mixed_content
|
|
329
|
+
map_content to: :text
|
|
330
|
+
map_element "inner", to: :inner
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Asymmetric variant: middle layer omits mixed_content while outer
|
|
335
|
+
# and inner both have it. Mirrors the original trigger from the
|
|
336
|
+
# metanorma-document BUGS.sts 06 report (FmtStemElement lacked
|
|
337
|
+
# mixed_content at the time, sandwiched between ParagraphBlock and
|
|
338
|
+
# SemxElement which both had it). Symmetric coverage alone misses
|
|
339
|
+
# this case.
|
|
340
|
+
class AsymmetricMiddle < Lutaml::Model::Serializable
|
|
341
|
+
attribute :inner, NestedTextInner, collection: true
|
|
342
|
+
|
|
343
|
+
xml do
|
|
344
|
+
element "middle"
|
|
345
|
+
map_element "inner", to: :inner
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
class AsymmetricOuter < Lutaml::Model::Serializable
|
|
350
|
+
attribute :text, :string, collection: true
|
|
351
|
+
attribute :middle, AsymmetricMiddle, collection: true
|
|
352
|
+
|
|
353
|
+
xml do
|
|
354
|
+
element "outer"
|
|
355
|
+
mixed_content
|
|
356
|
+
map_content to: :text
|
|
357
|
+
map_element "middle", to: :middle
|
|
358
|
+
end
|
|
359
|
+
end
|
|
278
360
|
end
|
|
279
361
|
|
|
280
362
|
RSpec.describe "MixedContent" do
|
|
@@ -1244,6 +1326,95 @@ RSpec.describe "MixedContent" do
|
|
|
1244
1326
|
end
|
|
1245
1327
|
end
|
|
1246
1328
|
end
|
|
1329
|
+
|
|
1330
|
+
# Regression: nested mixed_content text ownership
|
|
1331
|
+
# Verifies that text directly inside a child element with mixed_content
|
|
1332
|
+
# is attributed to the CHILD, not the parent. Covers standard and
|
|
1333
|
+
# ordered variants, single and multiple children, and outer text.
|
|
1334
|
+
context "nested mixed_content text ownership" do
|
|
1335
|
+
context "with standard mixed_content" do
|
|
1336
|
+
it "attributes child's direct text to the child, not the parent" do
|
|
1337
|
+
outer = MixedContentSpec::NestedTextOuter.from_xml(
|
|
1338
|
+
"<outer><inner>target text<child>x</child></inner></outer>",
|
|
1339
|
+
)
|
|
1340
|
+
inner = outer.inner.first
|
|
1341
|
+
|
|
1342
|
+
expect(inner.text).to eq(["target text"])
|
|
1343
|
+
expect(inner.child).to eq(["x"])
|
|
1344
|
+
expect(outer.text).to eq([])
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
it "keeps outer text segments in the outer model" do
|
|
1348
|
+
outer = MixedContentSpec::NestedTextOuter.from_xml(
|
|
1349
|
+
"<outer>prefix<inner>target text<child>x</child></inner>suffix</outer>",
|
|
1350
|
+
)
|
|
1351
|
+
inner = outer.inner.first
|
|
1352
|
+
|
|
1353
|
+
expect(inner.text).to eq(["target text"])
|
|
1354
|
+
expect(outer.text).to eq(%w[prefix suffix])
|
|
1355
|
+
end
|
|
1356
|
+
|
|
1357
|
+
it "handles multiple nested children with their own text" do
|
|
1358
|
+
outer = MixedContentSpec::NestedTextOuter.from_xml(
|
|
1359
|
+
"<outer>p1<inner>i1text<c>x</c>i2text</inner>p2<inner>i3text</inner>p3</outer>",
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
expect(outer.inner[0].text).to eq(%w[i1text i2text])
|
|
1363
|
+
expect(outer.inner[1].text).to eq(["i3text"])
|
|
1364
|
+
expect(outer.text).to eq(%w[p1 p2 p3])
|
|
1365
|
+
end
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
context "with ordered mixed_content" do
|
|
1369
|
+
it "attributes child's direct text to the child, not the parent" do
|
|
1370
|
+
outer = MixedContentSpec::NestedTextOuterOrdered.from_xml(
|
|
1371
|
+
"<outer><inner>target text<child>x</child></inner></outer>",
|
|
1372
|
+
)
|
|
1373
|
+
inner = outer.inner.first
|
|
1374
|
+
|
|
1375
|
+
expect(inner.text).to eq(["target text"])
|
|
1376
|
+
expect(inner.child).to eq(["x"])
|
|
1377
|
+
expect(outer.text).to eq([])
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
it "keeps outer text segments in the outer model" do
|
|
1381
|
+
outer = MixedContentSpec::NestedTextOuterOrdered.from_xml(
|
|
1382
|
+
"<outer>prefix<inner>target text<child>x</child></inner>suffix</outer>",
|
|
1383
|
+
)
|
|
1384
|
+
inner = outer.inner.first
|
|
1385
|
+
|
|
1386
|
+
expect(inner.text).to eq(["target text"])
|
|
1387
|
+
expect(outer.text).to eq(%w[prefix suffix])
|
|
1388
|
+
end
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
context "with asymmetric nesting (middle layer omits mixed_content)" do
|
|
1392
|
+
# Mirrors the original trigger from metanorma-document BUGS.sts 06:
|
|
1393
|
+
# ParagraphBlock (mixed_content) > FmtStemElement (no mixed_content
|
|
1394
|
+
# at the time) > SemxElement (mixed_content). Symmetric coverage
|
|
1395
|
+
# in the prior contexts does not exercise this code path.
|
|
1396
|
+
it "attributes innermost text to the innermost model" do
|
|
1397
|
+
outer = MixedContentSpec::AsymmetricOuter.from_xml(
|
|
1398
|
+
"<outer>prefix<middle><inner>target text<child>x</child></inner></middle>suffix</outer>",
|
|
1399
|
+
)
|
|
1400
|
+
inner = outer.middle.first.inner.first
|
|
1401
|
+
|
|
1402
|
+
expect(inner.text).to eq(["target text"])
|
|
1403
|
+
expect(inner.child).to eq(["x"])
|
|
1404
|
+
expect(outer.text).to eq(%w[prefix suffix])
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
it "preserves innermost text when outer has no own text" do
|
|
1408
|
+
outer = MixedContentSpec::AsymmetricOuter.from_xml(
|
|
1409
|
+
"<outer><middle><inner>target text<child>x</child></inner></middle></outer>",
|
|
1410
|
+
)
|
|
1411
|
+
inner = outer.middle.first.inner.first
|
|
1412
|
+
|
|
1413
|
+
expect(inner.text).to eq(["target text"])
|
|
1414
|
+
expect(outer.text).to eq([])
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
end
|
|
1247
1418
|
end
|
|
1248
1419
|
|
|
1249
1420
|
describe Lutaml::Xml::Adapter::NokogiriAdapter do
|
|
@@ -71,6 +71,47 @@ RSpec.describe "Opal compatibility", if: RUBY_ENGINE == "opal" do
|
|
|
71
71
|
expect(instance.flag).to be true
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
it "resolves scalar union attributes" do
|
|
75
|
+
reading = Class.new do
|
|
76
|
+
include Lutaml::Model::Serialize
|
|
77
|
+
|
|
78
|
+
attribute :value, %i[integer string]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
expect(reading.from_json('{"value":42}').value).to eq(42)
|
|
82
|
+
expect(reading.from_json('{"value":"hot"}').value).to eq("hot")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "resolves a union of a model member and a scalar" do
|
|
86
|
+
temperature = Class.new do
|
|
87
|
+
include Lutaml::Model::Serialize
|
|
88
|
+
|
|
89
|
+
attribute :celsius, :float
|
|
90
|
+
end
|
|
91
|
+
holder = Class.new do
|
|
92
|
+
include Lutaml::Model::Serialize
|
|
93
|
+
|
|
94
|
+
attribute :temp, [temperature, :string]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
structured = holder.from_json('{"temp":{"celsius":12.5}}')
|
|
98
|
+
expect(structured.temp).to be_a(temperature)
|
|
99
|
+
expect(structured.temp.celsius).to eq(12.5)
|
|
100
|
+
expect(holder.from_json('{"temp":"cold"}').temp).to eq("cold")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "validates a pattern on a union :string member" do
|
|
104
|
+
coded = Class.new do
|
|
105
|
+
include Lutaml::Model::Serialize
|
|
106
|
+
|
|
107
|
+
attribute :code, %i[integer string], pattern: /\A[A-Z]+\z/
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
expect(coded.new(code: "ABC").validate).to be_empty
|
|
111
|
+
expect(coded.new(code: "abc").validate).not_to be_empty
|
|
112
|
+
expect(coded.new(code: 42).validate).to be_empty
|
|
113
|
+
end
|
|
114
|
+
|
|
74
115
|
it "handles nested models" do
|
|
75
116
|
address = Class.new do
|
|
76
117
|
include Lutaml::Model::Serialize
|
|
@@ -110,8 +151,32 @@ RSpec.describe "Opal compatibility", if: RUBY_ENGINE == "opal" do
|
|
|
110
151
|
expect(Lutaml::Model::RuntimeCompatibility.opal?).to be true
|
|
111
152
|
end
|
|
112
153
|
|
|
113
|
-
it "AdapterResolver auto-detects
|
|
154
|
+
it "AdapterResolver auto-detects Oga as the default" do
|
|
114
155
|
adapter = Lutaml::Model::AdapterResolver.detect_xml_adapter
|
|
115
|
-
expect(adapter).to eq(:
|
|
156
|
+
expect(adapter).to eq(:oga)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "exposes both Oga and REXML as available adapters under Opal" do
|
|
160
|
+
# Both are pure Ruby; Nokogiri and Ox are C extensions and excluded.
|
|
161
|
+
options = Lutaml::Model::FormatRegistry.adapter_options_for(:xml)
|
|
162
|
+
expect(options).not_to be_nil
|
|
163
|
+
|
|
164
|
+
available = options[:available]
|
|
165
|
+
expect(available).to include(:oga, :rexml)
|
|
166
|
+
expect(available).not_to include(:nokogiri, :ox)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Opal's Module#prepend raises "Prepending a module multiple times
|
|
170
|
+
# is not supported" on the second call; runtime_compatibility.rb
|
|
171
|
+
# patches it to match MRI's idempotent behavior. Top-level lib files
|
|
172
|
+
# (lib/lutaml/model.rb, lib/lutaml/xml.rb) rely on this when Opal's
|
|
173
|
+
# eager loader re-evaluates them.
|
|
174
|
+
it "Module#prepend is idempotent under Opal" do
|
|
175
|
+
mod = Module.new
|
|
176
|
+
klass = Class.new
|
|
177
|
+
klass.prepend(mod)
|
|
178
|
+
|
|
179
|
+
expect { klass.prepend(mod) }.not_to raise_error
|
|
180
|
+
expect(klass.ancestors.count(mod)).to eq(1)
|
|
116
181
|
end
|
|
117
182
|
end
|
|
@@ -145,6 +145,36 @@ RSpec.describe "OrderedContent" do
|
|
|
145
145
|
expect(serialized).to be_xml_equivalent_to(expected_xml)
|
|
146
146
|
end
|
|
147
147
|
end
|
|
148
|
+
|
|
149
|
+
# Regression for issue #735: `ordered` builder must honour setter
|
|
150
|
+
# call order, not declaration order. The @__order_tracking__ flag
|
|
151
|
+
# was gated on mixed_content? instead of ordered?.
|
|
152
|
+
context "ordered builder honours setter call order" do
|
|
153
|
+
it "emits elements in builder-call order, not declaration order" do
|
|
154
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
155
|
+
attribute :a, :string
|
|
156
|
+
attribute :b, :string
|
|
157
|
+
|
|
158
|
+
xml do
|
|
159
|
+
element "item"
|
|
160
|
+
ordered
|
|
161
|
+
map_element "a", to: :a
|
|
162
|
+
map_element "b", to: :b
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
item = klass.new do |i|
|
|
167
|
+
i.b "first"
|
|
168
|
+
i.a "second"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
xml = item.to_xml
|
|
172
|
+
b_pos = xml.index("<b>first</b>")
|
|
173
|
+
a_pos = xml.index("<a>second</a>")
|
|
174
|
+
expect(b_pos).to be < a_pos
|
|
175
|
+
expect(item.element_order.map(&:name)).to eq(%w[b a])
|
|
176
|
+
end
|
|
177
|
+
end
|
|
148
178
|
end
|
|
149
179
|
|
|
150
180
|
describe Lutaml::Xml::Adapter::NokogiriAdapter do
|
|
@@ -87,6 +87,50 @@ module RawElementSpec
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# mixed_content parent with a collection raw: :element mapping.
|
|
91
|
+
# Round-trip dispatches through OrderedApplier#apply_element_rule_single
|
|
92
|
+
# (see issue #727 — previously double-wrapped each fragment).
|
|
93
|
+
class MixedContentRawCollectionContainer < Lutaml::Model::Serializable
|
|
94
|
+
attribute :name, :string
|
|
95
|
+
attribute :fragments, :string, collection: true
|
|
96
|
+
|
|
97
|
+
xml do
|
|
98
|
+
element "container"
|
|
99
|
+
mixed_content
|
|
100
|
+
map_element "name", to: :name
|
|
101
|
+
map_element "fragment", to: :fragments, raw: :element
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# mixed_content parent with a single (non-collection) raw: :element mapping.
|
|
106
|
+
# Dispatches through OrderedApplier -> :apply_rule -> RuleApplier#apply_rule,
|
|
107
|
+
# which already had the short-circuit. Kept as a regression guard.
|
|
108
|
+
class MixedContentRawSingleContainer < Lutaml::Model::Serializable
|
|
109
|
+
attribute :name, :string
|
|
110
|
+
attribute :embedded, :string
|
|
111
|
+
|
|
112
|
+
xml do
|
|
113
|
+
element "container"
|
|
114
|
+
mixed_content
|
|
115
|
+
map_element "name", to: :name
|
|
116
|
+
map_element "foreign", to: :embedded, raw: :element
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# ordered (no mixed_content) parent with a collection raw: :element mapping.
|
|
121
|
+
# Same dispatch path as mixed_content; same bug class.
|
|
122
|
+
class OrderedRawCollectionContainer < Lutaml::Model::Serializable
|
|
123
|
+
attribute :name, :string
|
|
124
|
+
attribute :fragments, :string, collection: true
|
|
125
|
+
|
|
126
|
+
xml do
|
|
127
|
+
element "container"
|
|
128
|
+
ordered
|
|
129
|
+
map_element "name", to: :name
|
|
130
|
+
map_element "fragment", to: :fragments, raw: :element
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
90
134
|
RSpec.describe "map_element raw: :element" do
|
|
91
135
|
describe "deserialization" do
|
|
92
136
|
describe "foreign namespace element capture" do
|
|
@@ -378,6 +422,87 @@ module RawElementSpec
|
|
|
378
422
|
end
|
|
379
423
|
end
|
|
380
424
|
|
|
425
|
+
# Regression for issue #727: OrderedApplier#apply_element_rule_single
|
|
426
|
+
# was missing the raw: :element short-circuit, so each captured fragment
|
|
427
|
+
# string (which already contains its own <fragment> wrapper) got wrapped
|
|
428
|
+
# again, producing <fragment><fragment>...</fragment></fragment>.
|
|
429
|
+
describe "round-trip with mixed_content + raw: :element collection" do
|
|
430
|
+
let(:mixed_collection_xml) do
|
|
431
|
+
<<~XML
|
|
432
|
+
<container>
|
|
433
|
+
before
|
|
434
|
+
<name>multi</name>
|
|
435
|
+
<fragment id="1">first</fragment>
|
|
436
|
+
<fragment id="2"><nested>second</nested></fragment>
|
|
437
|
+
after
|
|
438
|
+
</container>
|
|
439
|
+
XML
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it "does not double-wrap fragments on round-trip" do
|
|
443
|
+
doc = MixedContentRawCollectionContainer.from_xml(mixed_collection_xml)
|
|
444
|
+
output = doc.to_xml
|
|
445
|
+
|
|
446
|
+
expect(output).not_to include("<fragment><fragment")
|
|
447
|
+
expect(output).to include('id="1"')
|
|
448
|
+
expect(output).to include("first")
|
|
449
|
+
expect(output).to include('id="2"')
|
|
450
|
+
expect(output).to include("<nested>second</nested>")
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
it "is stable across repeated round-trips" do
|
|
454
|
+
doc = MixedContentRawCollectionContainer.from_xml(mixed_collection_xml)
|
|
455
|
+
once = MixedContentRawCollectionContainer.from_xml(doc.to_xml)
|
|
456
|
+
twice = MixedContentRawCollectionContainer.from_xml(once.to_xml)
|
|
457
|
+
|
|
458
|
+
expect(twice.fragments.length).to eq(2)
|
|
459
|
+
expect(twice.fragments[0]).to include("first")
|
|
460
|
+
expect(twice.fragments[1]).to include("<nested>second</nested>")
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Regression guard: the non-collection path under mixed_content dispatches
|
|
465
|
+
# through :apply_rule -> RuleApplier#apply_rule, which already had the
|
|
466
|
+
# short-circuit. Lock in the working behavior.
|
|
467
|
+
describe "round-trip with mixed_content + raw: :element single" do
|
|
468
|
+
let(:mixed_single_xml) do
|
|
469
|
+
'<container>before<name>x</name><foreign attr="v"><child>c</child></foreign>after</container>'
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it "emits the foreign element verbatim without wrapping" do
|
|
473
|
+
doc = MixedContentRawSingleContainer.from_xml(mixed_single_xml)
|
|
474
|
+
output = doc.to_xml
|
|
475
|
+
|
|
476
|
+
expect(output).not_to include("<foreign><foreign")
|
|
477
|
+
expect(output).to include('attr="v"')
|
|
478
|
+
expect(output).to include("<child>c</child>")
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Same dispatch path (OrderedApplier) as mixed_content; same bug class.
|
|
483
|
+
describe "round-trip with ordered + raw: :element collection" do
|
|
484
|
+
let(:ordered_collection_xml) do
|
|
485
|
+
<<~XML
|
|
486
|
+
<container>
|
|
487
|
+
<name>multi</name>
|
|
488
|
+
<fragment id="1">first</fragment>
|
|
489
|
+
<fragment id="2">second</fragment>
|
|
490
|
+
</container>
|
|
491
|
+
XML
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
it "does not double-wrap fragments on round-trip" do
|
|
495
|
+
doc = OrderedRawCollectionContainer.from_xml(ordered_collection_xml)
|
|
496
|
+
output = doc.to_xml
|
|
497
|
+
|
|
498
|
+
expect(output).not_to include("<fragment><fragment")
|
|
499
|
+
expect(output).to include('id="1"')
|
|
500
|
+
expect(output).to include("first")
|
|
501
|
+
expect(output).to include('id="2"')
|
|
502
|
+
expect(output).to include("second")
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
381
506
|
describe "nil and empty values" do
|
|
382
507
|
it "omits raw element when value is nil" do
|
|
383
508
|
model = SvgContainer.new(svg_data: nil)
|
|
@@ -9,19 +9,10 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
|
|
|
9
9
|
let(:rule) { instance_double(Lutaml::Model::Jsonl::MappingRule) }
|
|
10
10
|
let(:doc) { { "name" => "Test", "value" => 123 } }
|
|
11
11
|
let(:format) { :json }
|
|
12
|
-
let(:attr) {
|
|
13
|
-
let(:register) {
|
|
12
|
+
let(:attr) { Lutaml::Model::Attribute.new("name", :string) }
|
|
13
|
+
let(:register) { Lutaml::Model::Config.default_register }
|
|
14
14
|
let(:options) { {} }
|
|
15
15
|
|
|
16
|
-
def mock_resolver(default_set_value, default_value_data = nil)
|
|
17
|
-
resolver_double = instance_double(Lutaml::Model::Services::DefaultValueResolver,
|
|
18
|
-
default_set?: default_set_value,
|
|
19
|
-
default_value: default_value_data)
|
|
20
|
-
allow(Lutaml::Model::Services::DefaultValueResolver).to receive(:new)
|
|
21
|
-
.with(attr, register, instance)
|
|
22
|
-
.and_return(resolver_double)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
16
|
describe "#call" do
|
|
26
17
|
context "when rule has single mapping" do
|
|
27
18
|
before do
|
|
@@ -82,7 +73,7 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
|
|
|
82
73
|
end
|
|
83
74
|
end
|
|
84
75
|
|
|
85
|
-
context "when value is not found" do
|
|
76
|
+
context "when value is not found and attribute has no default" do
|
|
86
77
|
before do
|
|
87
78
|
allow(rule).to receive_messages(
|
|
88
79
|
multiple_mappings?: false,
|
|
@@ -90,7 +81,6 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
|
|
|
90
81
|
root_mapping?: false,
|
|
91
82
|
raw_mapping?: false,
|
|
92
83
|
)
|
|
93
|
-
mock_resolver(false)
|
|
94
84
|
end
|
|
95
85
|
|
|
96
86
|
it "returns uninitialized value" do
|
|
@@ -99,6 +89,10 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
|
|
|
99
89
|
end
|
|
100
90
|
|
|
101
91
|
context "when attribute has default value" do
|
|
92
|
+
let(:attr) do
|
|
93
|
+
Lutaml::Model::Attribute.new("name", :string, default: "default_value")
|
|
94
|
+
end
|
|
95
|
+
|
|
102
96
|
before do
|
|
103
97
|
allow(rule).to receive_messages(
|
|
104
98
|
multiple_mappings?: false,
|
|
@@ -106,7 +100,6 @@ RSpec.describe Lutaml::Model::RuleValueExtractor do
|
|
|
106
100
|
root_mapping?: false,
|
|
107
101
|
raw_mapping?: false,
|
|
108
102
|
)
|
|
109
|
-
mock_resolver(true, "default_value")
|
|
110
103
|
end
|
|
111
104
|
|
|
112
105
|
it "returns default value" do
|
|
@@ -33,4 +33,29 @@ RSpec.describe Lutaml::Model::RuntimeCompatibility do
|
|
|
33
33
|
.to eq(error_class)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
# lib/lutaml/model.rb and lib/lutaml/xml.rb re-evaluate their top-level
|
|
38
|
+
# prepend calls when Opal's eager loader processes them more than once.
|
|
39
|
+
# Opal's Module#prepend raises "Prepending a module multiple times is
|
|
40
|
+
# not supported" in that case; runtime_compatibility.rb aligns Opal
|
|
41
|
+
# with MRI's idempotent behavior. MRI has been idempotent since 2.0,
|
|
42
|
+
# so the spec applies to both runtimes.
|
|
43
|
+
describe "Module#prepend idempotency" do
|
|
44
|
+
it "is a no-op when the module is already in the ancestor chain" do
|
|
45
|
+
mod = Module.new
|
|
46
|
+
klass = Class.new
|
|
47
|
+
klass.prepend(mod)
|
|
48
|
+
|
|
49
|
+
expect { klass.prepend(mod) }.not_to raise_error
|
|
50
|
+
expect(klass.ancestors.count(mod)).to eq(1)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "still prepends when the module is not yet present" do
|
|
54
|
+
mod = Module.new
|
|
55
|
+
klass = Class.new
|
|
56
|
+
|
|
57
|
+
expect { klass.prepend(mod) }.not_to raise_error
|
|
58
|
+
expect(klass.ancestors).to include(mod)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
36
61
|
end
|