lutaml-model 0.8.19 → 0.8.23

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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +1 -1
  3. data/.github/workflows/rake.yml +1 -0
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +133 -72
  6. data/bench/bench_common.rb +8 -5
  7. data/bench/bench_niso.rb +1 -2
  8. data/bench/gate_config.rb +4 -1
  9. data/benchmark/serialization_benchmark.rb +4 -4
  10. data/docs/_guides/value-transformations.adoc +27 -0
  11. data/docs/_guides/xml-mapping.adoc +31 -0
  12. data/lib/compat/opal/generate_boot.rb +3 -1
  13. data/lib/compat/opal/lutaml_model_boot.rb +10 -7
  14. data/lib/compat/opal/moxml_boot.rb +6 -1
  15. data/lib/lutaml/hash_format/format.rb +29 -0
  16. data/lib/lutaml/hash_format.rb +5 -27
  17. data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
  18. data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
  19. data/lib/lutaml/json/format.rb +61 -0
  20. data/lib/lutaml/json/generator_options.rb +62 -0
  21. data/lib/lutaml/json.rb +4 -59
  22. data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
  23. data/lib/lutaml/jsonl/format.rb +22 -0
  24. data/lib/lutaml/jsonl.rb +5 -19
  25. data/lib/lutaml/jsonld/adapter.rb +12 -4
  26. data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +7 -0
  27. data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +17 -5
  28. data/lib/lutaml/key_value/adapter/jsonl/standard_adapter.rb +1 -1
  29. data/lib/lutaml/key_value/format.rb +26 -0
  30. data/lib/lutaml/key_value/transform.rb +4 -1
  31. data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
  32. data/lib/lutaml/key_value.rb +4 -21
  33. data/lib/lutaml/model/attribute.rb +38 -4
  34. data/lib/lutaml/model/collection.rb +7 -2
  35. data/lib/lutaml/model/compiled_rule.rb +1 -1
  36. data/lib/lutaml/model/config.rb +2 -2
  37. data/lib/lutaml/model/consolidation_map.rb +2 -2
  38. data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
  39. data/lib/lutaml/model/errors.rb +2 -2
  40. data/lib/lutaml/model/global_context.rb +4 -2
  41. data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
  42. data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
  43. data/lib/lutaml/model/runtime_compatibility.rb +2 -2
  44. data/lib/lutaml/model/schema/class_loader.rb +2 -1
  45. data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
  46. data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
  47. data/lib/lutaml/model/schema/file_writer.rb +5 -2
  48. data/lib/lutaml/model/schema/renderers/base.rb +4 -3
  49. data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
  50. data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
  51. data/lib/lutaml/model/schema/renderers/model.rb +18 -6
  52. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
  53. data/lib/lutaml/model/schema/renderers.rb +2 -1
  54. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
  55. data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
  56. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
  57. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
  58. data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
  59. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
  60. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
  61. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
  62. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
  63. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +2 -2
  64. data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
  65. data/lib/lutaml/model/sequence.rb +4 -4
  66. data/lib/lutaml/model/serialization_adapter.rb +1 -1
  67. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
  68. data/lib/lutaml/model/serialize/format_conversion.rb +40 -9
  69. data/lib/lutaml/model/serialize/initialization.rb +9 -1
  70. data/lib/lutaml/model/serialize/model_import.rb +1 -0
  71. data/lib/lutaml/model/serialize.rb +22 -3
  72. data/lib/lutaml/model/transformation.rb +5 -0
  73. data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
  74. data/lib/lutaml/model/type.rb +9 -0
  75. data/lib/lutaml/model/type_registry.rb +2 -2
  76. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  77. data/lib/lutaml/model/union.rb +5 -2
  78. data/lib/lutaml/model/utils.rb +2 -2
  79. data/lib/lutaml/model/value_policy.rb +27 -0
  80. data/lib/lutaml/model/version.rb +1 -1
  81. data/lib/lutaml/model.rb +17 -16
  82. data/lib/lutaml/rdf/mapping.rb +2 -2
  83. data/lib/lutaml/rdf/namespace_set.rb +2 -2
  84. data/lib/lutaml/rdf/transform.rb +2 -2
  85. data/lib/lutaml/toml/format.rb +48 -0
  86. data/lib/lutaml/toml.rb +5 -46
  87. data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
  88. data/lib/lutaml/xml/builder/base.rb +2 -2
  89. data/lib/lutaml/xml/format.rb +331 -0
  90. data/lib/lutaml/xml/mapping.rb +26 -2
  91. data/lib/lutaml/xml/mapping_rule.rb +62 -3
  92. data/lib/lutaml/xml/model_transform.rb +183 -55
  93. data/lib/lutaml/xml/namespace_collector.rb +3 -3
  94. data/lib/lutaml/xml/parse_session.rb +59 -0
  95. data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
  96. data/lib/lutaml/xml/schema/builder.rb +5 -5
  97. data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
  98. data/lib/lutaml/xml/schema/xsd.rb +0 -2
  99. data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
  100. data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
  101. data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
  102. data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
  103. data/lib/lutaml/xml/transformation.rb +29 -0
  104. data/lib/lutaml/xml/xml_element.rb +2 -0
  105. data/lib/lutaml/xml.rb +4 -328
  106. data/lib/lutaml/yaml/format.rb +30 -0
  107. data/lib/lutaml/yaml.rb +5 -28
  108. data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
  109. data/lib/lutaml/yamls/format.rb +22 -0
  110. data/lib/lutaml/yamls.rb +5 -19
  111. data/lib/tasks/benchmark_runner.rb +2 -2
  112. data/lutaml-model.gemspec +1 -1
  113. data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
  114. data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
  115. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
  116. data/spec/lutaml/model/attribute_spec.rb +8 -3
  117. data/spec/lutaml/model/collection_spec.rb +1 -1
  118. data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
  119. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +2 -2
  120. data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
  121. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
  122. data/spec/lutaml/model/defaults_spec.rb +45 -0
  123. data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
  124. data/spec/lutaml/model/json_spec.rb +1 -2
  125. data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
  126. data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
  127. data/spec/lutaml/model/store_spec.rb +3 -1
  128. data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
  129. data/spec/lutaml/model/union_attribute_spec.rb +4 -2
  130. data/spec/lutaml/model/value_policy_spec.rb +82 -0
  131. data/spec/lutaml/require_load_order_spec.rb +25 -0
  132. data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
  133. data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
  134. data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -0
  135. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
  136. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
  137. data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
  138. data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
  139. metadata +26 -4
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # json 3.0 removed JSON::State#[]. Ruby's generator passes a JSON::State to
6
+ # #to_json whenever a model is nested inside another JSON.generate call, so the
7
+ # model layer must not read option keys off it.
8
+ RSpec.describe "a model nested inside JSON.generate" do
9
+ before do
10
+ stub_const("JsonStateNestingModel", Class.new(Lutaml::Model::Serializable) do
11
+ attribute :name, :string
12
+ attribute :age, :integer
13
+ end)
14
+ end
15
+
16
+ let(:model) { JsonStateNestingModel.new(name: "John", age: 30) }
17
+
18
+ it "serializes inside a Hash" do
19
+ expect(JSON.generate({ "person" => model }))
20
+ .to eq('{"person":{"name":"John","age":30}}')
21
+ end
22
+
23
+ it "serializes inside an Array" do
24
+ expect(JSON.generate([model])).to eq('[{"name":"John","age":30}]')
25
+ end
26
+
27
+ # The generator's JSON::State carries the surrounding indent context. It has
28
+ # to reach the adapter, not be discarded, or a nested model silently renders
29
+ # compact inside an otherwise pretty document. Reference values are the
30
+ # output of unmodified main on json 2.20.0.
31
+ it "inherits the surrounding indent inside a pretty Hash" do
32
+ expect(JSON.pretty_generate({ "person" => model }))
33
+ .to eq(%({\n "person": {\n "name": "John",\n "age": 30\n }\n}))
34
+ end
35
+
36
+ it "inherits the surrounding indent inside a pretty Array" do
37
+ expect(JSON.pretty_generate([model]))
38
+ .to eq(%([\n {\n "name": "John",\n "age": 30\n }\n]))
39
+ end
40
+
41
+ it "still honours LutaML's own options when called directly" do
42
+ expect(model.to_json(pretty: true))
43
+ .to eq(%({\n "name": "John",\n "age": 30\n}))
44
+ end
45
+
46
+ # A non-Hash options argument must be replaced, not merely captured -- a
47
+ # falsy one (nil, false) previously stayed in place and the next line called
48
+ # .delete on it.
49
+ it "does not raise on a nil options argument" do
50
+ expect(model.to_json(nil)).to eq('{"name":"John","age":30}')
51
+ end
52
+
53
+ it "does not raise on a false options argument" do
54
+ expect(model.to_json(false)).to eq('{"name":"John","age":30}')
55
+ end
56
+
57
+ # A non-Hash options argument reaches every format, not just JSON, so the
58
+ # normalisation must not skip format-specific validation on the way past.
59
+ # A type-only model has no root mapping and must refuse to serialise alone.
60
+ it "still applies XML root validation to a non-Hash argument" do
61
+ stub_const("TypeOnlyModel", Class.new(Lutaml::Model::Serializable) do
62
+ attribute :n, :string
63
+ end)
64
+
65
+ expect { TypeOnlyModel.new(n: "a").to_xml(nil) }
66
+ .to raise_error(Lutaml::Model::TypeOnlyMappingError)
67
+ end
68
+
69
+ # Every example above goes through an INSTANCE entry point. The class-level
70
+ # one normalises separately, and deleting that normaliser leaves them all
71
+ # green.
72
+ it "normalises a state passed to the class-level entry point" do
73
+ state = JSON::State.new(indent: " ", object_nl: "\n", space: " ")
74
+
75
+ expect(JsonStateNestingModel.to_json(model, state))
76
+ .to eq(%({\n "name": "John",\n "age": 30\n}))
77
+ end
78
+
79
+ it "leaves non-JSON formats untouched" do
80
+ expect(model.to_yaml).to eq("---\nname: John\nage: 30\n")
81
+ end
82
+ end
@@ -112,7 +112,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
112
112
  <items>c</items>
