lutaml-model 0.8.19 → 0.8.22

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 (120) 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/lib/compat/opal/generate_boot.rb +3 -1
  12. data/lib/compat/opal/lutaml_model_boot.rb +10 -7
  13. data/lib/compat/opal/moxml_boot.rb +6 -1
  14. data/lib/lutaml/hash_format/format.rb +29 -0
  15. data/lib/lutaml/hash_format.rb +5 -27
  16. data/lib/lutaml/json/format.rb +61 -0
  17. data/lib/lutaml/json.rb +4 -59
  18. data/lib/lutaml/jsonl/format.rb +22 -0
  19. data/lib/lutaml/jsonl.rb +5 -19
  20. data/lib/lutaml/key_value/format.rb +26 -0
  21. data/lib/lutaml/key_value/transform.rb +4 -1
  22. data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
  23. data/lib/lutaml/key_value.rb +4 -21
  24. data/lib/lutaml/model/attribute.rb +38 -4
  25. data/lib/lutaml/model/collection.rb +2 -2
  26. data/lib/lutaml/model/compiled_rule.rb +1 -1
  27. data/lib/lutaml/model/config.rb +2 -2
  28. data/lib/lutaml/model/consolidation_map.rb +2 -2
  29. data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
  30. data/lib/lutaml/model/errors.rb +2 -2
  31. data/lib/lutaml/model/global_context.rb +4 -2
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
  33. data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
  34. data/lib/lutaml/model/runtime_compatibility.rb +2 -2
  35. data/lib/lutaml/model/schema/class_loader.rb +2 -1
  36. data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
  37. data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
  38. data/lib/lutaml/model/schema/file_writer.rb +5 -2
  39. data/lib/lutaml/model/schema/renderers/base.rb +4 -3
  40. data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
  41. data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
  42. data/lib/lutaml/model/schema/renderers/model.rb +18 -6
  43. data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
  44. data/lib/lutaml/model/schema/renderers.rb +2 -1
  45. data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
  46. data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
  47. data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
  48. data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
  49. data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
  50. data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
  51. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
  52. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
  53. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
  54. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +2 -2
  55. data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
  56. data/lib/lutaml/model/sequence.rb +4 -4
  57. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
  58. data/lib/lutaml/model/serialize/format_conversion.rb +2 -2
  59. data/lib/lutaml/model/serialize/initialization.rb +9 -1
  60. data/lib/lutaml/model/serialize/model_import.rb +1 -0
  61. data/lib/lutaml/model/serialize.rb +4 -3
  62. data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
  63. data/lib/lutaml/model/type.rb +9 -0
  64. data/lib/lutaml/model/type_registry.rb +2 -2
  65. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  66. data/lib/lutaml/model/union.rb +5 -2
  67. data/lib/lutaml/model/utils.rb +2 -2
  68. data/lib/lutaml/model/value_policy.rb +27 -0
  69. data/lib/lutaml/model/version.rb +1 -1
  70. data/lib/lutaml/model.rb +17 -16
  71. data/lib/lutaml/rdf/mapping.rb +2 -2
  72. data/lib/lutaml/rdf/namespace_set.rb +2 -2
  73. data/lib/lutaml/rdf/transform.rb +2 -2
  74. data/lib/lutaml/toml/format.rb +48 -0
  75. data/lib/lutaml/toml.rb +5 -46
  76. data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
  77. data/lib/lutaml/xml/builder/base.rb +2 -2
  78. data/lib/lutaml/xml/format.rb +331 -0
  79. data/lib/lutaml/xml/mapping.rb +2 -2
  80. data/lib/lutaml/xml/mapping_rule.rb +62 -3
  81. data/lib/lutaml/xml/model_transform.rb +132 -55
  82. data/lib/lutaml/xml/namespace_collector.rb +3 -3
  83. data/lib/lutaml/xml/parse_session.rb +59 -0
  84. data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
  85. data/lib/lutaml/xml/schema/builder.rb +5 -5
  86. data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
  87. data/lib/lutaml/xml/schema/xsd.rb +0 -2
  88. data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
  89. data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
  90. data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
  91. data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
  92. data/lib/lutaml/xml/xml_element.rb +2 -0
  93. data/lib/lutaml/xml.rb +4 -328
  94. data/lib/lutaml/yaml/format.rb +30 -0
  95. data/lib/lutaml/yaml.rb +5 -28
  96. data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
  97. data/lib/lutaml/yamls/format.rb +22 -0
  98. data/lib/lutaml/yamls.rb +5 -19
  99. data/lib/tasks/benchmark_runner.rb +2 -2
  100. data/lutaml-model.gemspec +1 -1
  101. data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
  102. data/spec/lutaml/model/attribute_spec.rb +8 -3
  103. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +2 -2
  104. data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
  105. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
  106. data/spec/lutaml/model/defaults_spec.rb +45 -0
  107. data/spec/lutaml/model/json_spec.rb +1 -2
  108. data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
  109. data/spec/lutaml/model/store_spec.rb +3 -1
  110. data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
  111. data/spec/lutaml/model/union_attribute_spec.rb +4 -2
  112. data/spec/lutaml/model/value_policy_spec.rb +82 -0
  113. data/spec/lutaml/require_load_order_spec.rb +25 -0
  114. data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
  115. data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
  116. data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -0
  117. data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
  118. data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
  119. data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
  120. metadata +20 -4
