lutaml-model 0.5.2 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +2 -0
  3. data/.rubocop_todo.yml +39 -13
  4. data/Gemfile +1 -0
  5. data/README.adoc +430 -52
  6. data/lib/lutaml/model/constants.rb +7 -0
  7. data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
  8. data/lib/lutaml/model/error.rb +1 -0
  9. data/lib/lutaml/model/key_value_mapping.rb +31 -2
  10. data/lib/lutaml/model/mapping_hash.rb +8 -0
  11. data/lib/lutaml/model/mapping_rule.rb +8 -0
  12. data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
  13. data/lib/lutaml/model/schema/xml_compiler.rb +720 -0
  14. data/lib/lutaml/model/schema.rb +5 -0
  15. data/lib/lutaml/model/serialize.rb +33 -13
  16. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
  17. data/lib/lutaml/model/type/hash.rb +11 -11
  18. data/lib/lutaml/model/utils.rb +7 -0
  19. data/lib/lutaml/model/version.rb +1 -1
  20. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +5 -1
  21. data/lib/lutaml/model/xml_adapter/xml_document.rb +11 -15
  22. data/lib/lutaml/model/xml_mapping.rb +4 -2
  23. data/lib/lutaml/model/xml_mapping_rule.rb +1 -4
  24. data/lib/lutaml/model.rb +1 -0
  25. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  26. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  27. data/spec/fixtures/xml/test_schema.xsd +53 -0
  28. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  29. data/spec/lutaml/model/cdata_spec.rb +2 -2
  30. data/spec/lutaml/model/custom_model_spec.rb +7 -20
  31. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  32. data/spec/lutaml/model/map_all_spec.rb +188 -0
  33. data/spec/lutaml/model/mixed_content_spec.rb +15 -15
  34. data/spec/lutaml/model/render_nil_spec.rb +29 -0
  35. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1431 -0
  36. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  37. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  38. data/spec/lutaml/model/xml_mapping_spec.rb +108 -1
  39. metadata +12 -2
@@ -2,6 +2,7 @@ require_relative "schema/json_schema"
2
2
  require_relative "schema/xsd_schema"
3
3
  require_relative "schema/relaxng_schema"
4
4
  require_relative "schema/yaml_schema"
5
+ require_relative "schema/xml_compiler"
5
6
 
6
7
  module Lutaml
7
8
  module Model
@@ -21,6 +22,10 @@ module Lutaml
21
22
  def self.to_yaml(klass, options = {})
22
23
  YamlSchema.generate(klass, options)
23
24
  end
25
+
26
+ def self.from_xml(xml, options = {})
27
+ XmlCompiler.to_models(xml, options)
28
+ end
24
29
  end
25
30
  end
26
31
  end
@@ -269,17 +269,22 @@ module Lutaml
269
269
 
270
270
  value = instance.send(name)
271
271
 
272
+ if rule.raw_mapping?
273
+ adapter = Lutaml::Model::Config.send(:"#{format}_adapter")
274
+ return adapter.parse(value, options)
275
+ end
276
+
272
277
  attribute = attributes[name]
273
278
 
274
- next hash.merge!(generate_hash_from_child_mappings(value, format, rule.root_mappings)) if rule.root_mapping?
279
+ next hash.merge!(generate_hash_from_child_mappings(attribute, value, format, rule.root_mappings)) if rule.root_mapping?
275
280
 
276
281
  value = if rule.child_mappings
277
- generate_hash_from_child_mappings(value, format, rule.child_mappings)
282
+ generate_hash_from_child_mappings(attribute, value, format, rule.child_mappings)
278
283
  else
279
284
  attribute.serialize(value, format, options)
280
285
  end
281
286
 
282
- next if Utils.blank?(value) && !rule.render_nil
287
+ next unless rule.render?(value)
283
288
 
284
289
  rule_from_name = rule.multiple_mappings? ? rule.from.first.to_s : rule.from.to_s
285
290
  hash[rule_from_name] = value
@@ -346,7 +351,7 @@ module Lutaml
346
351
  end
347
352
  end
348
353
 
349
- def generate_hash_from_child_mappings(value, format, child_mappings)
354
+ def generate_hash_from_child_mappings(attr, value, format, child_mappings)
350
355
  return value unless child_mappings
351
356
 
352
357
  hash = {}
@@ -365,7 +370,11 @@ module Lutaml
365
370
  value.each do |child_obj|
366
371
  map_key = nil
367
372
  map_value = {}
373
+ mapping_rules = attr.type.mappings_for(format)
374
+
368
375
  child_mappings.each do |attr_name, path|
