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
@@ -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
@@ -11,6 +11,13 @@ module Lutaml
11
11
  def to_xml_attribute
12
12
  "#{@namespace} #{@location}".strip
13
13
  end
14
+
15
+ def eql?(other)
16
+ other.class == self.class &&
17
+ namespace == other.namespace &&
18
+ location == other.location
19
+ end
20
+ alias == eql?
14
21
  end
15
22
 
16
23
  class SchemaLocation
@@ -0,0 +1,71 @@
1
+ module Lutaml
2
+ module Model
3
+ class Sequence
4
+ attr_reader :attributes,
5
+ :model
6
+
7
+ def initialize(model)
8
+ @attributes = []
9
+ @model = model
10
+ end
11
+
12
+ def attribute(name, type, options = {})
13
+ options[:sequence] = self
14
+ @model.attribute(name, type, options)
15
+ end
16
+
17
+ def sequence(&block)
18
+ instance_eval(&block)
19
+ end
20
+
21
+ def map_element(
22
+ name,
23
+ to: nil,
24
+ render_nil: false,
25
+ render_default: false,
26
+ with: {},
27
+ delegate: nil,
28
+ cdata: false,
29
+ namespace: nil,
30
+ prefix: nil,
31
+ transform: {}
32
+ )
33
+ @attributes << @model.map_element(
34
+ name,
35
+ to: to,
36
+ render_nil: render_nil,
37
+ render_default: render_default,
38
+ with: with,
39
+ delegate: delegate,
40
+ cdata: cdata,
41
+ namespace: namespace,
42
+ prefix: prefix,
43
+ transform: transform,
44
+ )
45
+ end
46
+
47
+ def map_attribute(*)
48
+ raise Lutaml::Model::UnknownSequenceMappingError.new("map_attribute")
49
+ end
50
+
51
+ def map_content(*)
52
+ raise Lutaml::Model::UnknownSequenceMappingError.new("map_content")
53
+ end
54
+
55
+ def map_all(*)
56
+ raise Lutaml::Model::UnknownSequenceMappingError.new("map_all")
57
+ end
58
+
59
+ def validate_content!(element_order)
60
+ defined_order = @attributes.map { |rule| rule.name.to_s }
61
+ start_index = element_order.index(defined_order.first)
62
+
63
+ defined_order.each.with_index(start_index) do |element, i|
64
+ unless element_order[i] == element
65
+ raise Lutaml::Model::IncorrectSequenceError.new(element, element_order[i])
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -12,12 +12,16 @@ require_relative "comparable_model"
12
12
  require_relative "schema_location"
13
13
  require_relative "validation"
14
14
  require_relative "error"
15
+ require_relative "choice"
16
+ require_relative "sequence"
17
+ require_relative "liquefiable"
15
18
 
16
19
  module Lutaml
17
20
  module Model
18
21
  module Serialize
19
22
  include ComparableModel
20
23
  include Validation
24
+ include Lutaml::Model::Liquefiable
21
25
 
22
26
  def self.included(base)
23
27
  base.extend(ClassMethods)
@@ -25,7 +29,9 @@ module Lutaml
25
29
  end
26
30
 
27
31
  module ClassMethods
28
- attr_accessor :attributes, :mappings
32
+ include Lutaml::Model::Liquefiable::ClassMethods
33
+
34
+ attr_accessor :attributes, :mappings, :choice_attributes
29
35
 
30
36
  def inherited(subclass)
31
37
  super
@@ -40,6 +46,7 @@ module Lutaml
40
46
  def initialize_attrs(source_class)
41
47
  @mappings = Utils.deep_dup(source_class.instance_variable_get(:@mappings)) || {}
42
48
  @attributes = Utils.deep_dup(source_class.instance_variable_get(:@attributes)) || {}
49
+ @choice_attributes = Utils.deep_dup(source_class.instance_variable_get(:@choice_attributes)) || []
43
50
  instance_variable_set(:@model, self)
44
51
  end
45
52
 
@@ -81,6 +88,12 @@ module Lutaml
81
88
  value
82
89
  end
83
90
 
91
+ def choice(min: 1, max: 1, &block)
92
+ @choice_attributes << Choice.new(self, min, max).tap do |c|
93
+ c.instance_eval(&block)
94
+ end
95
+ end
96
+
84
97
  # Define an attribute for the model
85
98
  def attribute(name, type, options = {})
86
99
  attr = Attribute.new(name, type, options)
