lutaml-model 0.6.7 → 0.7.1

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 (60) 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_todo.yml +18 -33
  5. data/README.adoc +4380 -2557
  6. data/lib/lutaml/model/attribute.rb +94 -15
  7. data/lib/lutaml/model/choice.rb +7 -0
  8. data/lib/lutaml/model/comparable_model.rb +48 -9
  9. data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +1 -1
  10. data/lib/lutaml/model/error/polymorphic_error.rb +9 -0
  11. data/lib/lutaml/model/error.rb +1 -0
  12. data/lib/lutaml/model/mapping/json_mapping.rb +17 -0
  13. data/lib/lutaml/model/{key_value_mapping.rb → mapping/key_value_mapping.rb} +58 -14
  14. data/lib/lutaml/model/{key_value_mapping_rule.rb → mapping/key_value_mapping_rule.rb} +18 -2
  15. data/lib/lutaml/model/mapping/mapping_rule.rb +299 -0
  16. data/lib/lutaml/model/mapping/toml_mapping.rb +25 -0
  17. data/lib/lutaml/model/{xml_mapping.rb → mapping/xml_mapping.rb} +97 -15
  18. data/lib/lutaml/model/{xml_mapping_rule.rb → mapping/xml_mapping_rule.rb} +20 -3
  19. data/lib/lutaml/model/mapping/yaml_mapping.rb +17 -0
  20. data/lib/lutaml/model/mapping.rb +14 -0
  21. data/lib/lutaml/model/schema/xml_compiler.rb +15 -15
  22. data/lib/lutaml/model/sequence.rb +2 -2
  23. data/lib/lutaml/model/serialize.rb +247 -97
  24. data/lib/lutaml/model/type/date.rb +1 -1
  25. data/lib/lutaml/model/type/date_time.rb +2 -2
  26. data/lib/lutaml/model/type/hash.rb +1 -1
  27. data/lib/lutaml/model/type/time.rb +2 -2
  28. data/lib/lutaml/model/type/time_without_date.rb +2 -2
  29. data/lib/lutaml/model/uninitialized_class.rb +64 -0
  30. data/lib/lutaml/model/utils.rb +14 -0
  31. data/lib/lutaml/model/validation.rb +1 -0
  32. data/lib/lutaml/model/version.rb +1 -1
  33. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +1 -1
  34. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +1 -1
  35. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +1 -1
  36. data/lib/lutaml/model/xml_adapter/xml_document.rb +38 -17
  37. data/lib/lutaml/model/xml_adapter/xml_element.rb +17 -7
  38. data/lib/lutaml/model.rb +1 -0
  39. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +3 -3
  40. data/spec/fixtures/person.rb +5 -5
  41. data/spec/lutaml/model/attribute_spec.rb +37 -1
  42. data/spec/lutaml/model/cdata_spec.rb +2 -2
  43. data/spec/lutaml/model/collection_spec.rb +50 -2
  44. data/spec/lutaml/model/comparable_model_spec.rb +92 -27
  45. data/spec/lutaml/model/defaults_spec.rb +1 -1
  46. data/spec/lutaml/model/enum_spec.rb +1 -1
  47. data/spec/lutaml/model/group_spec.rb +316 -14
  48. data/spec/lutaml/model/key_value_mapping_spec.rb +41 -3
  49. data/spec/lutaml/model/polymorphic_spec.rb +348 -0
  50. data/spec/lutaml/model/render_empty_spec.rb +194 -0
  51. data/spec/lutaml/model/render_nil_spec.rb +206 -22
  52. data/spec/lutaml/model/simple_model_spec.rb +9 -9
  53. data/spec/lutaml/model/value_map_spec.rb +240 -0
  54. data/spec/lutaml/model/xml/namespace/nested_with_explicit_namespace_spec.rb +85 -0
  55. data/spec/lutaml/model/xml/xml_element_spec.rb +93 -0
  56. data/spec/lutaml/model/xml_mapping_rule_spec.rb +102 -2
  57. data/spec/lutaml/model/xml_mapping_spec.rb +45 -3
  58. data/spec/sample_model_spec.rb +3 -3
  59. metadata +20 -8
  60. data/lib/lutaml/model/mapping_rule.rb +0 -109