376
+ mapping_rule = mapping_rules.find_by_to(attr_name)
377
+
369
378
  attr_value = child_obj.send(attr_name)
370
379
 
371
380
  attr_value = if attr_value.is_a?(Lutaml::Model::Serialize)
@@ -376,7 +385,7 @@ module Lutaml
376
385
  attr_value
377
386
  end
378
387
 
379
- next if Utils.blank?(attr_value)
388
+ next unless mapping_rule&.render?(attr_value)
380
389
 
381
390
  if path == :key
382
391
  map_key = attr_value
@@ -446,11 +455,11 @@ module Lutaml
446
455
  instance.mixed = mappings_for(:xml).mixed_content? || options[:mixed_content]
447
456
  end
448
457
 
449
- if doc["__schema_location"]
458
+ if doc["attributes"]&.key?("__schema_location")
450
459
  instance.schema_location = Lutaml::Model::SchemaLocation.new(
451
- schema_location: doc["__schema_location"][:schema_location],
452
- prefix: doc["__schema_location"][:prefix],
453
- namespace: doc["__schema_location"][:namespace],
460
+ schema_location: doc["attributes"]["__schema_location"][:schema_location],
461
+ prefix: doc["attributes"]["__schema_location"][:prefix],
462
+ namespace: doc["attributes"]["__schema_location"][:namespace],
454
463
  )
455
464
  end
456
465
 
@@ -461,18 +470,17 @@ module Lutaml
461
470
 
462
471
  attr = attribute_for_rule(rule)
463
472
 
464
- namespaced_names = rule.namespaced_names(options[:default_namespace])
465
-
466
473
  value = if rule.raw_mapping?
467
474
  doc.node.inner_xml
468
475
  elsif rule.content_mapping?
469
476
  doc[rule.content_key]
470
- elsif key = (namespaced_names & doc.keys).first
471
- doc[key]
477
+ elsif val = value_for_rule(doc, rule, options)
478
+ val
472
479
  else
473
480
  defaults_used << rule.to
474
481
  attr&.default || rule.to_value_for(instance)
475
482
  end
483
+
476
484
  value = normalize_xml_value(value, rule, attr, options)
477
485
  rule.deserialize(instance, value, attributes, self)
478
486
  end
@@ -484,6 +492,15 @@ module Lutaml
484
492
  instance
485
493
  end
486
494
 
495
+ def value_for_rule(doc, rule, options)
496
+ rule_names = rule.namespaced_names(options[:default_namespace])
497
+ hash = rule.attribute? ? doc["attributes"] : doc["elements"]
498
+ return unless hash
499
+
500
+ value_key = rule_names.find { |name| hash.key_exist?(name) }
501
+ hash.fetch(value_key) if value_key
502
+ end
503
+
487
504
  def apply_hash_mapping(doc, instance, format, options = {})
488
505
  mappings = options[:mappings] || mappings_for(format).mappings
489
506
  mappings.each do |rule|
@@ -496,6 +513,9 @@ module Lutaml
496
513
  value = names.collect do |rule_name|
497
514
  if rule.root_mapping?
498
515
  doc
516
+ elsif rule.raw_mapping?
517
+ adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
518
+ adapter.new(doc).public_send(:"to_#{format}")
499
519
  elsif doc.key?(rule_name.to_s)
500
520
  doc[rule_name.to_s]
501
521
  elsif doc.key?(rule_name.to_sym)
@@ -6,8 +6,7 @@ module Lutaml
6
6
  module TomlAdapter
7
7
  class TomlRbAdapter < TomlDocument
8
8
  def self.parse(toml, _options = {})
9
- data = TomlRB.parse(toml)
10
- new(data)
9
+ TomlRB.parse(toml)
11
10
  end
12
11
 
13
12
  def to_toml(*)
@@ -19,18 +19,18 @@ module Lutaml
19
19
 
20
20
  hash = hash.to_h if hash.is_a?(Lutaml::Model::MappingHash)
21
21
 
22
- hash = hash.except("text")
23
-
24
- hash.transform_values do |value|
25
- if value.is_a?(::Hash)
26
- # Only process if value is a Hash
27
- nested = normalize_hash(value)
28
- # Only include non-text nodes in nested hashes if it's a hash
29
- nested.is_a?(::Hash) ? nested.except("text") : nested
30
- else
31
- value
32
- end
22
+ normalized_hash = hash.transform_values do |value|
23
+ normalize_value(value)
33
24
  end
