lutaml-model 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +2 -0
  3. data/.rubocop_todo.yml +86 -23
  4. data/Gemfile +2 -0
  5. data/README.adoc +1441 -220
  6. data/lib/lutaml/model/attribute.rb +33 -10
  7. data/lib/lutaml/model/choice.rb +56 -0
  8. data/lib/lutaml/model/config.rb +1 -0
  9. data/lib/lutaml/model/constants.rb +7 -0
  10. data/lib/lutaml/model/error/choice_lower_bound_error.rb +9 -0
  11. data/lib/lutaml/model/error/choice_upper_bound_error.rb +9 -0
  12. data/lib/lutaml/model/error/import_model_with_root_error.rb +9 -0
  13. data/lib/lutaml/model/error/incorrect_sequence_error.rb +9 -0
  14. data/lib/lutaml/model/error/invalid_choice_range_error.rb +20 -0
  15. data/lib/lutaml/model/error/no_root_mapping_error.rb +9 -0
  16. data/lib/lutaml/model/error/no_root_namespace_error.rb +9 -0
  17. data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
  18. data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
  19. data/lib/lutaml/model/error.rb +9 -0
  20. data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
  21. data/lib/lutaml/model/key_value_mapping.rb +34 -3
  22. data/lib/lutaml/model/key_value_mapping_rule.rb +4 -2
  23. data/lib/lutaml/model/liquefiable.rb +59 -0
  24. data/lib/lutaml/model/mapping_hash.rb +9 -1
  25. data/lib/lutaml/model/mapping_rule.rb +19 -2
  26. data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
  27. data/lib/lutaml/model/schema/xml_compiler.rb +762 -0
  28. data/lib/lutaml/model/schema.rb +5 -0
  29. data/lib/lutaml/model/schema_location.rb +7 -0
  30. data/lib/lutaml/model/sequence.rb +71 -0
  31. data/lib/lutaml/model/serialize.rb +139 -33
  32. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
  33. data/lib/lutaml/model/type/decimal.rb +0 -4
  34. data/lib/lutaml/model/type/hash.rb +11 -11
  35. data/lib/lutaml/model/type/time.rb +3 -3
  36. data/lib/lutaml/model/utils.rb +19 -15
  37. data/lib/lutaml/model/validation.rb +12 -1
  38. data/lib/lutaml/model/version.rb +1 -1
  39. data/lib/lutaml/model/xml_adapter/builder/oga.rb +10 -7
  40. data/lib/lutaml/model/xml_adapter/builder/ox.rb +20 -13
  41. data/lib/lutaml/model/xml_adapter/element.rb +32 -0
  42. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +13 -9
  43. data/lib/lutaml/model/xml_adapter/oga/element.rb +14 -13
  44. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +86 -19
  45. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +19 -15
  46. data/lib/lutaml/model/xml_adapter/xml_document.rb +82 -25
  47. data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
  48. data/lib/lutaml/model/xml_mapping.rb +53 -9
  49. data/lib/lutaml/model/xml_mapping_rule.rb +8 -6
  50. data/lib/lutaml/model.rb +2 -0
  51. data/lutaml-model.gemspec +5 -0
  52. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +75 -0
  53. data/spec/ceramic_spec.rb +39 -0
  54. data/spec/fixtures/ceramic.rb +23 -0
  55. data/spec/fixtures/xml/address_example_260.xsd +9 -0
  56. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  57. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  58. data/spec/fixtures/xml/test_schema.xsd +53 -0
  59. data/spec/fixtures/xml/user.xsd +10 -0
  60. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  61. data/spec/lutaml/model/cdata_spec.rb +4 -5
  62. data/spec/lutaml/model/choice_spec.rb +168 -0
  63. data/spec/lutaml/model/collection_spec.rb +1 -1
  64. data/spec/lutaml/model/custom_model_spec.rb +7 -21
  65. data/spec/lutaml/model/custom_serialization_spec.rb +74 -2
  66. data/spec/lutaml/model/defaults_spec.rb +3 -1
  67. data/spec/lutaml/model/delegation_spec.rb +7 -5
  68. data/spec/lutaml/model/enum_spec.rb +35 -0
  69. data/spec/lutaml/model/group_spec.rb +160 -0
  70. data/spec/lutaml/model/inheritance_spec.rb +25 -0
  71. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  72. data/spec/lutaml/model/liquefiable_spec.rb +121 -0
  73. data/spec/lutaml/model/map_all_spec.rb +188 -0
  74. data/spec/lutaml/model/mixed_content_spec.rb +95 -56
  75. data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
  76. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1624 -0
  77. data/spec/lutaml/model/sequence_spec.rb +216 -0
  78. data/spec/lutaml/model/transformation_spec.rb +230 -0
  79. data/spec/lutaml/model/type_spec.rb +138 -31
  80. data/spec/lutaml/model/utils_spec.rb +32 -0
  81. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  82. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
  83. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  84. data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
  85. data/spec/lutaml/model/xml_mapping_spec.rb +250 -112
  86. metadata +77 -2