@@ -103,6 +116,33 @@ module Lutaml
103
116
  instance_variable_set(:"@#{name}", attr.cast_value(value))
104
117
  end
105
118
  end
119
+
120
+ register_drop_method(name)
121
+
122
+ attr
123
+ end
124
+
125
+ def root?
126
+ mappings_for(:xml).root?
127
+ end
128
+
129
+ def import_model_attributes(model)
130
+ raise Lutaml::Model::ImportModelWithRootError.new(model) if model.root?
131
+
132
+ @attributes.merge!(model.attributes)
133
+ end
134
+
135
+ def import_model_mappings(model)
136
+ raise Lutaml::Model::ImportModelWithRootError.new(model) if model.root?
137
+
138
+ @mappings.merge!(model.mappings)
139
+ end
140
+
141
+ def import_model(model)
142
+ raise Lutaml::Model::ImportModelWithRootError.new(model) if model.root?
143
+
144
+ import_model_attributes(model)
145
+ import_model_mappings(model)
106
146
  end
107
147
 
108
148
  def add_enum_methods_to_model(klass, enum_name, values, collection: false)
@@ -140,7 +180,7 @@ module Lutaml
140
180
  enum_vals.delete(value)
141
181
  enum_vals
142
182
  else
143
- []
183
+ instance_variable_get(:"@#{enum_name}") - [value]
144
184
  end
145
185
 
146
186
  instance_variable_set(:"@#{enum_name}", enum_vals)
@@ -190,7 +230,7 @@ module Lutaml
190
230
  mappings[format] ||= klass.new
191
231
  mappings[format].instance_eval(&block)
192
232
 
193
- if format == :xml && !mappings[format].root_element
233
+ if format == :xml && !mappings[format].root_element && !mappings[format].no_root?
194
234
  mappings[format].root(model.to_s)
195
235
  end
196
236
  end
@@ -208,9 +248,10 @@ module Lutaml
208
248
  end
209
249
 
210
250
  if format == :xml
211
- doc_hash = doc.parse_element(doc.root, self, :xml)
251
+ raise Lutaml::Model::NoRootMappingError.new(self) unless root?
252
+
212
253
  options[:encoding] = doc.encoding
213
- apply_mappings(doc_hash, format, options)
254
+ apply_mappings(doc, format, options)
214
255
  else
215
256
  apply_mappings(doc.to_h, format)
216
257
  end
@@ -269,8 +310,17 @@ module Lutaml
269
310
 
270
311
  value = instance.send(name)
271
312
 
313
+ if rule.raw_mapping?
314
+ adapter = Lutaml::Model::Config.send(:"#{format}_adapter")
315
+ return adapter.parse(value, options)
316
+ end
317
+
272
318
  attribute = attributes[name]
273
319
 
320
+ if export_method = rule.transform[:export] || attribute.transform_export_method
321
+ value = export_method.call(value)
322
+ end
323
+
274
324
  next hash.merge!(generate_hash_from_child_mappings(attribute, value, format, rule.root_mappings)) if rule.root_mapping?
275
325
 
276
326
  value = if rule.child_mappings
@@ -424,6 +474,7 @@ module Lutaml
424
474
  return instance if Utils.blank?(doc)
425
475
 
426
476
  options[:mappings] = mappings_for(format).mappings
477
+
427
478
  return apply_xml_mapping(doc, instance, options) if format == :xml
428
479
 
429
480
  apply_hash_mapping(doc, instance, format, options)
@@ -440,43 +491,45 @@ module Lutaml
440
491
  end
441
492
  mappings = options[:mappings] || mappings_for(:xml).mappings
442
493
 
443
- if doc.is_a?(Array)
444
- raise Lutaml::Model::CollectionTrueMissingError(self, option[:caller_class])
445
- end
494
+ raise Lutaml::Model::CollectionTrueMissingError(self, option[:caller_class]) if doc.is_a?(Array)
446
495
 
447
- if instance.respond_to?(:ordered=) && doc.is_a?(Lutaml::Model::MappingHash)
448
- instance.element_order = doc.item_order
496
+ if instance.respond_to?(:ordered=)
497
+ instance.element_order = doc.root.order
449
498
  instance.ordered = mappings_for(:xml).ordered? || options[:ordered]
450
499
  instance.mixed = mappings_for(:xml).mixed_content? || options[:mixed_content]
451
500
  end
452
501
 
