lutaml-model 0.6.7 → 0.7.2

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos-todo.json +7 -0
  3. data/.github/workflows/dependent-repos.json +17 -9
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +51 -65
  6. data/Gemfile +4 -1
  7. data/README.adoc +5083 -2612
  8. data/RELEASE_NOTES.adoc +346 -0
  9. data/docs/custom_adapters.adoc +144 -0
  10. data/lib/lutaml/model/attribute.rb +101 -16
  11. data/lib/lutaml/model/choice.rb +7 -0
  12. data/lib/lutaml/model/comparable_model.rb +48 -9
  13. data/lib/lutaml/model/config.rb +48 -42
  14. data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +1 -1
  15. data/lib/lutaml/model/error/polymorphic_error.rb +14 -0
  16. data/lib/lutaml/model/error.rb +1 -0
  17. data/lib/lutaml/model/format_registry.rb +41 -0
  18. data/lib/lutaml/model/hash/document.rb +11 -0
  19. data/lib/lutaml/model/hash/mapping.rb +19 -0
  20. data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
  21. data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
  22. data/lib/lutaml/model/hash/transform.rb +8 -0
  23. data/lib/lutaml/model/hash.rb +21 -0
  24. data/lib/lutaml/model/json/document.rb +11 -0
  25. data/lib/lutaml/model/json/mapping.rb +19 -0
  26. data/lib/lutaml/model/json/mapping_rule.rb +9 -0
  27. data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
  28. data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
  29. data/lib/lutaml/model/json/transform.rb +8 -0
  30. data/lib/lutaml/model/json.rb +21 -0
  31. data/lib/lutaml/model/key_value_document.rb +27 -0
  32. data/lib/lutaml/model/{key_value_mapping.rb → mapping/key_value_mapping.rb} +64 -16
  33. data/lib/lutaml/model/{key_value_mapping_rule.rb → mapping/key_value_mapping_rule.rb} +18 -2
  34. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  35. data/lib/lutaml/model/mapping/mapping_rule.rb +300 -0
  36. data/lib/lutaml/model/schema/xml_compiler.rb +15 -15
  37. data/lib/lutaml/model/sequence.rb +2 -2
  38. data/lib/lutaml/model/serialization_adapter.rb +22 -0
  39. data/lib/lutaml/model/serialize.rb +219 -444
  40. data/lib/lutaml/model/services/logger.rb +54 -0
  41. data/lib/lutaml/model/services/transformer.rb +48 -0
  42. data/lib/lutaml/model/services.rb +2 -0
  43. data/lib/lutaml/model/toml/document.rb +11 -0
  44. data/lib/lutaml/model/toml/mapping.rb +27 -0
  45. data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
  46. data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
  47. data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
  48. data/lib/lutaml/model/toml/transform.rb +8 -0
  49. data/lib/lutaml/model/toml.rb +30 -0
  50. data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
  51. data/lib/lutaml/model/transform/xml_transform.rb +239 -0
  52. data/lib/lutaml/model/transform.rb +78 -0
  53. data/lib/lutaml/model/type/date.rb +1 -1
  54. data/lib/lutaml/model/type/date_time.rb +2 -2
  55. data/lib/lutaml/model/type/hash.rb +1 -1
  56. data/lib/lutaml/model/type/time.rb +2 -2
  57. data/lib/lutaml/model/type/time_without_date.rb +2 -2
  58. data/lib/lutaml/model/type/value.rb +6 -9
  59. data/lib/lutaml/model/uninitialized_class.rb +64 -0
  60. data/lib/lutaml/model/utils.rb +44 -0
  61. data/lib/lutaml/model/validation.rb +1 -0
  62. data/lib/lutaml/model/version.rb +1 -1
  63. data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
  64. data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
  65. data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
  66. data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +41 -21
  67. data/lib/lutaml/model/xml/element.rb +32 -0
  68. data/lib/lutaml/model/xml/mapping.rb +410 -0
  69. data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
  70. data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
  71. data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
  72. data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
  73. data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
  74. data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
  75. data/lib/lutaml/model/xml/transform.rb +8 -0
  76. data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
  77. data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +23 -10
  78. data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
  79. data/lib/lutaml/model/xml.rb +31 -0
  80. data/lib/lutaml/model/xml_adapter/element.rb +11 -25
  81. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
  82. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
  83. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
  84. data/lib/lutaml/model/yaml/document.rb +10 -0
  85. data/lib/lutaml/model/yaml/mapping.rb +19 -0
  86. data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
  87. data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
  88. data/lib/lutaml/model/yaml/transform.rb +8 -0
  89. data/lib/lutaml/model/yaml.rb +21 -0
  90. data/lib/lutaml/model.rb +40 -4
  91. data/lutaml-model.gemspec +0 -4
  92. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +7 -7
  93. data/spec/fixtures/person.rb +5 -5
  94. data/spec/lutaml/model/attribute_spec.rb +37 -1
  95. data/spec/lutaml/model/cdata_spec.rb +9 -9
  96. data/spec/lutaml/model/collection_spec.rb +50 -2
  97. data/spec/lutaml/model/comparable_model_spec.rb +92 -27
  98. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
  99. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
  100. data/spec/lutaml/model/defaults_spec.rb +1 -1
  101. data/spec/lutaml/model/enum_spec.rb +1 -1
  102. data/spec/lutaml/model/group_spec.rb +333 -20
  103. data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
  104. data/spec/lutaml/model/json_adapter_spec.rb +6 -6
  105. data/spec/lutaml/model/key_value_mapping_spec.rb +65 -3
  106. data/spec/lutaml/model/mixed_content_spec.rb +24 -24
  107. data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
  108. data/spec/lutaml/model/ordered_content_spec.rb +6 -6
  109. data/spec/lutaml/model/polymorphic_spec.rb +526 -0
  110. data/spec/lutaml/model/render_empty_spec.rb +194 -0
  111. data/spec/lutaml/model/render_nil_spec.rb +206 -22
  112. data/spec/lutaml/model/root_mappings_spec.rb +3 -3
  113. data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
  114. data/spec/lutaml/model/serializable_spec.rb +179 -103
  115. data/spec/lutaml/model/simple_model_spec.rb +9 -9
  116. data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
  117. data/spec/lutaml/model/toml_spec.rb +51 -0
  118. data/spec/lutaml/model/transformation_spec.rb +72 -15
  119. data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
  120. data/spec/lutaml/model/value_map_spec.rb +240 -0
  121. data/spec/lutaml/model/xml/namespace/nested_with_explicit_namespace_spec.rb +85 -0
  122. data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
  123. data/spec/lutaml/model/xml/xml_element_spec.rb +93 -0
  124. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
  125. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
  126. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
  127. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  128. data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
  129. data/spec/lutaml/model/xml_mapping_rule_spec.rb +105 -5
  130. data/spec/lutaml/model/xml_mapping_spec.rb +70 -16
  131. data/spec/lutaml/model/xml_spec.rb +63 -0
  132. data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
  133. data/spec/sample_model_spec.rb +3 -3
  134. data/spec/spec_helper.rb +3 -3
  135. metadata +76 -59
  136. data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
  137. data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
  138. data/lib/lutaml/model/loggable.rb +0 -15
  139. data/lib/lutaml/model/mapping_rule.rb +0 -109
  140. data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
  141. data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
  142. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
  143. data/lib/lutaml/model/toml_adapter.rb +0 -6
  144. data/lib/lutaml/model/xml_mapping.rb +0 -307
  145. data/lib/lutaml/model/xml_mapping_rule.rb +0 -122
  146. data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
  147. data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
  148. data/lib/lutaml/model/yaml_adapter.rb +0 -8