@@ -14,6 +14,9 @@ module Lutaml
14
14
  choice
15
15
  sequence
16
16
  method_name
17
+ polymorphic
18
+ polymorphic_class
19
+ initialize_empty
17
20
  ].freeze
18
21
 
19
22
  def initialize(name, type, options = {})
@@ -25,6 +28,10 @@ module Lutaml
25
28
  process_options!
26
29
  end
27
30
 
31
+ def polymorphic?
32
+ @options[:polymorphic_class]
33
+ end
34
+
28
35
  def derived?
29
36
  type.nil?
30
37
  end
@@ -41,6 +48,10 @@ module Lutaml
41
48
  @options[:method_name]
42
49
  end
43
50
 
51
+ def initialize_empty?
52
+ @options[:initialize_empty]
53
+ end
54
+
44
55
  def cast_type!(type)
45
56
  case type
46
57
  when Symbol
@@ -97,11 +108,17 @@ module Lutaml
97
108
  type.attributes[to].default
98
109
  elsif options[:default].is_a?(Proc)
99
110
  options[:default].call
100
- else
111
+ elsif options.key?(:default)
101
112
  options[:default]
113
+ else
114
+ Lutaml::Model::UninitializedClass.instance
102
115
  end
103
116
  end
104
117
 
118
+ def default_set?
119
+ !Utils.uninitialized?(default_value)
120
+ end
121
+
105
122
  def pattern
106
123
  options[:pattern]
107
124
  end
@@ -121,6 +138,7 @@ module Lutaml
121
138
  def valid_value!(value)
122
139
  return true if value.nil? && singular?
123
140
  return true unless enum?
141
+ return true if Utils.uninitialized?(value)
124
142
 
125
143
  unless valid_value?(value)
126
144
  raise Lutaml::Model::InvalidValueError.new(name, value, enum_values)
@@ -162,7 +180,21 @@ module Lutaml
162
180
 
163
181
  valid_value!(value) &&
164
182
  valid_collection!(value, self) &&
165
- valid_pattern!(value)
183
+ valid_pattern!(value) &&
184
+ validate_polymorphic!(value)
185
+ end
186
+
187
+ def validate_polymorphic(value)
188
+ return value.all? { |v| validate_polymorphic!(v) } if value.is_a?(Array)
189
+ return true unless polymorphic_enabled?
190
+
191
+ valid_polymorphic_type?(value)
192
+ end
193
+
194
+ def validate_polymorphic!(value)
195
+ return true if validate_polymorphic(value)
196
+
197
+ raise Lutaml::Model::PolymorphicError.new(value, options)
166
198
  end
167
199
 
168
200
  def validate_collection_range
@@ -200,9 +232,6 @@ module Lutaml
200
232
 
201
233
  return true unless collection?
202
234
 
203
- # Allow nil values for collections during initialization
204
- return true if value.nil?
205
-
206
235
  # Allow any value for unbounded collections
207
236
  return true if options[:collection] == true
208
237
 
@@ -235,7 +264,8 @@ module Lutaml
235
264
  end
236
265
 
237
266
  def serialize(value, format, options = {})
238
- return if value.nil?
267
+ value ||= [] if collection? && initialize_empty?
268
+ return value if value.nil? || Utils.uninitialized?(value)
239
269
  return value if derived?
240
270
  return serialize_array(value, format, options) if value.is_a?(Array)
241
271
  return serialize_model(value, format, options) if type <= Serialize
@@ -244,27 +274,64 @@ module Lutaml
244
274
  end
245
275
 
246
276
  def cast(value, format, options = {})