453
- if doc["__schema_location"]
502
+ schema_location = doc.attributes.values.find do |a|
503
+ a.unprefixed_name == "schemaLocation"
504
+ end
505
+
506
+ if !schema_location.nil?
454
507
  instance.schema_location = Lutaml::Model::SchemaLocation.new(
455
- schema_location: doc["__schema_location"][:schema_location],
456
- prefix: doc["__schema_location"][:prefix],
457
- namespace: doc["__schema_location"][:namespace],
508
+ schema_location: schema_location.value,
509
+ prefix: schema_location.namespace_prefix,
510
+ namespace: schema_location.namespace,
458
511
  )
459
512
  end
460
513
 
461
514
  defaults_used = []
515
+ validate_sequence!(instance.element_order)
462
516
 
463
517
  mappings.each do |rule|
464
518
  raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
465
519
 
466
520
  attr = attribute_for_rule(rule)
467
521
 
468
- namespaced_names = rule.namespaced_names(options[:default_namespace])
469
-
470
522
  value = if rule.raw_mapping?
471
- doc.node.inner_xml
523
+ doc.root.inner_xml
472
524
  elsif rule.content_mapping?
473
- doc[rule.content_key]
474
- elsif key = (namespaced_names & doc.keys).first
475
- doc[key]
476
- else
525
+ rule.cdata ? doc.cdata : doc.text
526
+ elsif val = value_for_rule(doc, rule, options, instance)
527
+ val
528
+ elsif instance.using_default?(rule.to) || rule.render_default
477
529
  defaults_used << rule.to
478
530
  attr&.default || rule.to_value_for(instance)
479
531
  end
532
+
480
533
  value = normalize_xml_value(value, rule, attr, options)
481
534
  rule.deserialize(instance, value, attributes, self)
482
535
  end
@@ -488,6 +541,46 @@ module Lutaml
488
541
  instance
489
542
  end
490
543
 
544
+ def value_for_rule(doc, rule, options, instance)
545
+ rule_names = rule.namespaced_names(options[:default_namespace])
546
+
547
+ if rule.attribute?
548
+ doc.root.find_attribute_value(rule_names)
549
+ else
550
+ attr = attribute_for_rule(rule)
551
+
552
+ children = doc.children.select do |child|
553
+ rule_names.include?(child.namespaced_name)
554
+ end
555
+
556
+ if rule.using_custom_methods? || attr.type == Lutaml::Model::Type::Hash
557
+ return children.first
558
+ end
559
+
560
+ if Utils.present?(children)
561
+ instance.value_set_for(attr.name)
562
+ end
563
+
564
+ if rule.cdata
565
+ values = children.map do |child|
566
+ child.cdata_children&.map(&:text)
567
+ end.flatten
568
+ return children.count > 1 ? values : values.first
569
+ end
570
+
571
+ values = children.map do |child|
572
+ if !rule.using_custom_methods? && attr.type <= Serialize
573
+ attr.type.apply_xml_mapping(child, attr.type.new, options.except(:mappings))
574
+ elsif attr.raw?
575
+ inner_xml_of(child)
576
+ else
577
+ child&.children&.first&.text
578
+ end
579
+ end
580
+ attr&.collection? ? values : values.first
581
+ end
582
+ end
583
+
491
584
  def apply_hash_mapping(doc, instance, format, options = {})
492
585
  mappings = options[:mappings] || mappings_for(format).mappings
493
586
  mappings.each do |rule|
@@ -500,6 +593,9 @@ module Lutaml
500
593
  value = names.collect do |rule_name|
501
594
  if rule.root_mapping?
502
595
  doc
596
+ elsif rule.raw_mapping?
597
+ adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
598
+ adapter.new(doc).public_send(:"to_#{format}")
503
599
  elsif doc.key?(rule_name.to_s)
504
600
  doc[rule_name.to_s]
505
601
  elsif doc.key?(rule_name.to_sym)
@@ -530,18 +626,6 @@ module Lutaml
530
626
  def normalize_xml_value(value, rule, attr, options = {})
531
627
  value = [value].compact if attr&.collection? && !value.is_a?(Array)
532
628
 
533
- value = if value.is_a?(Array)
534
- value.map do |v|
535
- text_hash?(attr, v) ? v.text : v
536
- end
537
- elsif attr&.raw? && value
538
- value.node.children.map(&:to_xml).join
539
- elsif text_hash?(attr, value)
540
- value.text
541
- else
542
- value
543
- end
544
-
545
629
  return value unless cast_value?(attr, rule)
546
630
 