@@ -62,26 +62,26 @@ module SimpleModel
62
62
  xml do
63
63
  root "building"
64
64
  map_attribute "name", to: :name
65
- map_element "address", to: :address
66
- map_element "room", to: :rooms
65
+ map_element "address", to: :address, treat_omitted: :omited
66
+ map_element "room", to: :rooms, treat_omitted: :omited
67
67
  end
68
68
 
69
69
  yaml do
70
70
  map "name", to: :name
71
- map "address", to: :address
72
- map "rooms", to: :rooms
71
+ map "address", to: :address, treat_omitted: :omited
72
+ map "rooms", to: :rooms, treat_omitted: :omited
73
73
  end
74
74
 
75
75
  json do
76
76
  map "name", to: :name
77
- map "address", to: :address
78
- map "rooms", to: :rooms
77
+ map "address", to: :address, treat_omitted: :omited
78
+ map "rooms", to: :rooms, treat_omitted: :omited
79
79
  end
80
80
 
81
81
  toml do
82
82
  map "name", to: :name
83
- map "address", to: :address
84
- map "rooms", to: :rooms
83
+ map "address", to: :address, treat_omitted: :omited
84
+ map "rooms", to: :rooms, treat_omitted: :omited
85
85
  end
86
86
  end
87
87
  end