247
- return value if type <= Serialize && value.is_a?(type.model)
248
-
249
- value ||= [] if collection?
277
+ value ||= [] if collection? && !value.nil?
250
278
  return value.map { |v| cast(v, format, options) } if value.is_a?(Array)
251
279
 
252
- if type <= Serialize && castable?(value, format)
253
- type.apply_mappings(value, format, options)
254
- elsif !value.nil? && !value.is_a?(type)
255
- type.send(:"from_#{format}", value)
280
+ return value if already_serialized?(type, value)
281
+
282
+ klass = resolve_polymorphic_class(type, value, options)
283
+
284
+ if can_serialize?(klass, value, format)
285
+ klass.apply_mappings(value, format, options)
286
+ elsif needs_conversion?(klass, value)
287
+ klass.send(:"from_#{format}", value)
256
288
  else
257
- type.cast(value)
289
+ klass.cast(value)
258
290
  end
259
291
  end
260
292
 
293
+ def deep_dup
294
+ self.class.new(name, type, Utils.deep_dup(options))
295
+ end
296
+
261
297
  private
262
298
 
299
+ def resolve_polymorphic_class(type, value, options)
300
+ return type unless polymorphic_map_defined?(options, value)
301
+
302
+ val = value[options[:polymorphic][:attribute]]
303
+ klass_name = options[:polymorphic][:class_map][val]
304
+ Object.const_get(klass_name)
305
+ end
306
+
307
+ def polymorphic_map_defined?(options, value)
308
+ !value.nil? &&
309
+ options[:polymorphic] &&
310
+ !options[:polymorphic].empty? &&
311
+ value[options[:polymorphic][:attribute]]
312
+ end
313
+
263
314
  def castable?(value, format)
264
315
  value.is_a?(Hash) ||
265
316
  (format == :xml && value.is_a?(Lutaml::Model::XmlAdapter::XmlElement))
266
317
  end
267
318
 
319
+ def castable_serialized_type?(value)
320
+ type <= Serialize && value.is_a?(type.model)
321
+ end
322
+
323
+ def can_serialize?(klass, value, format)
324
+ klass <= Serialize && castable?(value, format)
325
+ end
326
+
327
+ def needs_conversion?(klass, value)
328
+ !value.nil? && !value.is_a?(klass)
329
+ end
330
+
331
+ def already_serialized?(klass, value)
332
+ klass <= Serialize && value.is_a?(klass.model)
333
+ end
334
+
268
335
  def serialize_array(value, format, options)
269
336
  value.map { |v| serialize(v, format, options) }
270
337
  end
@@ -300,7 +367,7 @@ module Lutaml
300
367
 
301
368
  def set_default_for_collection
302
369
  validate_collection_range
303
- @options[:default] ||= -> { [] }
370
+ @options[:default] ||= -> { [] } if initialize_empty?
304
371
  end
305
372
 
306
373
  def validate_options!(options)
@@ -315,6 +382,10 @@ module Lutaml
315
382
  "`pattern` is only allowed for :string type"
316
383
  end
317
384
 
385
+ if initialize_empty? && !collection?
386
+ raise StandardError,
387
+ "Invalid option `initialize_empty` given without `collection: true` option"
388
+ end
318
389
  true
319
390
  end
320
391
 
@@ -325,6 +396,14 @@ module Lutaml
325
396
  raise ArgumentError,
326
397
  "Invalid type: #{type}, must be a Symbol, String or a Class"
327
398
  end
399
+
400
+ def polymorphic_enabled?
401
+ options[:polymorphic]&.any?
402
+ end
403
+
404
+ def valid_polymorphic_type?(value)
405
+ value.is_a?(type) && options[:polymorphic].include?(value.class)
406
+ end
328
407
  end
329
408
  end
330
409
  end
@@ -15,6 +15,13 @@ module Lutaml
15
15
  raise Lutaml::Model::InvalidChoiceRangeError.new(@min, @max) if @min.negative? || @max.negative?
16
16
  end
17
17
 