547
631
  options.merge(caller_class: self, mixed_content: rule.mixed_content)
@@ -583,6 +667,26 @@ module Lutaml
583
667
  value
584
668
  end
585
669
  end
670
+
671
+ def validate_sequence!(element_order)
672
+ mapping_sequence = mappings_for(:xml).element_sequence
673
+ current_order = element_order.filter_map(&:element_tag)
674
+
675
+ mapping_sequence.each do |mapping|
676
+ mapping.validate_content!(current_order)
677
+ end
678
+ end
679
+
680
+ private
681
+
682
+ def inner_xml_of(node)
683
+ case node
684
+ when XmlAdapter::XmlElement
685
+ node.inner_xml
686
+ else
687
+ node.children.map(&:to_xml).join
688
+ end
689
+ end
586
690
  end
587
691
 
588
692
  attr_accessor :element_order, :schema_location, :encoding
@@ -703,6 +807,8 @@ module Lutaml
703
807
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
704
808
  define_method(:"to_#{format}") do |options = {}|
705
809
  adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
810
+ raise Lutaml::Model::NoRootMappingError.new(self.class) unless self.class.root?
811
+
706
812
  representation = if format == :xml
707
813
  self
708
814
  else
@@ -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(*)
@@ -27,10 +27,6 @@ module Lutaml
27
27
  value.to_s("F") # Use fixed-point notation to match test expectations
28
28
  end
29
29
 
30
- def self.from_xml(value)
31
- cast(value.text)
32
- end
33
-
34
30
  def self.check_dependencies!(value)
35
31
  unless defined?(BigDecimal)
36
32
  raise TypeNotEnabledError.new("Decimal", value)
@@ -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)
@@ -25,9 +25,9 @@ module Lutaml
25
25
  end
26
26
 
27
27
  # # xs:time format (HH:MM:SS.mmm±HH:MM)
28
- # def to_xml
29
- # value&.strftime("%H:%M:%S%:z")
30
- # end
28
+ def to_xml
29
+ value&.iso8601
30
+ end
31
31
 
32
32
  # # ISO8601 time format
33
33
  # def to_json
@@ -85,26 +85,30 @@ module Lutaml
85
85
  end
86
86
  end
87
87
 
88
- def deep_dup(hash)
89
- return hash if hash.nil?
90
-
91
- new_hash = {}
92
-
93
- hash.each do |key, value|
94
- new_hash[key] = if value.is_a?(Hash)
95
- deep_dup(value)
96
- elsif value.respond_to?(:deep_dup)
97
- value.deep_dup
98
- else
99
- value.dup
100
- end
101
- end
88
+ def deep_dup(object)
89
+ return object if object.nil?
102
90
 
103
- new_hash
91
+ case object
92
+ when Hash then deep_dup_hash(object)
93
+ when Array then deep_dup_array(object)
94
+ else deep_dup_object(object)
95
+ end
104
96
  end
105
97
 
106
98
  private
107
99
 
100
+ def deep_dup_hash(hash)
101
+ hash.transform_values { |value| deep_dup(value) }
102
+ end
103
+
104
+ def deep_dup_array(array)
105
+ array.map { |value| deep_dup(value) }
106
+ end
107
+
108
+ def deep_dup_object(object)
109
+ object.respond_to?(:deep_dup) ? object.deep_dup : object.dup
110
+ end
111
+
108
112
  def camelize_part(part)
109
113
  part.gsub(/(?:_|-|^)([a-z\d])/i) { $1.upcase }
110
114
  end
@@ -17,13 +17,24 @@ module Lutaml
17
17
  errors << e
18
18
  end
19
19
  end
20
- errors
20
+
21
+ validate_helper(errors)
21
22
  end
22
23
 
23
24
  def validate!
24
25
  errors = validate
25
26
  raise Lutaml::Model::ValidationError.new(errors) if errors.any?
26
27
  end
28
+
29
+ def validate_helper(errors)
30
+ self.class.choice_attributes.each do |attribute|
31
+ attribute.validate_content!(self)
32
+ end
33
+ errors
34
+ rescue Lutaml::Model::ChoiceUpperBoundError,
35
+ Lutaml::Model::ChoiceLowerBoundError => e
36
+ errors << e
37
+ end
27
38
  end
28
39
  end
29
40
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.5.3"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -6,19 +6,15 @@ module Lutaml
6
6
  module Builder
7
7
  class Oga
8
8
  def self.build(options = {}, &block)