@@ -182,7 +182,7 @@ RSpec.describe SimpleModel do
182
182
  default_model = SimpleModel::Building.new
183
183
  expect(default_model.name).to eq("Unnamed building")
184
184
  expect(default_model.address).to be_nil
185
- expect(default_model.rooms).to eq([])
185
+ expect(default_model.rooms).to be_nil
186
186
  end
187
187
 
188
188
  it "serializes to XML" do
@@ -1,6 +1,6 @@
1
1
  require "spec_helper"
2
- require "lutaml/model/toml_adapter/toml_rb_adapter"
3
- require "lutaml/model/toml_adapter/tomlib_adapter"
2
+ require "lutaml/model/toml/toml_rb_adapter"
3
+ require "lutaml/model/toml/tomlib_adapter"
4
4
  require_relative "../../fixtures/sample_model"
5
5
 
6
6
  RSpec.describe "TomlAdapter" do
@@ -9,9 +9,9 @@ RSpec.describe "TomlAdapter" do
9
9
  let(:model) { SampleModel.new(attributes) }
10
10
 
11
11
  let(:expected_toml) do
12
- if adapter_class == Lutaml::Model::TomlAdapter::TomlRbAdapter
12
+ if adapter_class == Lutaml::Model::Toml::TomlRbAdapter
13
13
  TomlRB.dump(attributes)
14
- elsif adapter_class == Lutaml::Model::TomlAdapter::TomlibAdapter
14
+ elsif adapter_class == Lutaml::Model::Toml::TomlibAdapter
15
15
  Tomlib.dump(attributes)
16
16
  end
17
17
  end
@@ -31,11 +31,11 @@ RSpec.describe "TomlAdapter" do
31
31
  end
32
32
  end
33
33
 
34
- describe Lutaml::Model::TomlAdapter::TomlRbAdapter do
34
+ describe Lutaml::Model::Toml::TomlRbAdapter do
35
35
  it_behaves_like "a TOML adapter", described_class
36
36
  end
37
37
 
38
- describe Lutaml::Model::TomlAdapter::TomlibAdapter do
38
+ describe Lutaml::Model::Toml::TomlibAdapter do
39
39
  it_behaves_like "a TOML adapter", described_class
40
40
  end
41
41
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/model/toml"
5
+
6
+ RSpec.describe Lutaml::Model::Toml do
7
+ describe ".detect_toml_adapter" do
8
+ before do
9
+ # Hide any existing constants first
10
+ hide_const("Tomlib") if Object.const_defined?(:Tomlib)
11
+ hide_const("TomlRb") if Object.const_defined?(:TomlRb)
12
+ end
13
+
14
+ context "when Tomlib is available" do
15
+ before do
16
+ stub_const("Tomlib", Module.new)
17
+ end
18
+
19
+ it "returns :tomlib" do
20
+ expect(described_class.detect_toml_adapter).to eq(:tomlib)
21
+ end
22
+ end
23
+
24
+ context "when TomlRb is available" do
25
+ before do
26
+ stub_const("TomlRb", Module.new)
27
+ end
28
+
29
+ it "returns :toml_rb" do
30
+ expect(described_class.detect_toml_adapter).to eq(:toml_rb)
31
+ end
32
+ end
33
+
34
+ context "when neither adapter is available" do
35
+ it "returns nil" do
36
+ expect(described_class.detect_toml_adapter).to be_nil
37
+ end
38
+ end
39
+
40
+ context "when both adapters are available" do
41
+ before do
42
+ stub_const("Tomlib", Module.new)
43
+ stub_const("TomlRb", Module.new)
44
+ end
45
+
46
+ it "prefers Tomlib" do
47
+ expect(described_class.detect_toml_adapter).to eq(:tomlib)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -56,7 +56,7 @@ module TransformationSpec
56
56
  import: ->(value) { value.to_s.downcase },