18
+ def ==(other)
19
+ @attributes == other.attributes &&
20
+ @min == other.min &&
21
+ @max == other.max &&
22
+ @model == other.model
23
+ end
24
+
18
25
  def attribute(name, type, options = {})
19
26
  options[:choice] = self
20
27
  @attributes << @model.attribute(name, type, options)
@@ -12,21 +12,61 @@ module Lutaml
12
12
  # Checks if two objects are equal based on their attributes
13
13
  # @param other [Object] The object to compare with
14
14
  # @return [Boolean] True if objects are equal, false otherwise
15
- def eql?(other)
16
- other.class == self.class &&
17
- self.class.attributes.all? do |attr, _|
18
- send(attr) == other.send(attr)
15
+ def eql?(other, compared_objects = {})
16
+ return true if equal?(other)
17
+ return false unless same_class?(other)
18
+ return true if already_compared?(other, compared_objects)
19
+
20
+ compared_objects[comparison_key(other)] = true
21
+ self.class.attributes.all? do |attr, _|
22
+ attr_value = send(attr)
23
+ other_value = other.send(attr)
24
+
25
+ if attr_value.respond_to?(:eql?) && same_class?(attr_value)
26
+ attr_value.eql?(other_value, compared_objects)
27
+ else
28
+ attr_value == other_value
19
29
  end
30
+ end
20
31
  end
21
32
 
22
33
  alias == eql?
23
34
 
35
+ def same_class?(other)
36
+ other.instance_of?(self.class)
37
+ end
38
+
39
+ def comparison_key(other)
40
+ "#{object_id}:#{other.object_id}"
41
+ end
42
+
43
+ def already_compared?(other, compared_objects)
44
+ compared_objects[comparison_key(other)]
45
+ end
46
+
24
47
  # Generates a hash value for the object
25
48
  # @return [Integer] The hash value
26
49
  def hash
27
- ([self.class] + self.class.attributes.map do |attr, _|
28
- send(attr).hash
29
- end).hash
50
+ calculate_hash
51
+ end
52
+
53
+ def calculate_hash(processed_objects = {}.compare_by_identity)
54
+ return if processed_objects.key?(self)
55
+
56
+ processed_objects[self] = true
57
+ ([self.class] + attributes_hash(processed_objects)).hash
58
+ end
59
+
60
+ def attributes_hash(processed_objects)
61
+ self.class.attributes.map do |attr, _|
62
+ attr_value = send(attr)
63
+
64
+ if attr_value.respond_to?(:calculate_hash)
65
+ attr_value.calculate_hash(processed_objects)
66
+ else
67
+ attr_value.hash
68
+ end
69
+ end
30
70
  end
31
71
 
32
72
  # Class methods added to the class that includes ComparableModel
@@ -419,8 +459,7 @@ module Lutaml
419
459
  # @param label [String] The label for the value
420
460
  # @param type_info [String, nil] Additional type information
421
461
  # @return [String] Formatted value tree
422
- def format_value_tree(value1, value2, parent_node, label,
423
- type_info = nil)
462
+ def format_value_tree(value1, value2, parent_node, label, type_info = nil)
424
463
  return if value1 == value2 && !@show_unchanged
425
464
 
426
465
  if value1 == value2
@@ -10,7 +10,7 @@ module Lutaml
10
10
  end
11
11
 
12
12
  def to_s
13
- "#{@attr_name} count is #{@value.size}, must be #{range_to_string}"
13
+ "#{@attr_name} count is #{@value&.size || 0}, must be #{range_to_string}"
14
14
  end
15
15
 
16
16
  private
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class PolymorphicError < Error
4
+ def initialize(value, options)
5
+ super("#{value.class} not in #{options[:polymorphic]}")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -26,3 +26,4 @@ require_relative "error/invalid_choice_range_error"
26
26
  require_relative "error/unknown_sequence_mapping_error"
27
27
  require_relative "error/choice_lower_bound_error"
28
28
  require_relative "error/no_root_namespace_error"
