lutaml-model 0.8.17 → 0.8.19
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/builder-dsl.adoc +127 -0
- 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 +23 -11
- data/lib/lutaml/model/serialize/builder.rb +70 -2
- data/lib/lutaml/model/serialize/initialization.rb +3 -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 +104 -4
- 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 +41 -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/serialize/builder_spec.rb +358 -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 +99 -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,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/schema/definitions"
|
|
5
|
+
require "lutaml/model/schema/renderers/namespace"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Lutaml::Model::Schema::Renderers::Namespace do
|
|
8
|
+
let(:defs) { Lutaml::Model::Schema::Definitions }
|
|
9
|
+
|
|
10
|
+
it "renders a namespace class with uri and prefix_default" do
|
|
11
|
+
spec = defs::Namespace.new(
|
|
12
|
+
class_name: "ExampleNs",
|
|
13
|
+
uri: "http://example.com/ns",
|
|
14
|
+
prefix_default: "ex",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
output = described_class.render(spec)
|
|
18
|
+
|
|
19
|
+
expect(output).to include("class ExampleNs < Lutaml::Xml::W3c::XmlNamespace")
|
|
20
|
+
expect(output).to include(%(uri "http://example.com/ns"))
|
|
21
|
+
expect(output).to include(%(prefix_default "ex"))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "wraps in module when module_namespace is supplied" do
|
|
25
|
+
spec = defs::Namespace.new(
|
|
26
|
+
class_name: "Inner",
|
|
27
|
+
uri: "http://example.com/inner",
|
|
28
|
+
prefix_default: nil,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
output = described_class.render(spec, module_namespace: "Mod::Sub")
|
|
32
|
+
|
|
33
|
+
expect(output).to include("module Mod")
|
|
34
|
+
expect(output).to include("module Sub")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/schema/definitions"
|
|
5
|
+
require "lutaml/model/schema/renderers/restricted_type"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Lutaml::Model::Schema::Renderers::RestrictedType do
|
|
8
|
+
let(:defs) { Lutaml::Model::Schema::Definitions }
|
|
9
|
+
|
|
10
|
+
it "renders min/max facets in the cast body" do
|
|
11
|
+
spec = defs::RestrictedType.new(
|
|
12
|
+
class_name: "PositiveInt",
|
|
13
|
+
parent_class: "Lutaml::Model::Type::Integer",
|
|
14
|
+
facets: defs::Facet.new(min_inclusive: 1, max_inclusive: 100),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
output = described_class.render(spec)
|
|
18
|
+
|
|
19
|
+
expect(output).to include("class PositiveInt < Lutaml::Model::Type::Integer")
|
|
20
|
+
expect(output).to include("def self.cast(value, options = {})")
|
|
21
|
+
expect(output).to include("options[:min] = 1")
|
|
22
|
+
expect(output).to include("options[:max] = 100")
|
|
23
|
+
expect(output).to include("value = super(value, options)")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "renders enumerations facet" do
|
|
27
|
+
spec = defs::RestrictedType.new(
|
|
28
|
+
class_name: "Color",
|
|
29
|
+
parent_class: "Lutaml::Model::Type::String",
|
|
30
|
+
facets: defs::Facet.new(enumerations: %w[red green blue]),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
output = described_class.render(spec)
|
|
34
|
+
|
|
35
|
+
expect(output).to include('options[:values] = [super("red"), super("green"), super("blue")]')
|
|
36
|
+
expect(output).not_to include("options[:min]")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "renders pattern facet as a regexp literal" do
|
|
40
|
+
spec = defs::RestrictedType.new(
|
|
41
|
+
class_name: "Slug",
|
|
42
|
+
parent_class: "Lutaml::Model::Type::String",
|
|
43
|
+
facets: defs::Facet.new(pattern: "[a-z0-9-]+"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
output = described_class.render(spec)
|
|
47
|
+
|
|
48
|
+
expect(output).to include("options[:pattern] = %r{[a-z0-9-]+}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "renders transform facet" do
|
|
52
|
+
spec = defs::RestrictedType.new(
|
|
53
|
+
class_name: "Upper",
|
|
54
|
+
parent_class: "Lutaml::Model::Type::String",
|
|
55
|
+
facets: defs::Facet.new,
|
|
56
|
+
transform_facet: defs::TransformFacet.new(expression: "value.upcase"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
output = described_class.render(spec)
|
|
60
|
+
|
|
61
|
+
expect(output).to include("value = value.upcase")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "uses lazy register memoisation" do
|
|
65
|
+
spec = defs::RestrictedType.new(
|
|
66
|
+
class_name: "Tiny",
|
|
67
|
+
parent_class: "Lutaml::Model::Type::Integer",
|
|
68
|
+
facets: defs::Facet.new(max_inclusive: 5),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
output = described_class.render(spec)
|
|
72
|
+
|
|
73
|
+
expect(output).to include("@register ||= Lutaml::Model::Config.default_register")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/schema/definitions"
|
|
5
|
+
require "lutaml/model/schema/renderers/union"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Lutaml::Model::Schema::Renderers::Union do
|
|
8
|
+
let(:defs) { Lutaml::Model::Schema::Definitions }
|
|
9
|
+
|
|
10
|
+
it "renders the :class_refs strategy as an each/begin/rescue loop" do
|
|
11
|
+
spec = defs::UnionType.new(
|
|
12
|
+
class_name: "NumberOrString",
|
|
13
|
+
members: [
|
|
14
|
+
defs::TypeRef.new(kind: :class_ref, value: "Integer"),
|
|
15
|
+
defs::TypeRef.new(kind: :class_ref, value: "String"),
|
|
16
|
+
],
|
|
17
|
+
cast_strategy: :class_refs,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
output = described_class.render(spec)
|
|
21
|
+
|
|
22
|
+
expect(output).to include("class NumberOrString < Lutaml::Model::Type::Value")
|
|
23
|
+
expect(output).to include("[Integer, String].each do |t|")
|
|
24
|
+
expect(output).to include("casted = t.cast(value, options)")
|
|
25
|
+
expect(output).to include("return casted unless casted.nil?")
|
|
26
|
+
expect(output).to include("rescue StandardError")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "renders the :resolve_type strategy as a ||-chained resolve_type call" do
|
|
30
|
+
spec = defs::UnionType.new(
|
|
31
|
+
class_name: "IntegerOrToken",
|
|
32
|
+
members: [
|
|
33
|
+
defs::TypeRef.new(kind: :symbol, value: "integer"),
|
|
34
|
+
defs::TypeRef.new(kind: :symbol, value: "token"),
|
|
35
|
+
],
|
|
36
|
+
cast_strategy: :resolve_type,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
output = described_class.render(spec)
|
|
40
|
+
|
|
41
|
+
expect(output).to include("Lutaml::Model::GlobalContext.resolve_type(:integer, @register).cast(value, options)")
|
|
42
|
+
expect(output).to include("Lutaml::Model::GlobalContext.resolve_type(:token, @register).cast(value, options)")
|
|
43
|
+
expect(output).to include(" ||\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "emits required_files lines above the module wrap" do
|
|
47
|
+
spec = defs::UnionType.new(
|
|
48
|
+
class_name: "AnyOf",
|
|
49
|
+
members: [defs::TypeRef.new(kind: :class_ref, value: "Integer")],
|
|
50
|
+
cast_strategy: :class_refs,
|
|
51
|
+
required_files: ['require_relative "integer"'],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
output = described_class.render(spec)
|
|
55
|
+
|
|
56
|
+
expect(output).to include('require_relative "integer"')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "../../../../lib/lutaml/model"
|
|
5
|
+
require "lutaml/xml/adapter/nokogiri_adapter"
|
|
6
|
+
|
|
7
|
+
# Specs for Lutaml::Model::Serialize::Builder — the module that gives
|
|
8
|
+
# Serializable the `Klass.new do |x| ... end` block syntax.
|
|
9
|
+
#
|
|
10
|
+
# These specs exist because of a regression where direct setters
|
|
11
|
+
# (`x.attr = v`) inside the builder block silently dropped attributes
|
|
12
|
+
# from serialized output while appender calls (`x.attr(v)`) worked.
|
|
13
|
+
# The root cause was that only getter-with-arg paths called track_order;
|
|
14
|
+
# generated setters did not. The fix centralises mutation recording via
|
|
15
|
+
# Builder#record_mutation / record_mutation_collection, and the
|
|
16
|
+
# serializer's apply_remaining_rules acts as a safety net so element
|
|
17
|
+
# values are never silently dropped.
|
|
18
|
+
#
|
|
19
|
+
# These specs cover the invariant ("no silent data loss"), call-order
|
|
20
|
+
# preservation, round-trip parity, equivalence between .tap and block,
|
|
21
|
+
# the no-op guarantee for non-ordered models, and the wholesale
|
|
22
|
+
# collection assignment case.
|
|
23
|
+
RSpec.describe "Lutaml::Model::Serialize::Builder" do
|
|
24
|
+
let(:ordered_klass) do
|
|
25
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
26
|
+
attribute :singular, :string
|
|
27
|
+
attribute :items, :string, collection: true
|
|
28
|
+
|
|
29
|
+
xml do
|
|
30
|
+
element "container"
|
|
31
|
+
ordered
|
|
32
|
+
map_element "singular", to: :singular
|
|
33
|
+
map_element "items", to: :items
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
let(:mixed_klass) do
|
|
39
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
40
|
+
attribute :singular, :string
|
|
41
|
+
attribute :items, :string, collection: true
|
|
42
|
+
|
|
43
|
+
xml do
|
|
44
|
+
element "container"
|
|
45
|
+
mixed_content
|
|
46
|
+
map_element "singular", to: :singular
|
|
47
|
+
map_element "items", to: :items
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
let(:plain_klass) do
|
|
53
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
54
|
+
attribute :singular, :string
|
|
55
|
+
attribute :items, :string, collection: true
|
|
56
|
+
|
|
57
|
+
xml do
|
|
58
|
+
element "container"
|
|
59
|
+
map_element "singular", to: :singular
|
|
60
|
+
map_element "items", to: :items
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
shared_examples "no silent data loss across mutation styles" do
|
|
66
|
+
it "appender-only: emits every call in order" do
|
|
67
|
+
obj = ordered_klass.new do |x|
|
|
68
|
+
x.singular "first"
|
|
69
|
+
x.items "a"
|
|
70
|
+
x.items "b"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
xml = obj.to_xml
|
|
74
|
+
expect(xml).to be_xml_equivalent_to(<<~XML)
|
|
75
|
+
<container>
|
|
76
|
+
<singular>first</singular>
|
|
77
|
+
<items>a</items>
|
|
78
|
+
<items>b</items>
|
|
79
|
+
</container>
|
|
80
|
+
XML
|
|
81
|
+
expect(obj.element_order.map(&:name)).to eq(%w[singular items items])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "direct setter for singular: still emits the element" do
|
|
85
|
+
obj = ordered_klass.new do |x|
|
|
86
|
+
x.singular = "first"
|
|
87
|
+
x.items "a"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
xml = obj.to_xml
|
|
91
|
+
expect(xml).to be_xml_equivalent_to(<<~XML)
|
|
92
|
+
<container>
|
|
93
|
+
<singular>first</singular>
|
|
94
|
+
<items>a</items>
|
|
95
|
+
</container>
|
|
96
|
+
XML
|
|
97
|
+
expect(obj.element_order.map(&:name)).to eq(%w[singular items])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "wholesale collection assignment: emits one element per item" do
|
|
101
|
+
obj = ordered_klass.new do |x|
|
|
102
|
+
x.singular "first"
|
|
103
|
+
x.items = %w[a b c]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
xml = obj.to_xml
|
|
107
|
+
expect(xml).to be_xml_equivalent_to(<<~XML)
|
|
108
|
+
<container>
|
|
109
|
+
<singular>first</singular>
|
|
110
|
+
<items>a</items>
|
|
111
|
+
<items>b</items>
|
|
112
|
+
<items>c</items>
|
|
113
|
+
</container>
|
|
114
|
+
XML
|
|
115
|
+
expect(obj.element_order.map(&:name)).to eq(%w[singular items items items])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "all direct setters: emits in call order" do
|
|
119
|
+
obj = ordered_klass.new do |x|
|
|
120
|
+
x.singular = "first"
|
|
121
|
+
x.items = %w[a b]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
xml = obj.to_xml
|
|
125
|
+
expect(xml).to be_xml_equivalent_to(<<~XML)
|
|
126
|
+
<container>
|
|
127
|
+
<singular>first</singular>
|
|
128
|
+
<items>a</items>
|
|
129
|
+
<items>b</items>
|
|
130
|
+
</container>
|
|
131
|
+
XML
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "mixed style preserves call order across setters and appenders" do
|
|
135
|
+
obj = ordered_klass.new do |x|
|
|
136
|
+
x.items "a"
|
|
137
|
+
x.singular = "middle"
|
|
138
|
+
x.items "b"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
xml = obj.to_xml
|
|
142
|
+
expect(xml).to be_xml_equivalent_to(<<~XML)
|
|
143
|
+
<container>
|
|
144
|
+
<items>a</items>
|
|
145
|
+
<singular>middle</singular>
|
|
146
|
+
<items>b</items>
|
|
147
|
+
</container>
|
|
148
|
+
XML
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
describe "no silent data loss" do
|
|
153
|
+
it_behaves_like "no silent data loss across mutation styles"
|
|
154
|
+
|
|
155
|
+
it "mixed_content model emits setter-only attributes too" do
|
|
156
|
+
obj = mixed_klass.new do |x|
|
|
157
|
+
x.singular = "first"
|
|
158
|
+
x.items "a"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
xml = obj.to_xml
|
|
162
|
+
expect(xml).to include("<singular>first</singular>")
|
|
163
|
+
expect(xml).to include("<items>a</items>")
|
|
164
|
+
expect(obj.element_order.map(&:name)).to eq(%w[singular items])
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe "call order preservation" do
|
|
169
|
+
it "reverse declaration order is honoured via direct setters" do
|
|
170
|
+
obj = ordered_klass.new do |x|
|
|
171
|
+
x.items = %w[a b]
|
|
172
|
+
x.singular = "last"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
xml = obj.to_xml
|
|
176
|
+
items_pos = xml.index("<items>")
|
|
177
|
+
singular_pos = xml.index("<singular>")
|
|
178
|
+
expect(items_pos).to be < singular_pos
|
|
179
|
+
expect(obj.element_order.map(&:name)).to eq(%w[items items singular])
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "reverse declaration order is honoured via appenders" do
|
|
183
|
+
obj = ordered_klass.new do |x|
|
|
184
|
+
x.items "a"
|
|
185
|
+
x.singular "last"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
xml = obj.to_xml
|
|
189
|
+
items_pos = xml.index("<items>")
|
|
190
|
+
singular_pos = xml.index("<singular>")
|
|
191
|
+
expect(items_pos).to be < singular_pos
|
|
192
|
+
expect(obj.element_order.map(&:name)).to eq(%w[items singular])
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "round-trip parity" do
|
|
197
|
+
it "ordered: parses and re-emits equivalent XML" do
|
|
198
|
+
original = <<~XML
|
|
199
|
+
<container>
|
|
200
|
+
<items>a</items>
|
|
201
|
+
<singular>middle</singular>
|
|
202
|
+
<items>b</items>
|
|
203
|
+
</container>
|
|
204
|
+
XML
|
|
205
|
+
|
|
206
|
+
parsed = ordered_klass.from_xml(original)
|
|
207
|
+
expect(parsed.to_xml).to be_xml_equivalent_to(original)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "mixed_content: parses and re-emits equivalent XML" do
|
|
211
|
+
original = <<~XML
|
|
212
|
+
<container>before <items>a</items> mid <singular>x</singular> end</container>
|
|
213
|
+
XML
|
|
214
|
+
|
|
215
|
+
parsed = mixed_klass.from_xml(original)
|
|
216
|
+
roundtrip = parsed.to_xml
|
|
217
|
+
expect(roundtrip).to include("<items>a</items>")
|
|
218
|
+
expect(roundtrip).to include("<singular>x</singular>")
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe ".tap vs builder block equivalence" do
|
|
223
|
+
it "produces identical element_order presence and XML shape" do
|
|
224
|
+
via_tap = ordered_klass.new.tap do |x|
|
|
225
|
+
x.singular = "first"
|
|
226
|
+
x.items = %w[a b]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
via_block = ordered_klass.new do |x|
|
|
230
|
+
x.singular = "first"
|
|
231
|
+
x.items = %w[a b]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# .tap path does not enable tracking; both must still emit the same XML.
|
|
235
|
+
expect(via_tap.element_order).to be_nil
|
|
236
|
+
expect(via_block.element_order).not_to be_nil
|
|
237
|
+
expect(via_tap.to_xml).to be_xml_equivalent_to(via_block.to_xml)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe "setter return value contract" do
|
|
242
|
+
# Ruby's setter contract: `obj.foo = v` evaluates to `v`. Callers
|
|
243
|
+
# like `obj.foo || (obj.foo = [])` depend on this. Generated
|
|
244
|
+
# setters must continue to honour this contract regardless of
|
|
245
|
+
# whether order tracking is enabled.
|
|
246
|
+
it "singular setter returns the assigned value" do
|
|
247
|
+
obj = plain_klass.new
|
|
248
|
+
expect((obj.singular = "v")).to eq("v")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "collection setter returns the assigned value" do
|
|
252
|
+
obj = plain_klass.new
|
|
253
|
+
expect((obj.items = %w[a b])).to eq(%w[a b])
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "supports the `obj.foo || (obj.foo = [])` pattern from downstream consumers" do
|
|
257
|
+
obj = plain_klass.new
|
|
258
|
+
# Mirror Docbook::Elements::Para#try_add_inline, which relies on
|
|
259
|
+
# the setter returning the array (not nil) when the getter is nil.
|
|
260
|
+
# The bug: when the setter returned nil (because record_mutation
|
|
261
|
+
# returned nil with tracking disabled), `collection` was nil and
|
|
262
|
+
# `collection << element` failed with NoMethodError.
|
|
263
|
+
collection = obj.items || (obj.items = [])
|
|
264
|
+
expect(collection).to eq([])
|
|
265
|
+
expect { collection << "x" }.not_to raise_error
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
describe "no-op guarantee when not tracking" do
|
|
270
|
+
it "plain (non-ordered) models do not allocate element_order entries" do
|
|
271
|
+
obj = plain_klass.new do |x|
|
|
272
|
+
x.singular = "first"
|
|
273
|
+
x.items "a"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
expect(obj.element_order).to be_nil
|
|
277
|
+
expect { obj.to_xml }.not_to raise_error
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "ordered models constructed without a block do not track" do
|
|
281
|
+
obj = ordered_klass.new
|
|
282
|
+
obj.singular = "first"
|
|
283
|
+
obj.items = %w[a b]
|
|
284
|
+
|
|
285
|
+
expect(obj.element_order).to be_nil
|
|
286
|
+
expect(obj.to_xml).to include("<singular>first</singular>")
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
describe "wholesale collection reassignment" do
|
|
291
|
+
it "tracks one entry per item across multiple assignments" do
|
|
292
|
+
obj = ordered_klass.new do |x|
|
|
293
|
+
x.items = %w[a b]
|
|
294
|
+
x.items = %w[c d e]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# element_order is a log of mutations, not a reflection of final state,
|
|
298
|
+
# so both assignments are recorded. The serializer emits the current
|
|
299
|
+
# value of the attribute via element_order in recorded order; the
|
|
300
|
+
# safety net guarantees no data loss.
|
|
301
|
+
expect(obj.element_order.map(&:name)).to eq(%w[items items items items items])
|
|
302
|
+
xml = obj.to_xml
|
|
303
|
+
# The serialized output must contain all current items
|
|
304
|
+
expect(xml).to include("<items>c</items>")
|
|
305
|
+
expect(xml).to include("<items>d</items>")
|
|
306
|
+
expect(xml).to include("<items>e</items>")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
describe "no-silent-drop invariant (cross-cutting)" do
|
|
311
|
+
# The bug class: any attribute with a non-default value MUST appear in
|
|
312
|
+
# the serialized output, regardless of how it was set. This spec is the
|
|
313
|
+
# regression guard for the whole class of bug, not just one instance.
|
|
314
|
+
shared_examples "no silent drop" do |description, mutation_block|
|
|
315
|
+
it "does not silently drop any attribute set via: #{description}" do
|
|
316
|
+
obj = ordered_klass.new(&mutation_block)
|
|
317
|
+
xml = obj.to_xml
|
|
318
|
+
|
|
319
|
+
if !obj.singular.nil? && !obj.singular.to_s.empty?
|
|
320
|
+
expect(xml).to include("<singular>"), "singular was dropped from output"
|
|
321
|
+
end
|
|
322
|
+
return if obj.items.nil? || obj.items.empty?
|
|
323
|
+
|
|
324
|
+
obj.items.each do |item|
|
|
325
|
+
expect(xml).to include("<items>#{item}</items>"),
|
|
326
|
+
"items element for #{item.inspect} was dropped from output"
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it_behaves_like "no silent drop",
|
|
332
|
+
"singular setter + wholesale items setter",
|
|
333
|
+
lambda { |x|
|
|
334
|
+
x.singular = "v"
|
|
335
|
+
x.items = %w[a b]
|
|
336
|
+
}
|
|
337
|
+
it_behaves_like "no silent drop",
|
|
338
|
+
"all appenders",
|
|
339
|
+
lambda { |x|
|
|
340
|
+
x.singular "v"
|
|
341
|
+
x.items "a"
|
|
342
|
+
x.items "b"
|
|
343
|
+
}
|
|
344
|
+
it_behaves_like "no silent drop",
|
|
345
|
+
"singular setter + items appender (the original bug)",
|
|
346
|
+
lambda { |x|
|
|
347
|
+
x.singular = "v"
|
|
348
|
+
x.items "a"
|
|
349
|
+
x.items "b"
|
|
350
|
+
}
|
|
351
|
+
it_behaves_like "no silent drop",
|
|
352
|
+
"singular appender + wholesale items setter",
|
|
353
|
+
lambda { |x|
|
|
354
|
+
x.singular "v"
|
|
355
|
+
x.items = %w[a b]
|
|
356
|
+
}
|
|
357
|
+
end
|
|
358
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
# Regression: Transform#apply_value_map must handle the nil-attr case used
|
|
4
|
+
# by custom-method-only key-value rules. Pre-consolidation behavior:
|
|
5
|
+
# the default { omitted: :nil, nil: :nil, empty: :empty } map converted
|
|
6
|
+
# UninitializedClass.instance to nil so the downstream Utils.present? guard
|
|
7
|
+
# in KeyValue::Transform#process_mapping_rule would skip the custom method
|
|
8
|
+
# for absent input keys.
|
|
9
|
+
RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
|
|
10
|
+
let(:transform_subclass) do
|
|
11
|
+
Class.new(described_class) do
|
|
12
|
+
# Expose the protected delegator for direct testing.
|
|
13
|
+
public :apply_value_map
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:context) { Class.new(Lutaml::Model::Serializable) }
|
|
18
|
+
let(:register) { Lutaml::Model::Config.default_register }
|
|
19
|
+
let(:transform) { transform_subclass.new(context, register) }
|
|
20
|
+
|
|
21
|
+
let(:uninit) { Lutaml::Model::UninitializedClass.instance }
|
|
22
|
+
let(:default_vmap) { { omitted: :nil, nil: :nil, empty: :empty } }
|
|
23
|
+
|
|
24
|
+
describe "default mapping with nil attr" do
|
|
25
|
+
it "converts UninitializedClass to nil via the :omitted => :nil mapping" do
|
|
26
|
+
# Before the fix: returned uninit unchanged, causing Utils.present? to
|
|
27
|
+
# be true and the custom method to run for absent fields.
|
|
28
|
+
expect(transform.apply_value_map(uninit, default_vmap, nil)).to be_nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "converts nil to nil via the :nil => :nil mapping" do
|
|
32
|
+
expect(transform.apply_value_map(nil, default_vmap, nil)).to be_nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "passes initialized values through unchanged" do
|
|
36
|
+
expect(transform.apply_value_map("hello", default_vmap, nil)).to eq("hello")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "explicit :omitted => :omitted with nil attr" do
|
|
41
|
+
let(:vmap) { { omitted: :omitted, nil: :nil, empty: :empty } }
|
|
42
|
+
|
|
43
|
+
it "preserves UninitializedClass for explicit omitted: :omitted" do
|
|
44
|
+
expect(transform.apply_value_map(uninit, vmap, nil)).to eq(uninit)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Behavior-preservation: the pre-consolidation attribute-less path mapped the
|
|
49
|
+
# :omitted option to UninitializedClass.instance (present? => true, so the
|
|
50
|
+
# custom method runs), and resolved :nil/:empty consistently for empty inputs.
|
|
51
|
+
describe "non-:nil options with nil attr" do
|
|
52
|
+
it "maps a nil value to uninitialized via nil: :omitted" do
|
|
53
|
+
expect(transform.apply_value_map(nil, { nil: :omitted }, nil)).to eq(uninit)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "maps an empty value to uninitialized via empty: :omitted" do
|
|
57
|
+
expect(transform.apply_value_map("", { empty: :omitted }, nil)).to eq(uninit)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "maps an empty value to nil via empty: :nil" do
|
|
61
|
+
expect(transform.apply_value_map("", { empty: :nil }, nil)).to be_nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|