57
57
  }
58
58
  attribute :email, :string, transform: {
59
- export: lambda(&:downcase),
59
+ export: lambda(&:upcase),
60
60
  import: lambda(&:downcase),
61
61
  }
62
62
  attribute :tags, :string, collection: true, transform: {
@@ -82,13 +82,36 @@ module TransformationSpec
82
82
  export: ->(value) { "Prof. #{value}" },
83
83
  import: ->(value) { value.gsub("Prof. ", "") },
84
84
  }
85
- map_element "contact-email", to: :email, transform: {
85
+ map_attribute "contact-email", to: :email, transform: {
86
86
  export: ->(value) { "contact+#{value}" },
87
87
  import: ->(value) { value.gsub("contact+", "") },
88
88
  }
89
89
  map_element "skills", to: :tags
90
90
  end
91
91
  end
92
+
93
+ class RoundTripTransformations < Lutaml::Model::Serializable
94
+ attribute :number, :string, transform: {
95
+ import: ->(value) { (value.to_f + 1).to_s },
96
+ export: ->(value) { (value.to_f - 1).to_s },
97
+ }
98
+
99
+ json do
100
+ map "number", to: :number, transform: {
101
+ import: ->(value) { ((value.to_f * 10) + 1).to_s },
102
+ export: ->(value) { ((value.to_f - 1) / 10.0).to_s },
103
+ }
104
+ end
105
+
106
+ xml do
107
+ root "RoundTripTransformations"
108
+
109
+ map_element "number", to: :number, transform: {
110
+ import: ->(value) { ((value.to_f * 10) + 1).to_s },
111
+ export: ->(value) { ((value.to_f - 1) / 10.0).to_s },
112
+ }
113
+ end
114
+ end
92
115
  end
93
116
 
94
117
  RSpec.describe "Value Transformations" do
@@ -147,6 +170,18 @@ RSpec.describe "Value Transformations" do
147
170
  )
148
171
  end
149
172
 
173
+ let(:json) do
174
+ {
175
+ "fullName" => "Dr. bob",
176
+ "emailAddress" => "bobattest.com",
177
+ "labels" => "senior|lead",
178
+ }.to_json
179
+ end
180
+
181
+ let(:parsed) do
182
+ TransformationSpec::MappingTransformPerson.from_json(json)
183
+ end
184
+
150
185
  let(:expected_xml) do
151
186
  <<~XML
152
187
  <person>
@@ -165,16 +200,15 @@ RSpec.describe "Value Transformations" do
165
200
  expect(parsed["labels"]).to eq("developer-|-architect")
166
201
  end
167
202
 
168
- it "applies mapping transformations during JSON deserialization" do
169
- json = {
170
- "fullName" => "Dr. bob",
171
- "emailAddress" => "bobattest.com",
172
- "labels" => "senior|lead",
173
- }.to_json
174
-
175
- parsed = TransformationSpec::MappingTransformPerson.from_json(json)
203
+ it "correctly deserialize name from JSON" do
176
204
  expect(parsed.name).to eq("Dr. bob")
205
+ end
206
+
207
+ it "correctly deserialize email from JSON" do
177
208
  expect(parsed.email).to eq("bob@test.com")
209
+ end
210
+
211
+ it "correctly deserialize tags from JSON" do
178
212
  expect(parsed.tags).to eq(["senior", "lead"])
179
213
  end
180
214
 
@@ -195,9 +229,8 @@ RSpec.describe "Value Transformations" do
195
229
 
196
230
  let(:expected_xml) do
197
231
  <<~XML
198
- <person>
199
- <full-name>Prof. carol</full-name>
200
- <contact-email>contact+CAROL@TEST.COM</contact-email>
232
+ <person contact-email="contact+CAROL@TEST.COM">
233
+ <full-name>Prof. Carol</full-name>
201
234
  <skills>MANAGER-1</skills>
202
235
  <skills>AGILE-1</skills>
203
236
  </person>