29
+ require_relative "error/polymorphic_error"
@@ -0,0 +1,17 @@
1
+ require_relative "key_value_mapping"
2
+
3
+ module Lutaml
4
+ module Model
5
+ class JsonMapping < KeyValueMapping
6
+ def initialize
7
+ super(:json)
8
+ end
9
+
10
+ def deep_dup
11
+ self.class.new.tap do |new_mapping|
12
+ new_mapping.instance_variable_set(:@mappings, duplicate_mappings)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,10 +3,11 @@ require_relative "key_value_mapping_rule"
3
3
  module Lutaml
4
4
  module Model
5
5
  class KeyValueMapping
6
- attr_reader :mappings
6
+ attr_reader :mappings, :format
7
7
 
8
- def initialize
8
+ def initialize(format = nil)
9
9
  @mappings = []
10
+ @format = format
10
11
  end
11
12
 
12
13
  def map(
@@ -14,25 +15,39 @@ module Lutaml
14
15
  to: nil,
15
16
  render_nil: false,
16
17
  render_default: false,
18
+ render_empty: false,
19
+ treat_nil: nil,
20
+ treat_empty: nil,
21
+ treat_omitted: nil,
17
22
  with: {},
18
23
  delegate: nil,
19
24
  child_mappings: nil,
20
25
  root_mappings: nil,
21
- transform: {}
26
+ polymorphic: {},
27
+ polymorphic_map: {},
28
+ transform: {},
29
+ value_map: {}
22
30
  )
23
31
  mapping_name = name_for_mapping(root_mappings, name)
24
- validate!(mapping_name, to, with)
32
+ validate!(mapping_name, to, with, render_nil, render_empty)
25
33
 
26
34
  @mappings << KeyValueMappingRule.new(
27
35
  mapping_name,
28
36
  to: to,
29
37
  render_nil: render_nil,
30
38
  render_default: render_default,
39
+ render_empty: render_empty,
40
+ treat_nil: treat_nil,
41
+ treat_empty: treat_empty,
42
+ treat_omitted: treat_omitted,
31
43
  with: with,
32
44
  delegate: delegate,
33
45
  child_mappings: child_mappings,
34
46
  root_mappings: root_mappings,
47
+ polymorphic: polymorphic,
48
+ polymorphic_map: polymorphic_map,
35
49
  transform: transform,
50
+ value_map: value_map,
36
51
  )
37
52
  end
38
53
 
@@ -46,7 +61,7 @@ module Lutaml
46
61
  delegate: nil
47
62
  )
48
63
  @raw_mapping = true