@@ -0,0 +1,74 @@
1
+ # frozen_string: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#746: an empty value ("", [], {}) is a PRESENT value per
6
+ # docs/_guides/missing-values-handling.adoc and must reach custom `from`
7
+ # methods. Only nil (non-existent) and an omitted key (undefined) skip them.
8
+ module CustomFromEmptyValuesSpec
9
+ class Tagged < Lutaml::Model::Serializable
10
+ attribute :received, :string
11
+
12
+ key_value do
13
+ map "tags", to: :received, with: { to: :tags_to, from: :tags_from }
14
+ end
15
+
16
+ def tags_to(model, doc)
17
+ doc["tags"] = model.received
18
+ end
19
+
20
+ def tags_from(model, value)
21
+ model.received = value.inspect
22
+ end
23
+ end
24
+
25
+ class Mapped < Lutaml::Model::Serializable
26
+ attribute :received, :string
27
+
28
+ key_value do
29
+ map "fields", to: :received, with: { to: :fields_to, from: :fields_from }
30
+ end
31
+
32
+ def fields_to(model, doc)
33
+ doc["fields"] = model.received
34
+ end
35
+
36
+ def fields_from(model, value)
37
+ model.received = value.inspect
38
+ end
39
+ end
40
+ end
41
+
42
+ RSpec.describe "custom from methods and empty values" do
43
+ it "receives an empty collection" do
44
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags: []").received)
45
+ .to eq("[]")
46
+ end
47
+
48
+ it "receives an empty string" do
49
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml('tags: ""').received)
50
+ .to eq("\"\"")
51
+ end
52
+
53
+ it "receives an empty mapping" do
54
+ expect(CustomFromEmptyValuesSpec::Mapped.from_yaml("fields: {}").received)
55
+ .to eq("{}")
56
+ end
57
+
58
+ it "receives empty values from JSON too" do
59
+ expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": []})).received)
60
+ .to eq("[]")
61
+ expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": ""})).received)
62
+ .to eq("\"\"")
63
+ end
64
+
65
+ it "still skips the method for a nil value" do
66
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags:").received)
67
+ .to be_nil
68
+ end
69
+
70
+ it "still skips the method for an omitted key" do
71
+ expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("{}").received)
72
+ .to be_nil
73
+ end
74
+ end
@@ -201,8 +201,8 @@ module CustomVobjectAdapterSpec
201
201
  @objects
202
202
  end
203
203
 