@@ -16,11 +16,13 @@ module Lutaml
16
16
  :namespace_uri,
17
17
  :namespace_prefix,
18
18
  :mixed_content,
19
- :ordered
19
+ :ordered,
20
+ :element_sequence
20
21
 
21
22
  def initialize
22
23
  @elements = {}
23
24
  @attributes = {}
25
+ @element_sequence = []
24
26
  @content_mapping = nil
25
27
  @raw_mapping = nil
26
28
  @mixed_content = false
@@ -35,6 +37,18 @@ module Lutaml
35
37
  @ordered = ordered || mixed # mixed contenet will always be ordered
36
38
  end
37
39
 
40
+ def root?
41
+ !!root_element
42
+ end
43
+
44
+ def no_root
45
+ @no_root = true
46
+ end
47
+
48
+ def no_root?
49
+ !!@no_root
50
+ end
51
+
38
52
  def prefixed_root
39
53
  if namespace_uri && namespace_prefix
40
54
  "#{namespace_prefix}:#{root_element}"
@@ -44,6 +58,8 @@ module Lutaml
44
58
  end
45
59
 
46
60
  def namespace(uri, prefix = nil)
61
+ raise Lutaml::Model::NoRootNamespaceError if no_root?
62
+
47
63
  @namespace_uri = uri
48
64
  @namespace_prefix = prefix
49
65
  end
@@ -60,7 +76,8 @@ module Lutaml
60
76
  namespace: (namespace_set = false
61
77
  nil),
62
78
  prefix: (prefix_set = false
63
- nil)
79
+ nil),
80
+ transform: {}
64
81
  )
65
82
  validate!(name, to, with, type: TYPES[:element])
66
83
 
@@ -77,6 +94,7 @@ module Lutaml
77
94
  prefix: prefix,
78
95
  namespace_set: namespace_set != false,
79
96
  prefix_set: prefix_set != false,
97
+ transform: transform,
80
98
  )
81
99
  @elements[rule.namespaced_name] = rule
82
100
  end
@@ -149,10 +167,10 @@ module Lutaml
149
167
  prefix: (prefix_set = false
150
168
  nil)
151
169
  )
152
- validate!("__raw_mapping", to, with, type: TYPES[:all_content])
170
+ validate!(Constants::RAW_MAPPING_KEY, to, with, type: TYPES[:all_content])
153
171
 
154
172
  rule = XmlMappingRule.new(
155
- "__raw_mapping",
173
+ Constants::RAW_MAPPING_KEY,
156
174
  to: to,
157
175
  render_nil: render_nil,
158
176
  render_default: render_default,
@@ -168,6 +186,23 @@ module Lutaml
168
186
  @raw_mapping = rule
169
187
  end
170
188
 
189
+ alias map_all_content map_all
190
+
191
+ def sequence(&block)
192
+ @element_sequence << Sequence.new(self).tap do |s|
193
+ s.instance_eval(&block)
194
+ end
195
+ end
196
+
197
+ def import_model_mappings(model)
198
+ raise Lutaml::Model::ImportModelWithRootError.new(model) if model.root?
199
+
200
+ mappings = model.mappings_for(:xml)
201
+ @elements.merge!(mappings.instance_variable_get(:@elements))
202
+ @attributes.merge!(mappings.instance_variable_get(:@attributes))
203
+ (@element_sequence << mappings.element_sequence).flatten!
204
+ end
205
+
171
206
  def validate!(key, to, with, type: nil)
172
207
  validate_mappings!(type)
173
208
 
@@ -241,14 +276,23 @@ module Lutaml
241
276
  ordered: @ordered)
242
277
  xml_mapping.namespace(@namespace_uri.dup, @namespace_prefix.dup)