25
+
26
+ normalized_hash["elements"] || normalized_hash
27
+ end
28
+
29
+ def self.normalize_value(value)
30
+ return value unless value.is_a?(::Hash)
31
+
32
+ nested = normalize_hash(value)
33
+ nested.is_a?(::Hash) ? nested.except("text") : nested
34
34
  end
35
35
 
36
36
  def self.serialize(value)
@@ -42,6 +42,13 @@ module Lutaml
42
42
  value.respond_to?(:empty?) ? value.empty? : value.nil?
43
43
  end
44
44
 
45
+ def empty_collection?(collection)
46
+ return false if collection.nil?
47
+ return false unless [Array, Hash].include?(collection.class)
48
+
49
+ collection.empty?
50
+ end
51
+
45
52
  def add_method_if_not_defined(klass, method_name, &block)
46
53
  unless klass.method_defined?(method_name)
47
54
  klass.class_eval do
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.5.2"
5
+ VERSION = "0.5.4"
6
6
  end
7
7
  end
@@ -124,7 +124,11 @@ module Lutaml
124
124
  end
125
125
 
126
126
  attributes = {}
127
- node.attributes.transform_values do |attr|
127
+
128
+ # Using `attribute_nodes` instead of `attributes` because
129
+ # `attribute_nodes` handles name collisions as well
130
+ # More info: https://devdocs.io/nokogiri/nokogiri/xml/node#method-i-attributes
131
+ node.attribute_nodes.each do |attr|
128
132
  name = if attr.namespace
129
133
  "#{attr.namespace.prefix}:#{attr.name}"
130
134
  else
@@ -77,26 +77,21 @@ module Lutaml
77
77
  result.node = element
78
78
  result.item_order = element.order
79
79
 
80
- element.children.each_with_object(result) do |child, hash|
80
+ element.children.each do |child|
81
81
  if klass&.<= Serialize
82
82
  attr = klass.attribute_for_child(child.name,
83
83
  format)
84
84
  end
85
85
 
86
- value = if child.text?
87
- child.text
88
- else
89
- parse_element(child, attr&.type || klass, format)
90
- end
91
-
92
- hash[child.namespaced_name] = if hash[child.namespaced_name]
93
- [hash[child.namespaced_name], value].flatten
94
- else
95
- value
96
- end
86
+ next result.assign_or_append_value(child.name, child.text) if child.text?
87
+
88
+ result["elements"] ||= Lutaml::Model::MappingHash.new
89
+ result["elements"].assign_or_append_value(child.namespaced_name, parse_element(child, attr&.type || klass, format))
97
90
  end
98
91
 
99
- result.merge(attributes_hash(element))
92
+ result["attributes"] = attributes_hash(element) if element.attributes&.any?
93
+
94
+ result
100
95
  end
101
96
 
102
97
  def attributes_hash(element)
@@ -167,8 +162,9 @@ module Lutaml
167
162
  def add_value(xml, value, attribute, cdata: false)
168
163
  if !value.nil?
169
164
  serialized_value = attribute.type.serialize(value)
170
-
171
- if attribute.type == Lutaml::Model::Type::Hash
165
+ if attribute.raw?
166
+ xml.add_xml_fragment(xml, value)
167
+ elsif attribute.type == Lutaml::Model::Type::Hash
172
168
  serialized_value.each do |key, val|
173
169
  xml.create_and_add_element(key) do |element|
174
170
  element.text(val)
@@ -149,10 +149,10 @@ module Lutaml
149
149
  prefix: (prefix_set = false
150
150
  nil)
151
151
  )
152
- validate!("__raw_mapping", to, with, type: TYPES[:all_content])
152
+ validate!(Constants::RAW_MAPPING_KEY, to, with, type: TYPES[:all_content])
153
153
 
154
154
  rule = XmlMappingRule.new(
155
- "__raw_mapping",
155
+ Constants::RAW_MAPPING_KEY,
156
156
  to: to,
157
157
  render_nil: render_nil,
158
158
  render_default: render_default,
@@ -168,6 +168,8 @@ module Lutaml
168
168
  @raw_mapping = rule
169
169
  end
170
170
 
171
+ alias map_all_content map_all
172
+
171
173
  def validate!(key, to, with, type: nil)
172
174
  validate_mappings!(type)
173
175
 
@@ -59,10 +59,6 @@ module Lutaml
59
59
  name.nil?
60
60
  end
61
61
 
62
- def raw_mapping?
63
- name == "__raw_mapping"
64
- end
65
-
66
62
  def content_key
67
63
  cdata ? "#cdata-section" : "text"
68
64
  end
@@ -111,6 +107,7 @@ module Lutaml
111
107
  prefix: prefix.dup,
112
108
  mixed_content: mixed_content,
113
109
  namespace_set: namespace_set?,
110
+ attribute: attribute,
114
111
  prefix_set: prefix_set?,
115
112
  default_namespace: default_namespace.dup,
116
113
  )