@@ -208,7 +241,7 @@ RSpec.describe "Value Transformations" do
208
241
  json = combined_person.to_json
209
242
  parsed = JSON.parse(json)
210
243
 
211
- expect(parsed["fullName"]).to eq("Prof. carol")
244
+ expect(parsed["fullName"]).to eq("Prof. Carol")
212
245
  expect(parsed["contactEmail"]).to eq("contact+CAROL@TEST.COM")
213
246
  end
214
247
 
@@ -217,7 +250,7 @@ RSpec.describe "Value Transformations" do
217
250
  json = combined_person.to_json
218
251
  parsed_json = TransformationSpec::CombinedTransformPerson.from_json(json)
219
252
  expect(parsed_json.name).to eq("carol")
220
- expect(parsed_json.email).to eq("CAROL@TEST.COM")
253
+ expect(parsed_json.email).to eq("carol@test.com")
221
254
  expect(parsed_json.tags).to eq(["MANAGER-1", "AGILE-1"])
222
255
  end
223
256
 
@@ -227,4 +260,28 @@ RSpec.describe "Value Transformations" do
227
260
  expect(parsed_xml.to_xml).to be_equivalent_to(expected_xml)
228
261
  end
229
262
  end
263
+
264
+ describe "Custom Transformations all mappings applied" do
265
+ let(:xml) do
266
+ <<~XML
267
+ <RoundTripTransformations>
268
+ <number>10.0</number>
269
+ </RoundTripTransformations>
270
+ XML
271
+ end
272
+
273
+ let(:json) do
274
+ { number: "10.0" }.to_json
275
+ end
276
+
277
+ it "correctly round trips XML" do
278
+ parsed = TransformationSpec::RoundTripTransformations.from_xml(xml)
279
+ expect(parsed.to_xml).to be_equivalent_to(xml)
280
+ end
281
+
282
+ it "correctly round trips JSON" do
283
+ parsed = TransformationSpec::RoundTripTransformations.from_json(json)
284
+ expect(parsed.to_json).to be_equivalent_to(json)
285
+ end
286
+ end
230
287
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Lutaml::Model::UninitializedClass do
4
+ subject(:uninitialized) { described_class.instance }
5
+
6
+ describe "#to_s" do
7
+ it "returns self" do
8
+ expect(uninitialized.to_s).to eq(uninitialized)
9
+ end
10
+ end
11
+
12
+ describe "#inspect" do
13
+ it "returns 'uninitialized'" do
14
+ expect(uninitialized.inspect).to eq("uninitialized")
15
+ end
16
+ end
17
+
18
+ describe "#uninitialized?" do
19
+ it "returns true" do
20
+ expect(uninitialized.uninitialized?).to be true
21
+ end
22
+ end
23
+
24
+ describe "#match?" do
25
+ it "returns false for any argument" do
26
+ expect(uninitialized).not_to match /pattern/
27
+ expect(uninitialized).not_to match "string"
28
+ expect(uninitialized).not_to match nil
29
+ end
30
+ end
31
+
32
+ describe "#include?" do
33
+ it "returns false for any argument" do
34
+ expect(uninitialized).not_to include "substring"
35
+ expect(uninitialized).not_to include nil
36
+ end
37
+ end
38
+
39
+ describe "#gsub" do
40
+ it "returns self regardless of arguments" do
41
+ expect(uninitialized.gsub("pattern", "replacement")).to eq(uninitialized)
42
+ expect(uninitialized.gsub("pattern", "replacement")).to eq(uninitialized)
43
+ end
44
+ end
45
+
46
+ describe "#to_yaml" do
47
+ it "returns nil" do
48
+ expect(uninitialized.to_yaml).to be_nil
49
+ end
50
+ end
51
+
52
+ describe "#to_f" do
53
+ it "returns self" do
54
+ expect(uninitialized.to_f).to eq(uninitialized)
55
+ end
56
+ end
57
+
58
+ describe "#size" do
59
+ it "returns 0" do
60
+ expect(uninitialized.size).to eq(0)
61
+ end
62
+ end
63
+
64
+ describe "#encoding" do
65
+ it "returns the default string encoding" do
66
+ expect(uninitialized.encoding).to eq("".encoding)
67
+ end
68
+ end
69
+
70
+ describe "method_missing" do
71
+ context "when method ends with '?'" do
72
+ it "returns false" do
73
+ expect(uninitialized).not_to be_empty
74
+ expect(uninitialized).not_to be_nil
75
+ expect(uninitialized).not_to be_blank
76
+ end
77
+ end
78
+
79
+ context "when method doesn't end with '?'" do
80
+ it "raises NoMethodError" do
81
+ expect { uninitialized.unknown_method }.to raise_error(NoMethodError)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "#respond_to_missing?" do
87
+ it "returns true for methods ending with ?" do
88
+ expect(uninitialized.respond_to?(:empty?)).to be true
89
+ expect(uninitialized.respond_to?(:nil?)).to be true
90
+ end
91
+
92
+ it "returns false for methods not ending with ?" do
93
+ expect(uninitialized.respond_to?(:unknown_method)).to be false
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,240 @@
1
+ # map :x, to: :y, value_map: {
2
+ # from_xml: {omitted: :omitted, xsi_nil: nil, empty: [] },
3
+ # to_xml: {omitted: :omitted, nil: :nil, empty: :empty}
4
+ # }
5
+
6
+ module ValueMapSpec
7
+ class WithValueMaps < Lutaml::Model::Serializable
8
+ attribute :omitted_as_omitted, :string
9
+ attribute :omitted_as_nil, :string
10
+ attribute :omitted_as_empty, :string
11
+
12
+ attribute :nil_as_nil, :string
13
+ attribute :nil_as_omitted, :string
14
+ attribute :nil_as_empty, :string
15
+
16
+ attribute :empty_as_empty, :string
17
+ attribute :empty_as_nil, :string
18
+ attribute :empty_as_omitted, :string
19
+
20
+ xml do
21
+ root "WithValueMaps"
22
+
23
+ map_element "omittedAsOmitted", to: :omitted_as_omitted, value_map: {
24
+ from: { omitted: :omitted },
25
+ to: { omitted: :omitted },
26
+ }
27
+ map_element "omittedAsNil", to: :omitted_as_nil, value_map: {
28
+ from: { omitted: :nil },
29
+ to: { omitted: :nil },
30
+ }
31
+ map_element "omittedAsEmpty", to: :omitted_as_empty, value_map: {
32
+ from: { omitted: :empty },
33
+ to: { omitted: :empty },
34
+ }
35
+
36
+ map_element "nilAsNil", to: :nil_as_nil, value_map: {
37
+ from: { nil: :nil },
38
+ to: { nil: :nil },
39
+ }
40
+ map_element "nilAsOmitted", to: :nil_as_omitted, value_map: {
41
+ from: { nil: :omitted },
42
+ to: { nil: :omitted },
43
+ }
44
+ map_element "nilAsEmpty", to: :nil_as_empty, value_map: {
45
+ from: { nil: :empty },
46
+ to: { nil: :empty },
47
+ }
48
+
49
+ map_element "emptyAsEmpty", to: :empty_as_empty, value_map: {
50
+ from: { empty: :empty },
51
+ to: { empty: :empty },
52
+ }
53
+ map_element "emptyAsNil", to: :empty_as_nil, value_map: {
54
+ from: { empty: :nil },
55
+ to: { empty: :nil },
56
+ }
57
+ map_element "emptyAsOmitted", to: :empty_as_omitted, value_map: {
58
+ from: { empty: :omitted },
59
+ to: { empty: :omitted },
60
+ }
61
+ end
62
+
63
+ key_value do
64
+ map "omitted_as_omitted", to: :omitted_as_omitted, value_map: {
65
+ from: { omitted: :omitted },
66
+ to: { omitted: :omitted },
67
+ }
68
+ map "omitted_as_nil", to: :omitted_as_nil, value_map: {
69
+ from: { omitted: :omitted },
70
+ to: { omitted: :nil },
71
+ }
72
+ map "omitted_as_empty", to: :omitted_as_empty, value_map: {
73
+ from: { omitted: :omitted },
74
+ to: { omitted: :empty },
75
+ }
76
+
77
+ map "nil_as_nil", to: :nil_as_nil, value_map: {
78
+ from: { nil: :nil },
79
+ to: { nil: :nil },
80
+ }
81
+ map "nil_as_omitted", to: :nil_as_omitted, value_map: {
82
+ from: { nil: :nil },
83
+ to: { nil: :omitted },
84
+ }
85
+ map "nil_as_empty", to: :nil_as_empty, value_map: {
86
+ from: { nil: :nil },
87
+ to: { nil: :empty },
88
+ }
89
+
90
+ map "empty_as_empty", to: :empty_as_empty, value_map: {
91
+ from: { empty: :empty },
92
+ to: { empty: :empty },
93
+ }
94
+ map "empty_as_nil", to: :empty_as_nil, value_map: {
95
+ from: { empty: :empty },
96
+ to: { empty: :nil },
97
+ }
98
+ map "empty_as_omitted", to: :empty_as_omitted, value_map: {
99
+ from: { empty: :empty },
100
+ to: { empty: :omitted },
101
+ }
102
+ end
103
+ end
104
+ end
105
+
106
+ RSpec.describe "ValueMap" do
107
+ describe "YAML" do
108
+ let(:model) do
109
+ uninitialized = Lutaml::Model::UninitializedClass.instance
110
+
111
+ ValueMapSpec::WithValueMaps.new(
112
+ omitted_as_omitted: uninitialized,
113
+ omitted_as_nil: uninitialized,
114
+ omitted_as_empty: uninitialized,
115
+ nil_as_nil: nil,
116
+ nil_as_omitted: nil,
117
+ nil_as_empty: nil,
118
+ empty_as_empty: "",
119
+ empty_as_nil: "",
120
+ empty_as_omitted: "",
121
+ )
122
+ end
123
+
124
+ let(:parsed) { ValueMapSpec::WithValueMaps.from_yaml(yaml) }
125
+
126
+ let(:expected_yaml) do
127
+ <<~YAML
128
+ ---
129
+ omitted_as_nil:
130
+ omitted_as_empty: ''
131
+ nil_as_nil:
132
+ nil_as_empty: ''
133
+ empty_as_empty: ''
134
+ empty_as_nil:
135
+ YAML
136
+ end
137
+
138
+ let(:yaml) do
139
+ <<~YAML
140
+ ---
141
+ nil_as_nil:
142
+ nil_as_omitted:
143
+ nil_as_empty:
144
+ empty_as_empty: ''
145
+ empty_as_nil: ''
146
+ empty_as_omitted: ''
147
+ YAML
148
+ end
149
+
150
+ it "serializes correctly" do
151
+ expect(parsed.to_yaml).to eq(expected_yaml)
152
+ end
153
+
154
+ it "deserializes correctly" do
155
+ expected = ValueMapSpec::WithValueMaps.new(
156
+ {
157
+ nil_as_nil: nil,
158
+ nil_as_omitted: nil,
159
+ nil_as_empty: nil,
160
+ empty_as_empty: "",
161
+ empty_as_nil: "",
162
+ empty_as_omitted: "",
163
+ },
164
+ { omitted: :omitted },
165
+ )
166
+
167
+ expect(parsed).to eq(expected)
168
+ end
169
+ end
170
+
171
+ describe "XML" do
172
+ context "when serializing" do
173
+ let(:model) do
174
+ ValueMapSpec::WithValueMaps.new(
175
+ {
176
+ nil_as_nil: nil,
177
+ nil_as_omitted: nil,
178
+ nil_as_empty: nil,
179
+ empty_as_empty: "",
180
+ empty_as_nil: "",
181
+ empty_as_omitted: "",
182
+ },
183
+ { omitted: :omitted },
184
+ )
185
+ end
186
+
187
+ let(:expected_xml) do
188
+ <<~XML.strip
189
+ <WithValueMaps>
190
+ <omittedAsNil xsi:nil="true"/>
191
+ <omittedAsEmpty/>
192
+ <nilAsNil xsi:nil="true"/>
193
+ <nilAsEmpty/>
194
+ <emptyAsEmpty/>
195
+ <emptyAsNil xsi:nil="true"/>
196
+ </WithValueMaps>
197
+ XML
198
+ end
199
+
200
+ it "sets correct values when serializing" do
201
+ expect(model.to_xml).to eq(expected_xml)
202
+ end
203
+ end
204
+
205
+ context "when deserializing" do
206
+ let(:xml) do
207
+ <<~XML
208
+ <WithValueMaps>
209
+ <nilAsNil xsi:nil="true" />
210
+ <nilAsOmitted xsi:nil="true" />
211
+ <nilAsEmpty xsi:nil="true" />
212
+ <emptyAsEmpty/>
213
+ <emptyAsNil/>
214
+ <emptyAsOmitted/>
215
+ </WithValueMaps>
216
+ XML
217
+ end
218
+
219
+ let(:parsed) { ValueMapSpec::WithValueMaps.from_xml(xml) }
220
+
221
+ let(:expected) do
222
+ ValueMapSpec::WithValueMaps.new(
223
+ {
224
+ nil_as_nil: nil,
225
+ nil_as_empty: "",
226
+ empty_as_empty: "",
227
+ empty_as_nil: nil,
228
+ omitted_as_nil: nil,
229
+ omitted_as_empty: "",
230
+ },
231
+ { omitted: :omitted },
232
+ )
233
+ end
234
+
235
+ it "sets correct values when deserializing" do
236
+ expect(parsed).to eq(expected)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,85 @@
1
+ # <Base xmlns:test="https://test-namespace">
2
+ # <test:Element1>
3
+ # <test:Element>Value</test:Element>
4
+ # <ElementWithoutNamespace>
5
+ # <NestedElementWithoutNamespace>
6
+ # Value for nested element without namespace
7
+ # </NestedElementWithoutNamespace>
8
+ # </ElementWithoutNamespace>
9
+ # </test:Element1>
10
+ # </Base>
11
+
12
+ module NestedWithExplicitNamespaceSpec
13
+ class NestedElementWithoutNamespace < Lutaml::Model::Serializable
14
+ attribute :nested_element_without_namespace, :string
15
+
16
+ xml do
17
+ map_element "NestedElementWithoutNamespace",
18
+ to: :nested_element_without_namespace
19
+ end
20
+ end
21
+
22
+ class ElementWithoutNamespace < Lutaml::Model::Serializable
23
+ attribute :nested_element_without_namespace, :string
24
+
25
+ xml do
26
+ map_element "NestedElementWithoutNamespace",
27
+ to: :nested_element_without_namespace
28
+ end
29
+ end
30
+
31
+ class Element1 < Lutaml::Model::Serializable
32
+ attribute :element, :string
33
+ attribute :element_without_namespace, ElementWithoutNamespace
34
+
35
+ xml do
36
+ map_element "Element", to: :element
37
+ map_element "ElementWithoutNamespace", to: :element_without_namespace,
38
+ namespace: nil,
39
+ prefix: nil
40
+ end
41
+ end
42
+
43
+ class Base < Lutaml::Model::Serializable
44
+ attribute :element1, Element1
45
+
46
+ xml do
47
+ root "Base"
48
+ namespace "https://test-namespace", "test"
49
+
50
+ map_element "Element1", to: :element1
51
+ end
52
+ end
53
+ end
54
+
55
+ RSpec.describe "NestedWithExplicitNamespace" do
56
+ let(:xml) do
57
+ <<~XML
58
+ <Base xmlns:test="https://test-namespace">
59
+ <test:Element1>
60
+ <test:Element>Value</test:Element>
61
+ <ElementWithoutNamespace>
62
+ <NestedElementWithoutNamespace>
63
+ Value for nested element without namespace
64
+ </NestedElementWithoutNamespace>
65
+ </ElementWithoutNamespace>
66
+ </test:Element1>
67
+ </Base>
68
+ XML
69
+ end
70
+
71
+ let(:parsed) { NestedWithExplicitNamespaceSpec::Base.from_xml(xml) }
72
+
73
+ it "parses namespaced element correctly" do
74
+ expect(parsed.element1.element).to eq("Value")
75
+ end
76
+
77
+ it "parses nested element without namespace correclty" do
78
+ generated_value = parsed.element1
79
+ .element_without_namespace
80
+ .nested_element_without_namespace
81
+ .strip
82
+
83
+ expect(generated_value).to eq("Value for nested element without namespace")
84
+ end
85
+ end