243
278
 
244
- xml_mapping.instance_variable_set(:@attributes,
245
- dup_mappings(@attributes))
246
- xml_mapping.instance_variable_set(:@elements, dup_mappings(@elements))
247
- xml_mapping.instance_variable_set(:@content_mapping,
248
- @content_mapping&.deep_dup)
279
+ attributes_to_dup.each do |var_name|
280
+ value = instance_variable_get(var_name)
281
+ xml_mapping.instance_variable_set(var_name, Utils.deep_dup(value))
282
+ end
249
283
  end
250
284
  end
251
285
 
286
+ def attributes_to_dup
287
+ @attributes_to_dup ||= %i[
288
+ @content_mapping
289
+ @raw_mapping
290
+ @element_sequence
291
+ @attributes
292
+ @elements
293
+ ]
294
+ end
295
+
252
296
  def dup_mappings(mappings)
253
297
  new_mappings = {}
254
298
 
@@ -19,7 +19,8 @@ module Lutaml
19
19
  namespace_set: false,
20
20
  prefix_set: false,
21
21
  attribute: false,
22
- default_namespace: nil
22
+ default_namespace: nil,
23
+ transform: {}
23
24
  )
24
25
  super(
25
26
  name,
@@ -28,7 +29,8 @@ module Lutaml
28
29
  render_default: render_default,
29
30
  with: with,
30
31
  delegate: delegate,
31
- attribute: attribute
32
+ attribute: attribute,
33
+ transform: transform
32
34
  )
33
35
 
34
36
  @namespace = if namespace.to_s == "inherit"
@@ -59,10 +61,6 @@ module Lutaml
59
61
  name.nil?
60
62
  end
61
63
 
62
- def raw_mapping?
63
- name == "__raw_mapping"
64
- end
65
-
66
64
  def content_key
67
65
  cdata ? "#cdata-section" : "text"
68
66
  end
@@ -105,14 +103,18 @@ module Lutaml
105
103
  name.dup,
106
104
  to: to,
107
105
  render_nil: render_nil,
106
+ render_default: render_default,
108
107
  with: Utils.deep_dup(custom_methods),
109
108
  delegate: delegate,
110
109
  namespace: namespace.dup,
111
110
  prefix: prefix.dup,
112
111
  mixed_content: mixed_content,
112
+ cdata: cdata,
113
113
  namespace_set: namespace_set?,
114
114
  prefix_set: prefix_set?,
115
+ attribute: attribute,
115
116
  default_namespace: default_namespace.dup,
117
+ transform: transform.dup,
116
118
  )
117
119
  end
118
120
  end
data/lib/lutaml/model.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "moxml"
3
4
  require_relative "model/version"
4
5
  require_relative "model/loggable"
5
6
  require_relative "model/type"
@@ -10,6 +11,7 @@ require_relative "model/yaml_adapter/standard_yaml_adapter"
10
11
  require_relative "model/xml_adapter"
11
12
  require_relative "model/toml_adapter"
12
13
  require_relative "model/error"
14
+ require_relative "model/constants"
13
15
 
14
16
  module Lutaml
15
17
  module Model
data/lutaml-model.gemspec CHANGED
@@ -30,6 +30,11 @@ Gem::Specification.new do |spec|
30
30
  end
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
 
33
+ # TODO: remove once https://github.com/Shopify/liquid/issues/1772 is fixed
34
+ # needed for liquid with ruby 3.4
35
+ spec.add_dependency "base64"
36
+ spec.add_dependency "liquid", "~> 5"
37
+ spec.add_dependency "moxml", ">= 0.1.2"
33
38
  spec.add_dependency "thor"
34
39
  spec.metadata["rubygems_mfa_required"] = "true"
35
40
  end