113
113
  </container>
114
114
  XML
115
- expect(obj.element_order.map(&:name)).to eq(%w[singular items items items])
115
+ expect(obj.element_order.map(&:name)).to eq(%w[singular items items
116
+ items])
116
117
  end
117
118
 
118
119
  it "all direct setters: emits in call order" do
@@ -298,7 +299,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
298
299
  # so both assignments are recorded. The serializer emits the current
299
300
  # value of the attribute via element_order in recorded order; the
300
301
  # safety net guarantees no data loss.
301
- expect(obj.element_order.map(&:name)).to eq(%w[items items items items items])
302
+ expect(obj.element_order.map(&:name)).to eq(%w[items items items items
303
+ items])
302
304
  xml = obj.to_xml
303
305
  # The serialized output must contain all current items
304
306
  expect(xml).to include("<items>c</items>")
@@ -317,7 +319,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
317
319
  xml = obj.to_xml
318
320
 
319
321
  if !obj.singular.nil? && !obj.singular.to_s.empty?
320
- expect(xml).to include("<singular>"), "singular was dropped from output"
322
+ expect(xml).to include("<singular>"),
323
+ "singular was dropped from output"
321
324
  end
322
325
  return if obj.items.nil? || obj.items.empty?
323
326
 
@@ -43,7 +43,9 @@ RSpec.describe Lutaml::Model::Store do
43
43
 