204
- def map(&)
205
- @objects.map(&)
204
+ def map(&block)
205
+ @objects.map(&block)
206
206
  end
207
207
 
208
208
  private
@@ -260,16 +260,16 @@ module CustomVobjectAdapterSpec
260
260
  @element_type = element_type
261
261
  end
262
262
 
263
- def map_value(to:, **)
264
- add_mapping(:value, to, type: :simple, **)
263
+ def map_value(to:, **options)
264
+ add_mapping(:value, to, type: :simple, **options)
265
265
  end
266
266
 
267
- def map_property(name, to:, type: :simple, **)
268
- add_mapping(name.downcase, to, type: type, **)
267
+ def map_property(name, to:, type: :simple, **options)
268
+ add_mapping(name.downcase, to, type: type, **options)
269
269
  end
270
270
 
271
- def map_component(name, to:, **)
272
- add_mapping(name.downcase, to, type: :component, **)
271
+ def map_component(name, to:, **options)
272
+ add_mapping(name.downcase, to, type: :component, **options)
273
273
  end
274
274
 
275
275
  def map_field_set(count:, item_type:, item_options: {})
@@ -146,6 +146,33 @@ module DefaultsSpec
146
146
 
147
147
  class InstanceAwareGrandChild < InstanceAwareChild
148
148
  end
149
+
150
+ # Class-method defaults must keep resolving as they did before 0.8.0
151
+ # (lutaml-model#745): default lambdas are evaluated against the instance,
152
+ # with the model class as fallback for methods the instance does not have.
153
+ class ClassMethodDefault < Lutaml::Model::Serializable
154
+ attribute :os, :string, default: -> { detect_os }
155
+
156
+ def self.detect_os
157
+ "from-class-method"
158
+ end
159
+ end
160
+
161
+ class ClassMethodDefaultWithArgs < Lutaml::Model::Serializable
162
+ attribute :label, :string, default: -> { build_label("x") }
163
+
164
+ def self.build_label(prefix)
165
+ "#{prefix}-label"
166
+ end
167
+ end
168
+
169
+ class InstanceMethodDefault < Lutaml::Model::Serializable
170
+ attribute :value, :string, default: -> { instance_helper }
171
+
172
+ def instance_helper
173
+ "from-instance-method"
174
+ end
175
+ end
149
176
  end
150
177
 
151
178
  RSpec.describe DefaultsSpec::Glaze do
@@ -346,3 +373,21 @@ RSpec.describe DefaultsSpec::Glaze do
346
373
  end
347
374
  end
348
375
  end
376
+
377
+ RSpec.describe DefaultsSpec::ClassMethodDefault do
378
+ it "resolves class methods from default lambdas" do
379
+ expect(described_class.new.os).to eq("from-class-method")
380
+ end
381
+ end
382
+
383
+ RSpec.describe DefaultsSpec::ClassMethodDefaultWithArgs do
384
+ it "passes arguments through to class methods" do
385
+ expect(described_class.new.label).to eq("x-label")
386
+ end
387
+ end
388
+
389
+ RSpec.describe DefaultsSpec::InstanceMethodDefault do
390
+ it "still resolves instance methods in default lambdas" do
391
+ expect(described_class.new.value).to eq("from-instance-method")
392
+ end
393
+ end
@@ -8,9 +8,8 @@ RSpec.describe Lutaml::Model::Json do
8
8
  let(:lib_path) { File.expand_path("../../../lib", __dir__) }
9
9
 
10
10
  it "loads require 'lutaml/json' without preloading lutaml/model" do
11
- # rubocop:disable Style/CommandLiteral
11
+ # rubocop:disable-next Style/CommandLiteral
12
12
  result = `#{RbConfig.ruby} -I#{lib_path} -e 'require "lutaml/json"; puts :ok' 2>&1`
13
- # rubocop:enable Style/CommandLiteral
14
13
 
15
14
  expect($?.success?).to be(true), result
16
15
  expect(result).to include("ok")
@@ -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