@@ -0,0 +1,75 @@
1
+ require "benchmark"
2
+ require "benchmark/ips"
3
+ require "lutaml/model"
4
+ require "lutaml/model/xml_adapter/oga_adapter"
5
+
6
+ RSpec.describe "LutaML Model Performance" do
7
+ after do
8
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
9
+ end
10
+
11
+ let(:large_xml) do
12
+ xml = "<root>\n"
13
+ 1000.times do |i|
14
+ xml += "<item id='#{i}'><name>Test #{i}</name><value>#{i}</value></item>\n"
15
+ end
16
+ xml += "</root>"
17
+ xml
18
+ end
19
+
20
+ class DeserializerItem < Lutaml::Model::Serializable
21
+ attribute :id, :integer
22
+ attribute :name, :string
23
+ attribute :value, :integer
24
+
25
+ xml do
26
+ map_attribute "id", to: :id
27
+ map_element "value", to: :value
28
+ map_element "name", to: :name
29
+ map_element "value", to: :value
30
+ end
31
+ end
32
+
33
+ class Deserializer < Lutaml::Model::Serializable
34
+ attribute :item, DeserializerItem, collection: true
35
+
36
+ xml do
37
+ root "root"
38
+ map_element "item", to: :item
39
+ end
40
+ end
41
+
42
+ it "measures parsing performance across adapters" do
43
+ report = Benchmark.ips do |x|
44
+ x.config(time: 5, warmup: 2)
45
+
46
+ x.report("Nokogiri Adapter") do
47
+ Deserializer.from_xml(large_xml)
48
+ end
49
+
50
+ x.report("Ox Adapter") do
51
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
52
+ Deserializer.from_xml(large_xml)
53
+ end
54
+
55
+ x.report("Oga Adapter") do
56
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
57
+ Deserializer.from_xml(large_xml)
58
+ end
59
+
60
+ x.compare!
61
+ end
62
+
63
+ thresholds = {
64
+ "Nokogiri Adapter" => 5,
65
+ "Ox Adapter" => 15,
66
+ "Oga Adapter" => 5,
67
+ }
68
+
69
+ report.entries.each do |entry|
70
+ puts "#{entry.label} performance: #{entry.ips.round(2)} ips"
71
+ expect(entry.ips).to be >= thresholds[entry.label],
72
+ "#{entry.label} performance below threshold: got #{entry.ips.round(2)} ips, expected >= #{thresholds[entry.label]} ips"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+ require_relative "fixtures/ceramic"
3
+
4
+ RSpec.describe Ceramic do
5
+ xml = <<~XML
6
+ <ceramic kilnFiringTimeAttribute="2012-04-07T01:51:37.112+02:00">
7
+ <kilnFiringTime>2012-04-07T01:51:37.112+02:00</kilnFiringTime>
8
+ </ceramic>
9
+ XML
10
+
11
+ it "deserializes from XML with high-precision date-time" do
12
+ ceramic = described_class.from_xml(xml)
13
+ expect(ceramic.kiln_firing_time.strftime("%Y-%m-%dT%H:%M:%S.%L%:z")).to eq("2012-04-07T01:51:37.112+02:00")
14
+ end
15
+
16
+ it "serializes to XML with high-precision date-time" do
17
+ ceramic = described_class.from_xml(xml)
18
+ expect(ceramic.to_xml).to be_equivalent_to(xml)
19
+ end
20
+
21
+ it "deserializes from JSON with high-precision date-time" do
22
+ json = {
23
+ kilnFiringTime: "2012-04-07T01:51:37+02:00",
24
+ }.to_json
25
+
26
+ ceramic_from_json = described_class.from_json(json)
27
+ expect(ceramic_from_json.kiln_firing_time).to eq(DateTime.new(2012, 4, 7, 1, 51, 37, "+02:00"))
28
+ end
29
+
30
+ it "serializes to JSON with high-precision date-time" do
31
+ ceramic = described_class.from_xml(xml)
32
+ expected_json = {
33
+ kilnFiringTime: "2012-04-07T01:51:37+02:00",
34
+ kilnFiringTimeAttribute: "2012-04-07T01:51:37+02:00",
35
+ }.to_json
36
+
37
+ expect(ceramic.to_json).to eq(expected_json)
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ require "lutaml/model"
2
+
3
+ class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
4
+ def to_xml
5
+ value.strftime("%Y-%m-%dT%H:%M:%S.%L%:z")
6
+ end
7
+ end
8
+
9
+ class Ceramic < Lutaml::Model::Serializable
10
+ attribute :kiln_firing_time, HighPrecisionDateTime
11
+ attribute :kiln_firing_time_attribute, HighPrecisionDateTime
12
+
13
+ xml do
14
+ root "ceramic"
15
+ map_element "kilnFiringTime", to: :kiln_firing_time
16
+ map_attribute "kilnFiringTimeAttribute", to: :kiln_firing_time_attribute
17
+ end
18
+
19
+ json do
20
+ map "kilnFiringTime", to: :kiln_firing_time
21
+ map "kilnFiringTimeAttribute", to: :kiln_firing_time_attribute
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="qualified">
2
+ <xs:element name="Address" type="Address"/>
3
+ <xs:complexType name="Address" mixed="true">
4
+ <xs:sequence>
5
+ <xs:element name="City" type="xs:string" minOccurs="0"/>
6
+ <xs:element name="ZIP" type="xs:string" minOccurs="0"/>
7
+ </xs:sequence>
8
+ </xs:complexType>
9
+ </xs:schema>
@@ -0,0 +1,4 @@
1
+ <MathDocument>
2
+ <Title>Example Title</Title>
3
+ <IPV4Address>Example Address</IPV4Address>
4
+ </MathDocument>
@@ -0,0 +1,56 @@
1
+ <schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math">
2
+ <xs:simpleType name="StringDatatype">
3
+ <xs:annotation>
4
+ <xs:documentation>
5
+ A string data type to be used for the example.
6
+ </xs:documentation>
7
+ </xs:annotation>
8
+ <xs:restriction base="xs:string"></xs:restriction>
9
+ </xs:simpleType>
10
+
11
+ <xs:simpleType name="IPV4AddressDatatype">
12
+ <xs:annotation>
13
+ <xs:documentation>
14
+ An Internet Protocol version 4 address represented using dotted-quad syntax as defined in section 3.2 of RFC2673.
15
+ </xs:documentation>
16
+ </xs:annotation>
17
+ <xs:restriction base="StringDatatype">
18
+ <xs:pattern value="((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]).){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])" />
19
+ </xs:restriction>
20
+ </xs:simpleType>
21
+
22
+ <xs:simpleType name="whiteSpaces">
23
+ <xs:annotation>
24
+ <xs:documentation>
25
+ A non-empty string of Unicode characters with leading and trailing whitespace
26
+ disallowed. Whitespace is: U+9, U+10, U+32 or [ \n\t]+
27
+ </xs:documentation>
28
+ </xs:annotation>
29
+ <xs:restriction base="xs:string">
30
+ <xs:annotation>
31
+ <xs:documentation>
32
+ The 'string' datatype restricts the XSD type by prohibiting leading
33
+ and trailing whitespace, and something (not only whitespace) is required.
34
+ </xs:documentation>
35
+ </xs:annotation>
36
+ <xs:pattern value="\S(.*\S)?">
37
+ <xs:annotation>
38
+ <xs:documentation>
39
+ This pattern ensures that leading and trailing whitespace is
40
+ disallowed. This helps to even the user experience between implementations
41
+ related to whitespace.
42
+ </xs:documentation>
43
+ </xs:annotation>
44
+ </xs:pattern>
45
+ </xs:restriction>
46
+ </xs:simpleType>
47
+
48
+ <xs:complexType name="MathDocument">
49
+ <xs:choice>
50
+ <xs:element name="Title" type="whiteSpaces"/>
51
+ <xs:element name="IPV4Address" type="IPV4AddressDatatype"/>
52
+ </xs:choice>
53
+ </xs:complexType>
54
+
55
+ <xs:element name="MathDocument" type="MathDocument"/>
56
+ </schema>
@@ -0,0 +1,53 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
3
+ <xsd:simpleType name="ST_Integer255">
4
+ <xsd:annotation>
5
+ <xsd:documentation>Integer value (1 to 255)</xsd:documentation>
6
+ </xsd:annotation>
7
+ <xsd:restriction base="xsd:integer">
8
+ <xsd:minInclusive value="1" />
9
+ <xsd:maxInclusive value="255" />
10
+ </xsd:restriction>
11
+ </xsd:simpleType>
12
+ <xsd:complexType name="CT_Integer255">
13
+ <xsd:attribute name="val" type="ST_Integer255" use="required">
14
+ <xsd:annotation>
15
+ <xsd:documentation>Value</xsd:documentation>
16
+ </xsd:annotation>
17
+ </xsd:attribute>
18
+ </xsd:complexType>
19
+ <xsd:simpleType name="ST_Integer2">
20
+ <xsd:annotation>
21
+ <xsd:documentation>Integer value (-2 to 2)</xsd:documentation>
22
+ </xsd:annotation>
23
+ <xsd:restriction base="xsd:integer">
24
+ <xsd:minInclusive value="-2" />
25
+ <xsd:maxInclusive value="2" />
26
+ </xsd:restriction>
27
+ </xsd:simpleType>
28
+ <xsd:complexType name="CT_Integer2">
29
+ <xsd:attribute name="val" type="ST_Integer2" use="required">
30
+ <xsd:annotation>
31
+ <xsd:documentation>Value</xsd:documentation>
32
+ </xsd:annotation>
33
+ </xsd:attribute>
34
+ </xsd:complexType>
35
+ <xsd:element name="MathTest" type="CT_Integer255">
36
+ <xsd:annotation>
37
+ <xsd:documentation>Main Test class</xsd:documentation>
38
+ </xsd:annotation>
39
+ </xsd:element>
40
+ <xsd:element name="MathTest1" type="CT_Integer2">
41
+ <xsd:annotation>
42
+ <xsd:documentation>Main Test class</xsd:documentation>
43
+ </xsd:annotation>
44
+ </xsd:element>
45
+ <xsd:complexType name="CT_MathTest">
46
+ <xsd:group>
47
+ <xsd:sequence>
48
+ <xsd:element ref="MathTest" />
49
+ <xsd:element ref="MathTest1" />
50
+ </xsd:sequence>
51
+ </xsd:group>
52
+ </xsd:complexType>
53
+ </xsd:schema>
@@ -0,0 +1,10 @@
1
+ <schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math">
2
+ <xsd:complexType name="User">
3
+ <xsd:sequence>
4
+ <xsd:element name="id" type="xsd:nonNegativeInteger" minOccurs="1" />
5
+ <xsd:element name="age" type="xsd:unsignedLong" minOccurs="0" />
6
+ <xsd:element name="token" type="xsd:token" minOccurs="0" />
7
+ </xsd:sequence>
8
+ </xsd:complexType>
9
+ <xsd:element name="User" type="User"/>
10
+ </schema>
@@ -0,0 +1,4 @@
1
+ <MathDocument>
2
+ <Title>Example Title</Title>
3
+ <IPV4Address>192.168.1.1</IPV4Address>
4
+ </MathDocument>
@@ -44,7 +44,7 @@ module CDATA
44
44
  end