44
44
  describe "indexed lookup" do
45
45
  it "builds index lazily on first resolve" do
46
- 5.times { |i| model_class.new(id: "obj-#{i}") }
46
+ # Hold strong references: Store keeps WeakRefs, so unreferenced
47
+ # objects can be collected before resolve runs.
48
+ _objects = Array.new(5) { |i| model_class.new(id: "obj-#{i}") }
47
49
 
48
50
  result = described_class.resolve(model_class, :id, "obj-3")
49
51
  expect(result.id).to eq("obj-3")
@@ -33,7 +33,8 @@ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
33
33
  end
34
34
 
35
35
  it "passes initialized values through unchanged" do
36
- expect(transform.apply_value_map("hello", default_vmap, nil)).to eq("hello")
36
+ expect(transform.apply_value_map("hello", default_vmap,
37
+ nil)).to eq("hello")
37
38
  end
38
39
  end
39
40
 
@@ -50,11 +51,13 @@ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
50
51
  # custom method runs), and resolved :nil/:empty consistently for empty inputs.
51
52
  describe "non-:nil options with nil attr" do
52
53
  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
+ expect(transform.apply_value_map(nil, { nil: :omitted },
55
+ nil)).to eq(uninit)
54
56
  end
55
57
 
56
58
  it "maps an empty value to uninitialized via empty: :omitted" do
57
- expect(transform.apply_value_map("", { empty: :omitted }, nil)).to eq(uninit)
59
+ expect(transform.apply_value_map("", { empty: :omitted },
60
+ nil)).to eq(uninit)
58
61
  end
59
62
 
60
63
  it "maps an empty value to nil via empty: :nil" do
@@ -311,7 +311,8 @@ RSpec.describe "Union-typed attributes (issue #190)" do
311
311
  it "serializes a model member built in plain Ruby identically to a deserialized one" do