49
- validate!(Constants::RAW_MAPPING_KEY, to, with)
64
+ validate!(Constants::RAW_MAPPING_KEY, to, with, render_nil, nil)
50
65
  @mappings << KeyValueMappingRule.new(
51
66
  Constants::RAW_MAPPING_KEY,
52
67
  to: to,
@@ -65,28 +80,47 @@ module Lutaml
65
80
  name
66
81
  end
67
82
 
68
- def validate!(key, to, with)
83
+ def validate!(key, to, with, render_nil, render_empty)
69
84
  validate_mappings!(key)
85
+ validate_to_and_with_arguments!(key, to, with)
70
86
 
87
+ # Validate `render_nil` for unsupported value
88
+ validate_blank_mappings!(render_nil, render_empty)
89
+ validate_root_mappings!(key)
90
+ end
91
+
92
+ def validate_to_and_with_arguments!(key, to, with)
71
93
  if to.nil? && with.empty? && !@raw_mapping
72
- msg = ":to or :with argument is required for mapping '#{key}'"
73
- raise IncorrectMappingArgumentsError.new(msg)
94
+ raise IncorrectMappingArgumentsError.new(
95
+ ":to or :with argument is required for mapping '#{key}'",
96
+ )
74
97
  end
75
98
 
99
+ validate_with_options!(key, with)
100
+ end
101
+
102
+ def validate_with_options!(key, with)
76
103
  if !with.empty? && (with[:from].nil? || with[:to].nil?) && !@raw_mapping
77
- msg = ":with argument for mapping '#{key}' requires :to and :from keys"
78
- raise IncorrectMappingArgumentsError.new(msg)
104
+ raise IncorrectMappingArgumentsError.new(
105
+ ":with argument for mapping '#{key}' requires :to and :from keys",
106
+ )
79
107
  end
80
-
81
- validate_mappings(key)
82
108
  end
83
109
 
84
- def validate_mappings(name)
110
+ def validate_root_mappings!(name)
85
111
  if @mappings.any?(&:root_mapping?) || (name == "root_mapping" && @mappings.any?)
86
112
  raise MultipleMappingsError.new("root_mappings cannot be used with other mappings")
87
113
  end
88
114
  end
89
115
 
116
+ def validate_blank_mappings!(render_nil, render_empty)
117
+ if render_nil == :as_blank || render_empty == :as_blank
118
+ raise IncorrectMappingArgumentsError.new(
119
+ ":as_blank is not supported for key-value mappings",
120
+ )
121
+ end
122
+ end
123
+
90
124
  def validate_mappings!(_type)
91
125
  if (@raw_mapping && Utils.present?(@mappings)) || (!@raw_mapping && @mappings.any?(&:raw_mapping?))
92
126
  raise StandardError, "map_all is not allowed with other mappings"
@@ -94,7 +128,7 @@ module Lutaml
94
128
  end
95
129
 
96
130
  def deep_dup
97
- self.class.new.tap do |new_mapping|
131
+ self.class.new(@format).tap do |new_mapping|
98
132
  new_mapping.instance_variable_set(:@mappings, duplicate_mappings)
99
133
  end
100
134
  end
@@ -106,6 +140,16 @@ module Lutaml
106
140
  def find_by_to(to)
107
141
  @mappings.find { |m| m.to.to_s == to.to_s }
108
142
  end
143
+
144
+ def polymorphic_mapping
145
+ @mappings.find(&:polymorphic_mapping?)
146
+ end
147
+
148
+ Lutaml::Model::Config::KEY_VALUE_FORMATS.each do |format|
149
+ define_method(:"format_#{format}?") do
150
+ @format == format
151
+ end
152
+ end
109
153
  end
110
154
  end
111
155
  end
@@ -11,20 +11,34 @@ module Lutaml
11
11
  to:,
12
12
  render_nil: false,
13
13
  render_default: false,
14
+ render_empty: false,
15
+ treat_nil: :nil,
16
+ treat_empty: :empty,
17
+ treat_omitted: :nil,
14
18
  with: {},
15
19
  delegate: nil,
16
20
  child_mappings: nil,
17
21
  root_mappings: nil,
18
- transform: {}
22
+ polymorphic: {},
23
+ polymorphic_map: {},
24
+ transform: {},
25
+ value_map: {}
19
26
  )
20
27
  super(
21
28
  name,
22
29
  to: to,
23
30
  render_nil: render_nil,
24
31
  render_default: render_default,
32
+ render_empty: render_empty,
33
+ treat_nil: treat_nil,
34
+ treat_empty: treat_empty,
35
+ treat_omitted: treat_omitted,
25
36
  with: with,
26
37
  delegate: delegate,
27
- transform: transform
38
+ polymorphic: polymorphic,
39
+ polymorphic_map: polymorphic_map,
40
+ transform: transform,
41
+ value_map: value_map,
28
42
  )
29
43
 
30
44
  @child_mappings = child_mappings
@@ -42,9 +56,11 @@ module Lutaml
42
56
  name.dup,
43
57
  to: to.dup,
44
58
  render_nil: render_nil.dup,
59
+ render_empty: render_empty.dup,
45
60
  with: Utils.deep_dup(custom_methods),
46
61
  delegate: delegate,
47
62
  child_mappings: Utils.deep_dup(child_mappings),
63
+ value_map: Utils.deep_dup(@value_map),
48
64
  )
49
65
  end
50
66