45
45
 
46
46
  def house_from_xml(model, node)
47
- model.house = node
47
+ model.house = node.children.first.text
48
48
  end
49
49
 
50
50
  def house_to_xml(model, _parent, doc)
@@ -54,7 +54,7 @@ module CDATA
54
54
  end
55
55
 
56
56
  def city_from_xml(model, node)
57
- model.city = node
57
+ model.city = node.children.first.text
58
58
  end
59
59
 
60
60
  def city_to_xml(model, _parent, doc)
@@ -117,9 +117,8 @@ module CDATA
117
117
 
118
118
  def child_from_xml(model, value)
119
119
  model.child_mapper ||= CustomModelChild.new
120
-
121
- model.child_mapper.street = value["street"].text
122
- model.child_mapper.city = value["city"].text
120
+ model.child_mapper.street = value.find_child_by_name("street").text
121
+ model.child_mapper.city = value.find_child_by_name("city").text
123
122
  end
124
123
  end
125
124
 
@@ -0,0 +1,168 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+
4
+ module ChoiceSpec
5
+ class CandidateType < Lutaml::Model::Serializable
6
+ attribute :id, :integer
7
+ attribute :name, :string
8
+
9
+ xml do
10
+ map_attribute "id", to: :id
11
+ map_attribute "name", to: :name
12
+ end
13
+ end
14
+
15
+ class DocumentState < Lutaml::Model::Serializable
16
+ choice(min: 1, max: 3) do
17
+ attribute :signed, :boolean
18
+ attribute :unsigned, :boolean
19
+ attribute :watermarked, :boolean
20
+ attribute :encrypted, :boolean
21
+ end
22
+
23
+ attribute :candidate, CandidateType
24
+
25
+ xml do
26
+ map_element "signed", to: :signed
27
+ map_element "unsigned", to: :unsigned
28
+ map_element "watermarked", to: :watermarked
29
+ map_element "encrypted", to: :encrypted
30
+ map_attribute "candidate", to: :candidate
31
+ end
32
+ end
33
+
34
+ class PersonDetails < Lutaml::Model::Serializable
35
+ choice(min: 1, max: 3) do
36
+ attribute :first_name, :string
37
+ attribute :middle_name, :string
38
+ choice(min: 2, max: 2) do
39
+ attribute :email, :string
40
+ attribute :phone, :string
41
+ attribute :check, :string
42
+ end
43
+ end
44
+
45
+ choice(min: 1, max: 2) do
46
+ attribute :fb, :string
47
+ choice(min: 1, max: 1) do
48
+ attribute :insta, :string
49
+ attribute :last_name, :string
50
+ end
51
+ end
52
+
53
+ key_value do
54
+ map :first_name, to: :first_name
55
+ map :email, to: :email
56
+ map :phone, to: :phone
57
+ map :fb, to: :fb
58
+ map :insta, to: :insta
59
+ map :last_name, to: :last_name
60
+ end
61
+ end
62
+ end
63
+
64
+ RSpec.describe "Choice" do
65
+ context "with choice option" do
66
+ let(:mapper) { ChoiceSpec::DocumentState }
67
+
68
+ it "returns an empty array for a valid choice instance" do
69
+ valid_instance = mapper.new(
70
+ signed: true,
71
+ unsigned: true,
72
+ watermarked: false,
73
+ candidate: ChoiceSpec::CandidateType.new(id: 1, name: "Smith"),
74
+ )
75
+
76
+ expect(valid_instance.validate).to be_empty
77
+ end
78
+
79
+ it "returns nil for a valid instance, if given attributes for choice are within defined range" do
80
+ valid_instance = mapper.new(
81
+ watermarked: false,
82
+ encrypted: true,
83
+ )
84
+
85
+ expect(valid_instance.validate!).to be_nil
86
+ end
87
+
88
+ it "raises error, if attributes given for choice are out of upper bound" do
89
+ valid_instance = mapper.new(
90
+ signed: true,
91
+ unsigned: false,
92
+ watermarked: false,
93
+ encrypted: true,
94
+ )
95
+
96
+ expect do
97
+ valid_instance.validate!
98
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
99
+ expect(error.error_messages.join("\n")).to include("Attributes `[:signed, :unsigned, :watermarked, :encrypted]` count exceeds the upper bound `3`")
100
+ end
101
+ end
102
+ end
103
+
104
+ context "with nested choice option" do
105
+ let(:mapper) { ChoiceSpec::PersonDetails }
106
+
107
+ it "returns an empty array for a valid instance" do
108
+ valid_instance = mapper.new(
109
+ first_name: "John",
110
+ middle_name: "S",
111
+ fb: "fb",
112
+ )
113
+
114
+ expect(valid_instance.validate).to be_empty
115
+ end
116
+
117
+ it "returns nil for a valid instance" do
118
+ valid_instance = mapper.new(
119
+ email: "email",
120
+ phone: "02344",
121
+ last_name: "last_name",
122
+ )
123
+
124
+ expect(valid_instance.validate!).to be_nil
125
+ end
126
+
127
+ it "raises error, if given attribute for choice are not within upper bound" do
128
+ valid_instance = mapper.new(
129
+ first_name: "Nick",
130
+ email: "email",
131
+ phone: "phone",
132
+ check: "check",
133
+ fb: "fb",
134
+ insta: "insta",
135
+ )
136
+
137
+ expect do
138
+ valid_instance.validate!
139
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
140
+ expect(error.error_messages.join("\n")).to eq("Attributes `[:email, :phone, :check]` count exceeds the upper bound `2`")
141
+ end
142
+ end
143
+
144
+ it "raises error, if given attribute for choice are not within lower bound" do
145
+ valid_instance = mapper.new(
146
+ fb: "fb",
147
+ insta: "insta",
148
+ )
149
+
150
+ expect do
151
+ valid_instance.validate!
152
+ end.to raise_error(Lutaml::Model::ValidationError) do |error|
153
+ expect(error.error_messages.join("\n")).to eq("Attributes `[]` count is less than the lower bound `1`")
154
+ end
155
+ end
156
+
157
+ it "raises error, if min, max is not positive" do
158
+ expect do
159
+ Class.new(Lutaml::Model::Serializable) do
160
+ choice(min: -1, max: -2) do
161
+ attribute :id, :integer
162
+ attribute :name, :string
163
+ end
164
+ end
165
+ end.to raise_error(Lutaml::Model::InvalidChoiceRangeError, "Choice lower bound `-1` must be positive")
166
+ end
167
+ end
168
+ end