lutaml-model 0.8.16 → 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/.github/workflows/release.yml +0 -3
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +76 -12
- 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/namespace-semantics.adoc +2 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_guides/xml-namespace-qualification.adoc +142 -0
- data/docs/_pages/validation.adoc +65 -0
- data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
- data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
- data/docs/namespace-management.adoc +2 -0
- data/docs/xml-schema-qualification.md +6 -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/jsonld.rb +1 -4
- 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/choice.rb +34 -0
- data/lib/lutaml/model/compiled_rule.rb +7 -0
- 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 +19 -0
- data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
- data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
- data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
- data/lib/lutaml/rdf.rb +3 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_element.rb +2 -2
- 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 +59 -31
- 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/lib/lutaml/yamlld/adapter.rb +25 -0
- data/lib/lutaml/yamlld.rb +25 -0
- 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/integration/multi_format_spec.rb +23 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +177 -13
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/custom_model_spec.rb +4 -4
- data/spec/lutaml/model/mixed_content_spec.rb +173 -2
- data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +33 -3
- 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/uninitialized_class_spec.rb +1 -1
- 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/model/xsd_form_default_patterns_spec.rb +2 -2
- data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
- data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
- data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +15 -1
- data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
- data/spec/lutaml/xml/mapping_spec.rb +4 -4
- data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
- data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
- 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/serializable_namespace_spec.rb +6 -6
- data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
- data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
- 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/lutaml/yamlld/adapter_spec.rb +56 -0
- data/spec/lutaml/yamlld/registration_spec.rb +33 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +108 -37
- 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,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Model::Choice, "#restrict and #remove_attribute" do
|
|
6
|
+
before do
|
|
7
|
+
stub_const("ChoiceRestrictParent", Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :age, :integer
|
|
10
|
+
attribute :email, :string
|
|
11
|
+
end)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#restrict" do
|
|
15
|
+
it "restricts a predefined attribute inside a choice block" do
|
|
16
|
+
stub_const("ChoiceRestrictModel", Class.new(Lutaml::Model::Serializable) do
|
|
17
|
+
attribute :foo, :string, collection: 1..10
|
|
18
|
+
|
|
19
|
+
choice min: 1, max: 1 do
|
|
20
|
+
restrict :foo, collection: 2..5
|
|
21
|
+
end
|
|
22
|
+
end)
|
|
23
|
+
|
|
24
|
+
attr = ChoiceRestrictModel.attributes[:foo]
|
|
25
|
+
expect(attr.options[:collection]).to eq(2..5)
|
|
26
|
+
choice = ChoiceRestrictModel.choice_attributes.first
|
|
27
|
+
expect(choice.attributes).to include(attr)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "raises UndefinedAttributeError for non-existent attribute" do
|
|
31
|
+
expect do
|
|
32
|
+
stub_const("ChoiceRestrictBadModel", Class.new(Lutaml::Model::Serializable) do
|
|
33
|
+
choice min: 1, max: 1 do
|
|
34
|
+
restrict :nonexistent, collection: 1..2
|
|
35
|
+
end
|
|
36
|
+
end)
|
|
37
|
+
end.to raise_error(Lutaml::Model::UndefinedAttributeError)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "restricts an imported model attribute inside a choice block" do
|
|
41
|
+
stub_const("ChoiceImportModel", Class.new(Lutaml::Model::Serializable) do
|
|
42
|
+
attribute :name, :string
|
|
43
|
+
attribute :age, :integer
|
|
44
|
+
|
|
45
|
+
choice min: 1, max: 1 do
|
|
46
|
+
import_model_attributes ChoiceRestrictParent
|
|
47
|
+
restrict :age, values: [18, 21, 30]
|
|
48
|
+
end
|
|
49
|
+
end)
|
|
50
|
+
|
|
51
|
+
attr = ChoiceImportModel.attributes[:age]
|
|
52
|
+
expect(attr.options[:values]).to eq([18, 21, 30])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "marks the attribute as belonging to the choice" do
|
|
56
|
+
stub_const("ChoiceRestrictOwnedModel", Class.new(Lutaml::Model::Serializable) do
|
|
57
|
+
attribute :tag, :string
|
|
58
|
+
|
|
59
|
+
choice min: 0, max: 1 do
|
|
60
|
+
restrict :tag, values: %w[a b c]
|
|
61
|
+
end
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
attr = ChoiceRestrictOwnedModel.attributes[:tag]
|
|
65
|
+
expect(attr.options[:choice]).to be_a(described_class)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "does not duplicate the attribute if restrict is called twice" do
|
|
69
|
+
stub_const("ChoiceRestrictDupModel", Class.new(Lutaml::Model::Serializable) do
|
|
70
|
+
attribute :val, :integer
|
|
71
|
+
|
|
72
|
+
choice min: 0, max: 1 do
|
|
73
|
+
restrict :val, values: [1, 2]
|
|
74
|
+
restrict :val, values: [1, 2, 3]
|
|
75
|
+
end
|
|
76
|
+
end)
|
|
77
|
+
|
|
78
|
+
choice = ChoiceRestrictDupModel.choice_attributes.first
|
|
79
|
+
count = choice.attributes.count do |a|
|
|
80
|
+
!a.is_a?(described_class) && a.name == :val
|
|
81
|
+
end
|
|
82
|
+
expect(count).to eq(1)
|
|
83
|
+
expect(ChoiceRestrictDupModel.attributes[:val].options[:values]).to eq([
|
|
84
|
+
1, 2, 3
|
|
85
|
+
])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "restricts an inherited attribute in a child class choice" do
|
|
89
|
+
stub_const("ChoiceRestrictParent", Class.new(Lutaml::Model::Serializable) do
|
|
90
|
+
attribute :tag, :string
|
|
91
|
+
attribute :label, :string
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
child = Class.new(ChoiceRestrictParent) do
|
|
95
|
+
include Lutaml::Model::Serialize
|
|
96
|
+
|
|
97
|
+
choice min: 1, max: 1 do
|
|
98
|
+
restrict :tag, values: %w[a b c]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
attr = child.attributes[:tag]
|
|
103
|
+
expect(attr.options[:values]).to eq(%w[a b c])
|
|
104
|
+
choice = child.choice_attributes.first
|
|
105
|
+
expect(choice.flat_attributes.any? { |a| a.name == :tag }).to be(true)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "#remove_attribute" do
|
|
110
|
+
it "removes an attribute from the choice block" do
|
|
111
|
+
stub_const("ChoiceRemoveModel", Class.new(Lutaml::Model::Serializable) do
|
|
112
|
+
attribute :keep, :string
|
|
113
|
+
attribute :drop, :string
|
|
114
|
+
|
|
115
|
+
choice min: 0, max: 1 do
|
|
116
|
+
attribute :keep, :string
|
|
117
|
+
attribute :drop, :string
|
|
118
|
+
remove_attribute :drop
|
|
119
|
+
end
|
|
120
|
+
end)
|
|
121
|
+
|
|
122
|
+
choice = ChoiceRemoveModel.choice_attributes.first
|
|
123
|
+
attr_names = choice.attributes.filter_map do |a|
|
|
124
|
+
a.is_a?(described_class) ? nil : a.name
|
|
125
|
+
end
|
|
126
|
+
expect(attr_names).to eq([:keep])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "clears the choice option from the removed attribute" do
|
|
130
|
+
stub_const("ChoiceRemoveOptModel", Class.new(Lutaml::Model::Serializable) do
|
|
131
|
+
attribute :item, :string
|
|
132
|
+
|
|
133
|
+
choice min: 0, max: 1 do
|
|
134
|
+
attribute :item, :string
|
|
135
|
+
remove_attribute :item
|
|
136
|
+
end
|
|
137
|
+
end)
|
|
138
|
+
|
|
139
|
+
attr = ChoiceRemoveOptModel.attributes[:item]
|
|
140
|
+
# The top-level :item attribute should still exist without :choice
|
|
141
|
+
# since the one in the choice was a different instance
|
|
142
|
+
expect(attr.options[:choice]).to be_nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "returns nil if the attribute is not in the choice" do
|
|
146
|
+
result = nil
|
|
147
|
+
stub_const("ChoiceRemoveNotFoundModel", Class.new(Lutaml::Model::Serializable) do
|
|
148
|
+
attribute :x, :string
|
|
149
|
+
|
|
150
|
+
choice min: 0, max: 1 do
|
|
151
|
+
result = remove_attribute(:nonexistent)
|
|
152
|
+
end
|
|
153
|
+
end)
|
|
154
|
+
|
|
155
|
+
expect(result).to be_nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "invalidates the flat_attributes cache" do
|
|
159
|
+
stub_const("ChoiceRemoveCacheModel", Class.new(Lutaml::Model::Serializable) do
|
|
160
|
+
attribute :a, :string
|
|
161
|
+
attribute :b, :string
|
|
162
|
+
|
|
163
|
+
choice min: 0, max: 1 do
|
|
164
|
+
attribute :a, :string
|
|
165
|
+
attribute :b, :string
|
|
166
|
+
end
|
|
167
|
+
end)
|
|
168
|
+
|
|
169
|
+
choice = ChoiceRemoveCacheModel.choice_attributes.first
|
|
170
|
+
expect(choice.flat_attributes.map(&:name)).to contain_exactly(:a, :b)
|
|
171
|
+
choice.remove_attribute(:b)
|
|
172
|
+
expect(choice.flat_attributes.map(&:name)).to contain_exactly(:a)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "removes an imported attribute from the choice block" do
|
|
176
|
+
source = Class.new(Lutaml::Model::Serializable) do
|
|
177
|
+
attribute :name, :string
|
|
178
|
+
attribute :email, :string
|
|
179
|
+
attribute :phone, :string
|
|
180
|
+
end
|
|
181
|
+
stub_const("ChoiceRemoveImportSource", source)
|
|
182
|
+
|
|
183
|
+
stub_const("ChoiceRemoveImportModel", Class.new(Lutaml::Model::Serializable) do
|
|
184
|
+
choice min: 1, max: 1 do
|
|
185
|
+
import_model_attributes ChoiceRemoveImportSource
|
|
186
|
+
remove_attribute :phone
|
|
187
|
+
end
|
|
188
|
+
end)
|
|
189
|
+
|
|
190
|
+
choice = ChoiceRemoveImportModel.choice_attributes.first
|
|
191
|
+
names = choice.flat_attributes.map(&:name)
|
|
192
|
+
expect(names).to contain_exactly(:name, :email)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "removes a restrict-added attribute from the choice" do
|
|
196
|
+
stub_const("ChoiceRemoveRestrictModel", Class.new(Lutaml::Model::Serializable) do
|
|
197
|
+
attribute :tag, :string
|
|
198
|
+
|
|
199
|
+
choice min: 0, max: 1 do
|
|
200
|
+
restrict :tag, values: %w[a b c]
|
|
201
|
+
remove_attribute :tag
|
|
202
|
+
end
|
|
203
|
+
end)
|
|
204
|
+
|
|
205
|
+
choice = ChoiceRemoveRestrictModel.choice_attributes.first
|
|
206
|
+
expect(choice.flat_attributes).to be_empty
|
|
207
|
+
expect(ChoiceRemoveRestrictModel.attributes[:tag].options[:choice]).to be_nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "returns the removed attribute" do
|
|
211
|
+
stub_const("ChoiceRemoveReturnModel", Class.new(Lutaml::Model::Serializable) do
|
|
212
|
+
attribute :item, :string
|
|
213
|
+
|
|
214
|
+
choice min: 0, max: 1 do
|
|
215
|
+
attribute :item, :string
|
|
216
|
+
end
|
|
217
|
+
end)
|
|
218
|
+
|
|
219
|
+
choice = ChoiceRemoveReturnModel.choice_attributes.first
|
|
220
|
+
removed = choice.remove_attribute(:item)
|
|
221
|
+
expect(removed).to be_a(Lutaml::Model::Attribute)
|
|
222
|
+
expect(removed.name).to eq(:item)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
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"])
|
|
@@ -153,16 +153,16 @@ module CustomModelSpecs
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
class MixedWithNestedContent < Lutaml::Model::Serializable
|
|
156
|
-
attribute :street, :string
|
|
157
|
-
attribute :city, :string
|
|
156
|
+
attribute :street, :string
|
|
157
|
+
attribute :city, :string
|
|
158
158
|
attribute :bibdata, Bibdata
|
|
159
159
|
|
|
160
160
|
xml do
|
|
161
161
|
element "MixedWithNestedContent"
|
|
162
162
|
mixed_content
|
|
163
163
|
|
|
164
|
-
map_element "street", to: :street
|
|
165
|
-
map_element "city", to: :city
|
|
164
|
+
map_element "street", to: :street, raw: :content
|
|
165
|
+
map_element "city", to: :city, raw: :content
|
|
166
166
|
map_element "bibdata",
|
|
167
167
|
to: :bibdata,
|
|
168
168
|
with: { from: :bibdata_from_xml, to: :bibdata_to_xml }
|
|
@@ -107,12 +107,12 @@ module MixedContentSpec
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
class SpecialCharContentWithRawAndMixedOption < Lutaml::Model::Serializable
|
|
110
|
-
attribute :special, :string
|
|
110
|
+
attribute :special, :string
|
|
111
111
|
|
|
112
112
|
xml do
|
|
113
113
|
element "SpecialCharContentWithRawOptionAndMixedOption"
|
|
114
114
|
mixed_content
|
|
115
|
-
map_element :special, to: :special
|
|
115
|
+
map_element :special, to: :special, raw: :content
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
@@ -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
|
|
@@ -240,12 +240,12 @@ RSpec.describe MultipleMapping do
|
|
|
240
240
|
expect(product1.name).to eq("Coffee Maker")
|
|
241
241
|
expect(product1.description).to eq("Premium coffee maker")
|
|
242
242
|
expect(product1.status).to eq("active")
|
|
243
|
-
expect(product1.content.to_s).to
|
|
243
|
+
expect(product1.content.to_s).to include("Some content here")
|
|
244
244
|
|
|
245
245
|
expect(product2.name).to eq("Coffee Maker")
|
|
246
246
|
expect(product2.description).to eq("Premium coffee maker")
|
|
247
247
|
expect(product2.status).to eq("in-stock")
|
|
248
|
-
expect(product2.content.to_s).to
|
|
248
|
+
expect(product2.content.to_s).to include("Different content")
|
|
249
249
|
|
|
250
250
|
# Test round-trip: serialize and deserialize should preserve data
|
|
251
251
|
# (Note: exact XML format differs between adapters due to mixed content whitespace handling)
|
|
@@ -253,13 +253,13 @@ RSpec.describe MultipleMapping do
|
|
|
253
253
|
expect(round_trip1.name).to eq(product1.name)
|
|
254
254
|
expect(round_trip1.description).to eq(product1.description)
|
|
255
255
|
expect(round_trip1.status).to eq(product1.status)
|
|
256
|
-
expect(round_trip1.content.to_s).to
|
|
256
|
+
expect(round_trip1.content.to_s).to include("Some content here")
|
|
257
257
|
|
|
258
258
|
round_trip2 = MultipleMapping::Product.from_xml(product2.to_xml)
|
|
259
259
|
expect(round_trip2.name).to eq(product2.name)
|
|
260
260
|
expect(round_trip2.description).to eq(product2.description)
|
|
261
261
|
expect(round_trip2.status).to eq(product2.status)
|
|
262
|
-
expect(round_trip2.content.to_s).to
|
|
262
|
+
expect(round_trip2.content.to_s).to include("Different content")
|
|
263
263
|
end
|
|
264
264
|
end
|
|
265
265
|
|
|
@@ -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
|
|
@@ -102,8 +102,8 @@ RSpec.describe "OrderedContent" do
|
|
|
102
102
|
expect(obj.bold).to eq(["bell", "cool"])
|
|
103
103
|
expect(obj.italic).to eq(["384,400 km"])
|
|
104
104
|
expect(obj.underline).to eq("craters")
|
|
105
|
-
expect(obj.content.first.to_s).to
|
|
106
|
-
expect(obj.content.join).to
|
|
105
|
+
expect(obj.content.first.to_s).to include("The Earth's Moon rings like a")
|
|
106
|
+
expect(obj.content.join).to include("Ain't that")
|
|
107
107
|
|
|
108
108
|
# Verify round-trip preserves data
|
|
109
109
|
# (Note: exact XML format differs between adapters in ordered mode)
|
|
@@ -112,7 +112,7 @@ RSpec.describe "OrderedContent" do
|
|
|
112
112
|
expect(round_trip.bold).to eq(obj.bold)
|
|
113
113
|
expect(round_trip.italic).to eq(obj.italic)
|
|
114
114
|
expect(round_trip.underline).to eq(obj.underline)
|
|
115
|
-
expect(round_trip.content.first.to_s).to
|
|
115
|
+
expect(round_trip.content.first.to_s).to include("The Earth's Moon rings like a")
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
@@ -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
|