data/lib/lutaml/model.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "model/yaml_adapter/standard_yaml_adapter"
10
10
  require_relative "model/xml_adapter"
11
11
  require_relative "model/toml_adapter"
12
12
  require_relative "model/error"
13
+ require_relative "model/constants"
13
14
 
14
15
  module Lutaml
15
16
  module Model
@@ -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,4 @@
1
+ <MathDocument>
2
+ <Title>Example Title</Title>
3
+ <IPV4Address>192.168.1.1</IPV4Address>
4
+ </MathDocument>
@@ -118,8 +118,8 @@ module CDATA
118
118
  def child_from_xml(model, value)
119
119
  model.child_mapper ||= CustomModelChild.new
120
120
 
121
- model.child_mapper.street = value["street"].text
122
- model.child_mapper.city = value["city"].text
121
+ model.child_mapper.street = value["elements"]["street"].text
122
+ model.child_mapper.city = value["elements"]["city"].text
123
123
  end
124
124
  end
125
125
 
@@ -56,8 +56,8 @@ class CustomModelParentMapper < Lutaml::Model::Serializable
56
56
  def child_from_xml(model, value)
57
57
  model.child_mapper ||= CustomModelChild.new
58
58
 
59
- model.child_mapper.street = value["street"].text
60
- model.child_mapper.city = value["city"].text
59
+ model.child_mapper.street = value["elements"]["street"].text
60
+ model.child_mapper.city = value["elements"]["city"].text
61
61
  end
62
62
  end
63
63
 
@@ -126,10 +126,10 @@ module CustomModelSpecs
126
126
 
127
127
  def bibdata_from_xml(model, value)
128
128
  model.bibdata = BibliographicItem.new(
129
- "type" => value["type"],
130
- "title" => value["title"],
131
- "language" => value["title"]["language"],
132
- "schema_version" => value["schema-version"],
129
+ "type" => value["attributes"]["type"],
130
+ "title" => value["elements"]["title"],
131
+ "language" => value["elements"]["title"]["attributes"]["language"],
132
+ "schema_version" => value["attributes"]["schema-version"],
133
133
  )
134
134
  end
135
135
 
@@ -410,22 +410,9 @@ RSpec.describe "CustomModel" do
410
410
  </MixedWithNestedContent>
411
411
  XML
412
412
 
413
- expected_xml = <<~XML
414
- <MixedWithNestedContent>
415
- <street>
416
- A &lt;p&gt;b&lt;/p&gt; B &lt;p&gt;c&lt;/p&gt; C
417
- </street>
418
- <bibdata type="collection" schema-version="v1.2.8">
419
- <title language="en">
420
- JCGM Collection 1
421
- </title>
422
- </bibdata>
423
- </MixedWithNestedContent>
424
- XML
425
-
426
413
  bibdata = CustomModelSpecs::MixedWithNestedContent.from_xml(xml)
427
414
 
428
- expect(bibdata.to_xml).to be_equivalent_to(expected_xml)
415
+ expect(bibdata.to_xml).to be_equivalent_to(xml)
429
416
  end
430
417
  end
431
418
  end
@@ -83,4 +83,31 @@ RSpec.describe Lutaml::Model::KeyValueMapping do
83
83
  expect(m.custom_methods.object_id).not_to eq(dup_m.custom_methods.object_id)
84
84
  end
85
85
  end
86
+
87
+ context "with map_all option" do
88
+ before do
89
+ mapping.map_all(
90
+ render_nil: true,
91
+ delegate: :container,
92
+ )
93
+ end
94
+
95
+ it "handles JSON mapping" do
96
+ expect(mapping.mappings[0].render_nil).to be true
97
+ expect(mapping.mappings[0].delegate).to eq(:container)
98
+ expect(mapping.mappings[0].raw_mapping?).to be true
99
+ end
100
+
101
+ it "handles YAML mapping" do
102
+ expect(mapping.mappings[0].render_nil).to be true
103
+ expect(mapping.mappings[0].delegate).to eq(:container)
104
+ expect(mapping.mappings[0].raw_mapping?).to be true
105
+ end
106
+
107
+ it "handles TOML mapping" do
108
+ expect(mapping.mappings[0].render_nil).to be true
109
+ expect(mapping.mappings[0].delegate).to eq(:container)
110
+ expect(mapping.mappings[0].raw_mapping?).to be true
111
+ end
112
+ end
86
113
  end