312
312
  built = UnionAttributeSpec::Ceramic.new(
313
313
  firing_temperature:
314
- UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0, unit: "C"),
314
+ UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0,
315
+ unit: "C"),
315
316
  )
316
317
  round = UnionAttributeSpec::Ceramic.from_yaml(built.to_yaml)
317
318
  expect(round.firing_temperature)
@@ -424,7 +425,8 @@ RSpec.describe "Union-typed attributes (issue #190)" do
424
425
  end
425
426
 
426
427
  it "rejects unsupported scalar member types at definition time" do
427
- %i[hash symbol time time_without_date date date_time].each do |unsupported|
428
+ %i[hash symbol time time_without_date date
429
+ date_time].each do |unsupported|
428
430
  expect do
429
431
  Class.new(Lutaml::Model::Serializable) do
430
432
  attribute :bad, [unsupported, :string]
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module ValuePolicySpec
6
+ # A value type whose whole purpose is to wrap any literal:
7
+ # scalar, list, or map (issue #752).
8
+ class LiteralValue < Lutaml::Model::Type::Value
9
+ end
10
+
11
+ class LiteralDoc < Lutaml::Model::Serializable
12
+ attribute :v, LiteralValue
13
+ key_value do
14
+ map "v", to: :v
15
+ end
16
+ end
17
+
18
+ class StringsDoc < Lutaml::Model::Serializable
19
+ attribute :v, :string
20
+ key_value do
21
+ map "v", to: :v
22
+ end
23
+ end
24
+
25
+ class CollectionDoc < Lutaml::Model::Serializable
26
+ attribute :v, :string, collection: true
27
+ key_value do
28
+ map "v", to: :v
29
+ end
30
+ end
31
+ end
32
+
33
+ RSpec.describe "ValuePolicy shaping of non-collection attributes" do
34
+ describe "user-defined Type::Value subclass" do
35
+ it "receives the whole array from key-value data" do
36
+ expect(ValuePolicySpec::LiteralDoc.from_yaml("v: [a, b]").v)
37
+ .to eq(["a", "b"])
38
+ end
39
+
40
+ it "receives an empty array from key-value data" do
41
+ expect(ValuePolicySpec::LiteralDoc.from_yaml("v: []").v).to eq([])
42
+ end
43
+
44
+ it "receives the whole hash from key-value data" do
45
+ v = ValuePolicySpec::LiteralDoc.from_yaml("v: {a: 1}").v
46
+ expect(v.value).to eq("a" => 1)
47
+ end
48
+
49
+ it "keeps scalar behavior" do
50
+ doc = ValuePolicySpec::LiteralDoc.from_yaml(<<~YAML)
51
+ v: hello
52
+ YAML
53
+ expect(doc.v).to eq("hello")
54
+ end
55
+
56
+ it "round-trips an array value through YAML" do
57
+ doc = ValuePolicySpec::LiteralDoc.from_yaml("v: [a, b]")
58
+ expect(ValuePolicySpec::LiteralDoc.from_yaml(doc.to_yaml).v)
59
+ .to eq(["a", "b"])
60
+ end
61
+
62
+ it "accepts an array assigned after construction" do
63
+ doc = ValuePolicySpec::LiteralDoc.new(v: "seed")
64
+ doc.v = ["a", "b"]
65
+ expect(doc.v).to eq(["a", "b"])
66
+ end
67
+ end
68
+
69
+ describe "built-in scalar types" do
70
+ it "keep the collection: true guidance error" do
71
+ expect { ValuePolicySpec::StringsDoc.from_yaml("v: [a]") }
72
+ .to raise_error(Lutaml::Model::CollectionTrueMissingError)
73
+ end
74
+ end
75
+
76
+ describe "collection attributes" do
77
+ it "keep element-wise casting" do
78
+ expect(ValuePolicySpec::CollectionDoc.from_yaml("v: [a, b]").v)
79
+ .to eq(["a", "b"])
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model: every public format entrypoint and `require "lutaml/model"`
6
+ # itself must not produce "circular require considered harmful" warnings
7
+ # under ruby -w (regression guard after the public-wrapper/internal-format
8
+ # restructuring).
9
+ RSpec.describe "require load order" do
10
+ it "loads lutaml/model with no circular-require warnings under -w" do
11
+ output = `bundle exec ruby -w -e 'require "lutaml/model"' 2>&1`
12
+ circular = output.lines.count { |l| l.include?("circular require considered harmful") }
13
+ expect(circular).to eq(0),
14
+ "Expected no circular-require warnings, got #{circular}:\n#{output}"
15
+ end
16
+
17
+ %w[json key_value yaml toml hash_format jsonl yamls xml].each do |format|
18
+ it "loads lutaml/#{format} standalone without circular-require warnings" do
19
+ output = `bundle exec ruby -w -e 'require "lutaml/#{format}"' 2>&1`
20
+ circular = output.lines.count { |l| l.include?("circular require considered harmful") }
21
+ expect(circular).to eq(0),
22
+ "Expected no circular-require warnings loading lutaml/#{format}, got #{circular}:\n#{output}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string: true
2
+
3
+ require "spec_helper"
4
+ require_relative "../../../lib/lutaml/model"
5
+
6
+ # lutaml-model#758: a plain (unnamespaced) map_attribute rule on an
7
+ # element-namespaced class must not absorb a namespaced sibling attribute
8
+ # (and vice versa), and a type-level `namespace :blank` must not crash.
9
+ module ElementNsDisjointSpec
10
+ class XmiNs < Lutaml::Xml::Namespace
11
+ uri "http://www.example.com/xmi"
12
+ prefix_default "xmi"
13
+ end
14
+
15
+ class ElemNs < Lutaml::Xml::Namespace
16
+ uri "http://www.example.com/elem"
17
+ prefix_default "e"
18
+ end
19
+
20
+ class XmiTypeAttr < Lutaml::Model::Type::String
21
+ xml do
22
+ namespace XmiNs
23
+ end
24
+ end
25
+
26
+ class DocBase < Lutaml::Model::Serializable
27
+ attribute :xmi_type, XmiTypeAttr
28
+
29
+ xml do
30
+ namespace ElemNs
31
+ map_attribute "type", to: :xmi_type
32
+ end
33
+ end
34
+
35
+ class Doc < DocBase
36
+ attribute :plain_type, Lutaml::Model::Type::String
37
+
38
+ xml do
39
+ root "doc"
40
+ map_attribute "type", to: :plain_type
41
+ end
42
+ end
43
+
44
+ class BlankType < Lutaml::Model::Type::String
45
+ xml do
46
+ namespace :blank
47
+ end
48
+ end
49
+
50
+ class BlankDoc < Lutaml::Model::Serializable
51
+ attribute :v, BlankType
52
+ xml do
53
+ root "bd"
54
+ map_attribute "v", to: :v
55
+ end
56
+ end
57
+ end
58
+
59
+ RSpec.describe "attribute namespace-disjointness on element-namespaced classes (#758)" do
60
+ it "routes xmi:type to the namespaced slot and leaves plain_type nil" do
61
+ doc = ElementNsDisjointSpec::Doc.from_xml(
62
+ %(<doc xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter"/>),
63
+ )
64
+ expect(doc.xmi_type).to eq("uml:Parameter")
65
+ expect(doc.plain_type).to be_nil
66
+ end
67
+
68
+ it "routes an unprefixed type to plain_type, leaves xmi_type nil" do
69
+ doc = ElementNsDisjointSpec::Doc.from_xml(%(<doc type="EAnone_void"/>))
70
+ expect(doc.plain_type).to eq("EAnone_void")
71
+ expect(doc.xmi_type).to be_nil
72
+ end
73
+
74
+ it "round-trips both spellings without emitting a spurious duplicate" do
75
+ doc = ElementNsDisjointSpec::Doc.from_xml(
76
+ %(<doc xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter"/>),
77
+ )
78
+ out = doc.to_xml.strip
79
+ expect(out).to include('xmi:type="uml:Parameter"')
80
+ expect(out).not_to match(/\stype="uml:Parameter"/)
81
+ end
82
+
83
+ it "survives type-level namespace :blank without crashing" do
84
+ expect do
85
+ ElementNsDisjointSpec::BlankDoc.from_xml(%(<bd v="x"/>))
86
+ end.not_to raise_error
87
+ end
88
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Two map_attribute rules that share a local name but are distinguished by
6
+ # their type classes' bound namespaces must parse disjointly, mirroring
7
+ # serialization (lutaml-model#744).
8
+ #
9
+ # The coexisting-spelling cases under nokogiri/rexml additionally require the
10
+ # moxml adapter fixes (lutaml/moxml#94, lutaml/moxml#95); oga and ox already
11
+ # preserve same-local-name attributes across namespaces.
12
+ module AttributeNamespaceDisjointnessSpec
13
+ class XmiNs < Lutaml::Xml::Namespace
14
+ uri "http://www.example.com/xmi"
15
+ prefix_default "xmi"
16
+ end
17
+
18
+ class XmiNsType < Lutaml::Model::Type::String
19
+ xml do
20
+ namespace XmiNs
21
+ end
22
+ end
23
+
24
+ class Probe < Lutaml::Model::Serializable
25
+ attribute :xmi_type, XmiNsType
26
+ attribute :plain_type, Lutaml::Model::Type::String
27
+
28
+ xml do
29
+ element "probe"
30
+ map_attribute "type", to: :xmi_type
31
+ map_attribute "type", to: :plain_type
32
+ end
33
+ end
34
+
35
+ def self.parse(xml, adapter)
36
+ Lutaml::Model::Config.with_adapter(xml: adapter) { Probe.from_xml(xml) }
37
+ end
38
+ end
39
+
40
+ RSpec.describe "Attribute namespace-disjoint parsing" do
41
+ let(:namespaced_xmi) do
42
+ %(<probe xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter"/>)
43
+ end
44
+
45
+ let(:namespaced_foo) do
46
+ %(<probe xmlns:foo="http://www.example.com/xmi" foo:type="uml:Parameter"/>)
47
+ end
48
+
49
+ let(:plain) { %(<probe type="EAnone_void"/>) }
50
+
51
+ let(:both_spellings) do
52
+ %(<probe xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter" type="EAnone_void"/>)
53
+ end
54
+
55
+ let(:both_spellings_plain_first) do
56
+ %(<probe type="EAnone_void" xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter"/>)
57
+ end
58
+
59
+ %i[oga ox nokogiri].each do |adapter|
60
+ context "with the #{adapter} adapter" do
61
+ it "routes a namespaced rule to the attribute bound to its URI" do
62
+ probe = AttributeNamespaceDisjointnessSpec.parse(namespaced_xmi,
63
+ adapter)
64
+
65
+ expect(probe.xmi_type).to eq("uml:Parameter")
66
+ expect(probe.plain_type).to be_nil
67
+ end
68
+
69
+ it "accepts any prefix bound to the namespace URI" do
70
+ probe = AttributeNamespaceDisjointnessSpec.parse(namespaced_foo,
71
+ adapter)
72
+
73
+ expect(probe.xmi_type).to eq("uml:Parameter")
74
+ expect(probe.plain_type).to be_nil
75
+ end
76
+
77
+ it "does not let a namespace-less attribute satisfy the namespaced rule" do
78
+ probe = AttributeNamespaceDisjointnessSpec.parse(plain, adapter)
79
+
80
+ expect(probe.xmi_type).to be_nil
81
+ expect(probe.plain_type).to eq("EAnone_void")
82
+ end
83
+ end
84
+ end
85
+
86
+ # Same-local-name attributes coexisting on one element require the moxml
87
+ # fixes (lutaml/moxml#94 drops one under nokogiri; lutaml/moxml#95 crashes
88
+ # under rexml), so these cases run on the adapters that preserve both.
89
+ %i[oga ox].each do |adapter|
90
+ context "with the #{adapter} adapter" do
91
+ it "keeps both spellings disjoint regardless of document order" do
92
+ [both_spellings, both_spellings_plain_first].each do |xml|
93
+ probe = AttributeNamespaceDisjointnessSpec.parse(xml, adapter)
94
+
95
+ expect(probe.xmi_type).to eq("uml:Parameter")
96
+ expect(probe.plain_type).to eq("EAnone_void")
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "serialization" do
103
+ it "writes each rule in its own namespace" do
104
+ probe = AttributeNamespaceDisjointnessSpec::Probe.new(
105
+ xmi_type: "uml:Parameter",
106
+ plain_type: "EAnone_void",
107
+ )
108
+
109
+ expect(probe.to_xml.strip).to be_xml_equivalent_to(
110
+ %(<probe xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter" type="EAnone_void"/>),
111
+ )
112
+ end
113
+
114
+ it "round-trips both spellings through parsing" do
115
+ probe = AttributeNamespaceDisjointnessSpec::Probe.new(
116
+ xmi_type: "uml:Parameter",
117
+ plain_type: "EAnone_void",
118
+ )
119
+ reparsed = AttributeNamespaceDisjointnessSpec.parse(probe.to_xml, :oga)
120
+
121
+ expect(reparsed.xmi_type).to eq("uml:Parameter")
122
+ expect(reparsed.plain_type).to eq("EAnone_void")
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#754: parsing a document whose elements live in a namespace
6
+ # different from the model's bound namespace must not silently drop
7
+ # element content. The document's own namespace is adopted as an implicit
8
+ # alias of the model's namespace for the duration of the parse, so
9
+ # lenient (out-of-namespace) parsing is lossless for element content the
10
+ # way in-namespace parsing is.
11
+ module OutOfNamespaceParsingSpec
12
+ class Gml32 < Lutaml::Xml::Namespace
13
+ uri "http://www.opengis.net/gml/3.2"
14
+ prefix_default "gml"
15
+ end
16
+
17
+ class Code < Lutaml::Model::Serializable
18
+ attribute :value, :string
19
+
20
+ xml do
21
+ root "name"
22
+ namespace Gml32
23
+ map_content to: :value
24
+ end
25
+ end
26
+
27
+ class EntryDefinition < Lutaml::Model::Serializable
28
+ attribute :description, :string
29
+ attribute :name, Code, collection: true
30
+
31
+ xml do
32
+ element "Definition"
33
+ namespace Gml32
34
+ map_element "description", to: :description
35
+ map_element "name", to: :name
36
+ end
37
+ end
38
+
39
+ class DictionaryEntry < Lutaml::Model::Serializable
40
+ attribute :definition, EntryDefinition
41
+
42
+ xml do
43
+ element "dictionaryEntry"
44
+ namespace Gml32
45
+ map_element "Definition", to: :definition
46
+ end
47
+ end
48
+
49
+ class Dictionary < Lutaml::Model::Serializable
50
+ attribute :name, Code, collection: true
51
+ attribute :dictionary_entry, DictionaryEntry, collection: true
52
+
53
+ xml do
54
+ element "Dictionary"
55
+ namespace Gml32
56
+ map_element "name", to: :name
57
+ map_element "dictionaryEntry", to: :dictionary_entry
58
+ end
59
+ end
60
+
61
+ def self.gml_doc(uri)
62
+ <<~XML
63
+ <?xml version="1.0" encoding="UTF-8"?>
64
+ <gml:Dictionary xmlns:gml="#{uri}">
65
+ <gml:name>codes</gml:name>
66
+ <gml:dictionaryEntry>
67
+ <gml:Definition>
68
+ <gml:description>first</gml:description>
69
+ <gml:name>ONE</gml:name>
70
+ </gml:Definition>
71
+ </gml:dictionaryEntry>
72
+ </gml:Dictionary>
73
+ XML
74
+ end
75
+ end
76
+
77
+ RSpec.describe "out-of-namespace document parsing" do
78
+ let(:in_namespace_xml) { OutOfNamespaceParsingSpec.gml_doc("http://www.opengis.net/gml/3.2") }
79
+ let(:out_of_namespace_xml) { OutOfNamespaceParsingSpec.gml_doc("http://www.opengis.net/gml") }
80
+
81
+ it "parses simple-type element content from a document in the bound namespace" do
82
+ definition = OutOfNamespaceParsingSpec::Dictionary
83
+ .from_xml(in_namespace_xml).dictionary_entry.first.definition
84
+ expect(definition.description).to eq("first")
85
+ end
86
+
87
+ it "keeps simple-type element content when the document namespace differs (#754)" do
88
+ definition = OutOfNamespaceParsingSpec::Dictionary
89
+ .from_xml(out_of_namespace_xml).dictionary_entry.first.definition
90
+ expect(definition.description).to eq("first")
91
+ end
92
+
93
+ it "keeps serializable-typed element content when the document namespace differs" do
94
+ definition = OutOfNamespaceParsingSpec::Dictionary
95
+ .from_xml(out_of_namespace_xml).dictionary_entry.first.definition
96
+ expect(definition.name.map(&:value)).to eq(["ONE"])
97
+ end
98
+
99
+ it "keeps simple-type content at the document root level" do
100
+ dictionary = OutOfNamespaceParsingSpec::Dictionary.from_xml(out_of_namespace_xml)
101
+ expect(dictionary.name.map(&:value)).to eq(["codes"])
102
+ end
103
+
104
+ it "does not adopt an unrelated child namespace" do
105
+ xml = <<~XML
106
+ <?xml version="1.0" encoding="UTF-8"?>
107
+ <gml:Dictionary xmlns:gml="http://www.opengis.net/gml"
108
+ xmlns:city="http://www.opengis.net/citygml/2.0">
109
+ <city:name>not mine</city:name>
110
+ </gml:Dictionary>
111
+ XML
112
+ dictionary = OutOfNamespaceParsingSpec::Dictionary.from_xml(xml)
113
+ expect(dictionary.name).to be_empty
114
+ end
115
+
116
+ it "round-trips out-of-namespace content without losing the description" do
117
+ dictionary = OutOfNamespaceParsingSpec::Dictionary.from_xml(out_of_namespace_xml)
118
+ output = dictionary.to_xml(prefix: true)
119
+ expect(output).to include("first")
120
+ reparsed = OutOfNamespaceParsingSpec::Dictionary.from_xml(output)
121
+ expect(reparsed.dictionary_entry.first.definition.description).to eq("first")
122
+ end
123
+ end
@@ -14,7 +14,9 @@ RSpec.describe Lutaml::Model::Schema::RncCompiler do
14
14
 
15
15
  describe ".to_models" do
16
16
  context "with an RNC schema using named patterns and start |=" do
17
- let(:schema) { File.read("spec/fixtures/xml/schema/rnc/address_book.rnc") }
17
+ let(:schema) do
18
+ File.read("spec/fixtures/xml/schema/rnc/address_book.rnc")
19
+ end
18
20
  let(:sources) { described_class.to_models(schema) }
19
21
 
20
22
  it "compiles RNC through the RNG compiler" do
@@ -65,7 +67,9 @@ RSpec.describe Lutaml::Model::Schema::RncCompiler do
65
67
 
66
68
  context "with create_files output" do
67
69
  let(:dir) { Dir.mktmpdir }
68
- let(:schema) { File.read("spec/fixtures/xml/schema/rnc/address_book.rnc") }
70
+ let(:schema) do
71
+ File.read("spec/fixtures/xml/schema/rnc/address_book.rnc")
72
+ end
69
73
 
70
74
  before do
71
75
  stub_const("RncAddressBookSpec", Module.new)
@@ -206,7 +210,8 @@ RSpec.describe Lutaml::Model::Schema::RncCompiler do
206
210
  "spec/fixtures/xml/schema/rnc/includes/circular_a.rnc"
207
211
 
208
212
  expect do
209
- described_class.to_models(File.read(circular_path), location: circular_path)
213
+ described_class.to_models(File.read(circular_path),
214
+ location: circular_path)
210
215
  end.to raise_error(/circular_a\.rnc/)
211
216
  end
212
217
  end
@@ -308,7 +313,9 @@ RSpec.describe Lutaml::Model::Schema::RncCompiler do
308
313
  # The inline restricted type is subclassed (not mutated) so a fresh
309
314
  # namespaced type inherits its constraints and carries the namespace.
310
315
  expect(sources["CodeType"]).not_to match(/namespace \w+Namespace/)
311
- subclass = sources.keys.find { |k| sources[k].match?(/class \w+ < CodeType/) }
316
+ subclass = sources.keys.find do |k|
317
+ sources[k].match?(/class \w+ < CodeType/)
318
+ end
312
319
  expect(subclass).not_to be_nil
313
320
  expect(sources[subclass]).to match(/namespace \w+Namespace/)
314
321
  end
@@ -323,7 +330,9 @@ RSpec.describe Lutaml::Model::Schema::RncCompiler do
323
330
  namespaces = sources.select { |_, v| v.include?("Lutaml::Xml::W3c::XmlNamespace") }
324
331
  expect(namespaces.size).to eq(2)
325
332
  uris = namespaces.values.map { |v| v[/uri "([^"]+)"/, 1] }
326
- prefixes = namespaces.values.map { |v| v[/prefix_default "([^"]+)"/, 1] }
333
+ prefixes = namespaces.values.map do |v|
334
+ v[/prefix_default "([^"]+)"/, 1]
335
+ end
327
336
  expect(uris).to contain_exactly("urn:a", "urn:b")
328
337
  expect(prefixes.uniq.size).to eq(2)
329
338
  end
@@ -341,7 +350,9 @@ RSpec.describe Lutaml::Model::Schema::RncCompiler do
341
350
  }
342
351
  RNC
343
352
 
344
- wrapper = sources.keys.find { |k| sources[k].match?(/namespace \w+Namespace/) && !k.include?("Namespace") }
353
+ wrapper = sources.keys.find do |k|
354
+ sources[k].match?(/namespace \w+Namespace/) && !k.include?("Namespace")
355
+ end
345
356
  expect(sources[wrapper]).to include("< Lutaml::Model::Type::DateTime")
346
357
  end
347
358
  end