9
- if block_given?
10
- XmlAdapter::Builder::Oga.new(options, &block)
11
- else
12
- XmlAdapter::Builder::Oga.new(options)
13
- end
9
+ new(options, &block)
14
10
  end
15
11
 
16
- attr_reader :document, :current_node, :options
12
+ attr_reader :document, :current_node, :encoding
17
13
 
18
14
  def initialize(options = {})
19
15
  @document = XmlAdapter::Oga::Document.new
20
16
  @current_node = @document
21
- @options = options
17
+ @encoding = options[:encoding]
22
18
  yield(self) if block_given?
23
19
  end
24
20
 
@@ -112,9 +108,14 @@ module Lutaml
112
108
  end
113
109
 
114
110
  def add_text(element, text, cdata: false)
111
+ text = text&.encode(encoding) if encoding && text.is_a?(String)
115
112
  return add_cdata(element, text) if cdata
116
113
 
117
114
  oga_text = ::Oga::XML::Text.new(text: text.to_s)
115
+ append_text_node(element, oga_text)
116
+ end
117
+
118
+ def append_text_node(element, oga_text)
118
119
  if element.is_a?(XmlAdapter::Oga::Document)
119
120
  children = element.children
120
121
  children.empty? ? children << oga_text : children.last.children << oga_text
@@ -166,6 +167,8 @@ module Lutaml
166
167
 
167
168
  def element_attributes(oga_element, attributes)
168
169
  oga_element.attributes = attributes.map do |name, value|
170
+ value = value.uri unless value.is_a?(String)
171
+
169
172
  ::Oga::XML::Attribute.new(
170
173
  name: name,
171
174
  value: value,
@@ -6,17 +6,18 @@ module Lutaml
6
6
  def self.build(options = {})
7
7
  if block_given?
8
8
  ::Ox::Builder.new(options) do |xml|
9
- yield(new(xml))
9
+ yield(new(xml, options))
10
10
  end
11
11
  else
12
- new(::Ox::Builder.new(options))
12
+ new(::Ox::Builder.new(options), options)
13
13
  end
14
14
  end
15
15
 
16
- attr_reader :xml
16
+ attr_reader :xml, :encoding
17
17
 
18
- def initialize(xml)
18
+ def initialize(xml, options = {})
19
19
  @xml = xml
20
+ @encoding = options[:encoding]
20
21
  @current_namespace = nil
21
22
  end
22
23
 
@@ -27,7 +28,7 @@ module Lutaml
27
28
 
28
29
  if block_given?
29
30
  xml.element(name, attributes) do |element|
30
- yield(self.class.new(element))
31
+ yield(self.class.new(element, { encoding: encoding }))
31
32
  end
32
33
  else
33
34
  xml.element(name, attributes)
@@ -44,17 +45,11 @@ module Lutaml
44
45
 
45
46
  def create_and_add_element(element_name, prefix: nil, attributes: {})
46
47
  element_name = element_name.first if element_name.is_a?(Array)
47
- prefixed_name = if prefix
48
- "#{prefix}:#{element_name}"
49
- elsif @current_namespace && !element_name.start_with?("#{@current_namespace}:")
50
- "#{@current_namespace}:#{element_name}"
51
- else
52
- element_name
53
- end
48
+ prefixed_name = set_prefixed_name(element_name, prefix)
54
49
 
55
50
  if block_given?
56
51
  xml.element(prefixed_name, attributes) do |element|
57
- yield(self.class.new(element))
52
+ yield(self.class.new(element, { encoding: encoding }))
58
53
  end
59
54
  else
60
55
  xml.element(prefixed_name, attributes)
@@ -63,6 +58,16 @@ module Lutaml
63
58
  @current_namespace = nil
64
59
  end
65
60
 
61
+ def set_prefixed_name(element_name, prefix)
62
+ if prefix
63
+ "#{prefix}:#{element_name}"
64
+ elsif @current_namespace && !element_name.start_with?("#{@current_namespace}:")
65
+ "#{@current_namespace}:#{element_name}"
66
+ else
67
+ element_name
68
+ end
69
+ end
70
+
66
71
  def <<(text)
67
72
  xml.text(text)
68
73
  end
@@ -72,6 +77,8 @@ module Lutaml
72
77
  end
73
78
 
74
79
  def add_text(element, text, cdata: false)
80
+ text = text&.encode(encoding) if encoding && text.is_a?(String)
81
+
75
82
  return element.cdata(text) if cdata
76
83
 
77
84
  element.text(text)