lutaml-model 0.4.0 → 0.5.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +36 -20
  3. data/README.adoc +1003 -192
  4. data/lib/lutaml/model/attribute.rb +6 -2
  5. data/lib/lutaml/model/error/collection_true_missing_error.rb +16 -0
  6. data/lib/lutaml/model/error/multiple_mappings_error.rb +6 -0
  7. data/lib/lutaml/model/error.rb +2 -0
  8. data/lib/lutaml/model/key_value_mapping.rb +25 -4
  9. data/lib/lutaml/model/key_value_mapping_rule.rb +16 -3
  10. data/lib/lutaml/model/loggable.rb +15 -0
  11. data/lib/lutaml/model/mapping_rule.rb +14 -2
  12. data/lib/lutaml/model/serialize.rb +114 -64
  13. data/lib/lutaml/model/type/decimal.rb +5 -0
  14. data/lib/lutaml/model/version.rb +1 -1
  15. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +1 -0
  16. data/lib/lutaml/model/xml_adapter/builder/oga.rb +180 -0
  17. data/lib/lutaml/model/xml_adapter/builder/ox.rb +1 -0
  18. data/lib/lutaml/model/xml_adapter/oga/document.rb +20 -0
  19. data/lib/lutaml/model/xml_adapter/oga/element.rb +117 -0
  20. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +77 -44
  21. data/lib/lutaml/model/xml_adapter/xml_document.rb +14 -12
  22. data/lib/lutaml/model/xml_mapping.rb +3 -0
  23. data/lib/lutaml/model/xml_mapping_rule.rb +13 -4
  24. data/lib/lutaml/model.rb +1 -0
  25. data/spec/address_spec.rb +1 -0
  26. data/spec/fixtures/sample_model.rb +7 -0
  27. data/spec/lutaml/model/custom_model_spec.rb +47 -1
  28. data/spec/lutaml/model/included_spec.rb +192 -0
  29. data/spec/lutaml/model/mixed_content_spec.rb +48 -32
  30. data/spec/lutaml/model/multiple_mapping_spec.rb +329 -0
  31. data/spec/lutaml/model/ordered_content_spec.rb +1 -1
  32. data/spec/lutaml/model/render_nil_spec.rb +3 -0
  33. data/spec/lutaml/model/root_mappings_spec.rb +297 -0
  34. data/spec/lutaml/model/serializable_spec.rb +42 -7
  35. data/spec/lutaml/model/type/boolean_spec.rb +62 -0
  36. data/spec/lutaml/model/with_child_mapping_spec.rb +182 -0
  37. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -11
  38. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +67 -1
  39. data/spec/lutaml/model/xml_adapter_spec.rb +2 -2
  40. data/spec/lutaml/model/xml_mapping_spec.rb +32 -9
  41. data/spec/sample_model_spec.rb +114 -0
  42. metadata +12 -2
@@ -142,7 +142,7 @@ module Lutaml
142
142
  # Use the default value if the value is nil
143
143
  value = default if value.nil?
144
144
 
145
- valid_value!(value) && valid_collection!(value) && valid_pattern!(value)
145
+ valid_value!(value) && valid_collection!(value, self) && valid_pattern!(value)
146
146
  end
147
147
 
148
148
  def validate_collection_range
@@ -169,7 +169,9 @@ module Lutaml
169
169
  end
170
170
  end
171
171
 
172
- def valid_collection!(value)
172
+ def valid_collection!(value, caller)
173
+ raise Lutaml::Model::CollectionTrueMissingError.new(name, caller) if value.is_a?(Array) && !collection?
174
+
173
175
  return true unless collection?
174
176
 
175
177
  # Allow nil values for collections during initialization
@@ -207,6 +209,8 @@ module Lutaml
207
209
  end
208
210
 
209
211
  def serialize(value, format, options = {})
212
+ return if value.nil?
213
+
210
214
  if value.is_a?(Array)
211
215
  value.map do |v|
212
216
  serialize(v, format, options)
@@ -0,0 +1,16 @@
1
+ module Lutaml
2
+ module Model
3
+ class CollectionTrueMissingError < Error
4
+ def initialize(attr_name, caller_class)
5
+ @attr_name = attr_name
6
+ @caller = caller_class
7
+
8
+ super()
9
+ end
10
+
11
+ def to_s
12
+ "May be `collection: true` is missing for `#{@attr_name}` in #{@caller}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ module Lutaml
2
+ module Model
3
+ class MultipleMappingsError < Error
4
+ end
5
+ end
6
+ end
@@ -14,3 +14,5 @@ require_relative "error/validation_error"
14
14
  require_relative "error/type_not_enabled_error"
15
15
  require_relative "error/type_error"
16
16
  require_relative "error/unknown_type_error"
17
+ require_relative "error/multiple_mappings_error"
18
+ require_relative "error/collection_true_missing_error"
@@ -10,29 +10,38 @@ module Lutaml
10
10
  end
11
11
 
12
12
  def map(
13
- name,
13
+ name = nil,
14
14
  to: nil,
15
15
  render_nil: false,
16
16
  render_default: false,
17
17
  with: {},
18
18
  delegate: nil,
19
- child_mappings: nil
19
+ child_mappings: nil,
20
+ root_mappings: nil
20
21
  )
21
- validate!(name, to, with)
22
+ mapping_name = name_for_mapping(root_mappings, name)
23
+ validate!(mapping_name, to, with)
22
24
 
23
25
  @mappings << KeyValueMappingRule.new(
24
- name,
26
+ mapping_name,
25
27
  to: to,
26
28
  render_nil: render_nil,
27
29
  render_default: render_default,
28
30
  with: with,
29
31
  delegate: delegate,
30
32
  child_mappings: child_mappings,
33
+ root_mappings: root_mappings,
31
34
  )
32
35
  end
33
36
 
34
37
  alias map_element map
35
38
 
39
+ def name_for_mapping(root_mappings, name)
40
+ return "root_mapping" if root_mappings
41
+
42
+ name
43
+ end
44
+
36
45
  def validate!(key, to, with)
37
46
  if to.nil? && with.empty?
38
47
  msg = ":to or :with argument is required for mapping '#{key}'"
@@ -43,6 +52,14 @@ module Lutaml
43
52
  msg = ":with argument for mapping '#{key}' requires :to and :from keys"
44
53
  raise IncorrectMappingArgumentsError.new(msg)
45
54
  end
55
+
56
+ validate_mappings(key)
57
+ end
58
+
59
+ def validate_mappings(name)
60
+ if @mappings.any?(&:root_mapping?) || (name == "root_mapping" && @mappings.any?)
61
+ raise MultipleMappingsError.new("root_mappings cannot be used with other mappings")
62
+ end
46
63
  end
47
64
 
48
65
  def deep_dup
@@ -54,6 +71,10 @@ module Lutaml
54
71
  def duplicate_mappings
55
72
  @mappings.map(&:deep_dup)
56
73
  end
74
+
75
+ def find_by_to(to)
76
+ @mappings.find { |m| m.to.to_s == to.to_s }
77
+ end
57
78
  end
58
79
  end
59
80
  end
@@ -3,7 +3,8 @@ require_relative "mapping_rule"
3
3
  module Lutaml
4
4
  module Model
5
5
  class KeyValueMappingRule < MappingRule
6
- attr_reader :child_mappings
6
+ attr_reader :child_mappings,
7
+ :root_mappings
7
8
 
8
9
  def initialize(
9
10
  name,
@@ -12,7 +13,8 @@ module Lutaml
12
13
  render_default: false,
13
14
  with: {},
14
15
  delegate: nil,
15
- child_mappings: nil
16
+ child_mappings: nil,
17
+ root_mappings: nil
16
18
  )
17
19
  super(
18
20
  name,
@@ -20,10 +22,17 @@ module Lutaml
20
22
  render_nil: render_nil,
21
23
  render_default: render_default,
22
24
  with: with,
23
- delegate: delegate,
25
+ delegate: delegate
24
26
  )
25
27
 
26
28
  @child_mappings = child_mappings
29
+ @root_mappings = root_mappings
30
+ end
31
+
32
+ def hash_mappings
33
+ return @root_mappings if @root_mappings
34
+
35
+ @child_mappings
27
36
  end
28
37
 
29
38
  def deep_dup
@@ -36,6 +45,10 @@ module Lutaml
36
45
  child_mappings: Utils.deep_dup(child_mappings),
37
46
  )
38
47
  end
48
+
49
+ def root_mapping?
50
+ name == "root_mapping"
51
+ end
39
52
  end
40
53
  end
41
54
  end
@@ -0,0 +1,15 @@
1
+ module Lutaml
2
+ module Model
3
+ module Loggable
4
+ def self.included(base)
5
+ base.define_method :warn_auto_handling do |name|
6
+ caller_file = File.basename(caller_locations(2, 1)[0].path)
7
+ caller_line = caller_locations(2, 1)[0].lineno
8
+
9
+ str = "[Lutaml::Model] WARN: `#{name}` is handled by default. No need to explecitly define at `#{caller_file}:#{caller_line}`"
10
+ warn(str)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -16,7 +16,8 @@ module Lutaml
16
16
  render_default: false,
17
17
  with: {},
18
18
  attribute: false,
19
- delegate: nil
19
+ delegate: nil,
20
+ root_mappings: nil
20
21
  )
21
22
  @name = name
22
23
  @to = to
@@ -25,6 +26,7 @@ module Lutaml
25
26
  @custom_methods = with
26
27
  @attribute = attribute
27
28
  @delegate = delegate
29
+ @root_mappings = root_mappings
28
30
  end
29
31
 
30
32
  alias from name
@@ -42,6 +44,8 @@ module Lutaml
42
44
  if delegate
43
45
  model.public_send(delegate).public_send(to)
44
46
  else
47
+ return if to.nil?
48
+
45
49
  model.public_send(to)
46
50
  end
47
51
  end
@@ -56,7 +60,7 @@ module Lutaml
56
60
 
57
61
  def deserialize(model, value, attributes, mapper_class = nil)
58
62
  if custom_methods[:from]
59
- mapper_class.new.send(custom_methods[:from], model, value)
63
+ mapper_class.new.send(custom_methods[:from], model, value) unless value.nil?
60
64
  elsif delegate
61
65
  if model.public_send(delegate).nil?
62
66
  model.public_send(:"#{delegate}=", attributes[delegate].type.new)
@@ -68,6 +72,14 @@ module Lutaml
68
72
  end
69
73
  end
70
74
 
75
+ def using_custom_methods?
76
+ !custom_methods.empty?
77
+ end
78
+
79
+ def multiple_mappings?
80
+ name.is_a?(Array)
81
+ end
82
+
71
83
  def deep_dup
72
84
  raise NotImplementedError, "Subclasses must implement `deep_dup`."
73
85
  end
@@ -21,6 +21,7 @@ module Lutaml
21
21
 
22
22
  def self.included(base)
23
23
  base.extend(ClassMethods)
24
+ base.initialize_attrs(base)
24
25
  end
25
26
 
26
27
  module ClassMethods
@@ -28,14 +29,18 @@ module Lutaml
28
29
 
29
30
  def inherited(subclass)
30
31
  super
32
+ subclass.initialize_attrs(self)
33
+ end
31
34
 
32
- @mappings ||= {}
33
- @attributes ||= {}
35
+ def included(base)
36
+ base.extend(ClassMethods)
37
+ base.initialize_attrs(self)
38
+ end
34
39
 
35
- subclass.instance_variable_set(:@attributes,
36
- Utils.deep_dup(@attributes))
37
- subclass.instance_variable_set(:@mappings, Utils.deep_dup(@mappings))
38
- subclass.instance_variable_set(:@model, subclass)
40
+ def initialize_attrs(source_class)
41
+ @mappings = Utils.deep_dup(source_class.instance_variable_get(:@mappings)) || {}
42
+ @attributes = Utils.deep_dup(source_class.instance_variable_get(:@attributes)) || {}
43
+ instance_variable_set(:@model, self)
39
44
  end
40
45
 
41
46
  def model(klass = nil)
@@ -264,15 +269,20 @@ module Lutaml
264
269
 
265
270
  value = instance.send(name)
266
271
 
267
- next if Utils.blank?(value) && !rule.render_nil
268
-
269
272
  attribute = attributes[name]
270
273
 
271
- hash[rule.from.to_s] = if rule.child_mappings
272
- generate_hash_from_child_mappings(value, rule.child_mappings)
273
- else
274
- attribute.serialize(value, format, options)
275
- end
274
+ next hash.merge!(generate_hash_from_child_mappings(value, format, rule.root_mappings)) if rule.root_mapping?
275
+
276
+ value = if rule.child_mappings
277
+ generate_hash_from_child_mappings(value, format, rule.child_mappings)
278
+ else
279
+ attribute.serialize(value, format, options)
280
+ end
281
+
282
+ next if Utils.blank?(value) && !rule.render_nil
283
+
284
+ rule_from_name = rule.multiple_mappings? ? rule.from.first.to_s : rule.from.to_s
285
+ hash[rule_from_name] = value
276
286
  end
277
287
  end
278
288
 
@@ -282,39 +292,14 @@ module Lutaml
282
292
  return if value.nil? && !rule.render_nil
283
293
 
284
294
  attribute = instance.send(rule.delegate).class.attributes[name]
285
- hash[rule.from.to_s] = attribute.serialize(value, format)
295
+ rule_from_name = rule.multiple_mappings? ? rule.from.first.to_s : rule.from.to_s
296
+ hash[rule_from_name] = attribute.serialize(value, format)
286
297
  end
287
298
 
288
299
  def mappings_for(format)
289
300
  mappings[format] || default_mappings(format)
290
301
  end
291
302
 
292
- def attr_value(attrs, name, attr_rule)
293
- value = if attrs.key?(name.to_sym)
294
- attrs[name.to_sym]
295
- elsif attrs.key?(name.to_s)
296
- attrs[name.to_s]
297
- else
298
- attr_rule.default
299
- end
300
-
301
- if attr_rule.collection? || value.is_a?(Array)
302
- (value || []).map do |v|
303
- if v.is_a?(Hash)
304
- attr_rule.type.new(v)
305
- else
306
- # TODO: This code is problematic because Type.cast does not know
307
- # about all the types.
308
- Lutaml::Model::Type.cast(v, attr_rule.type)
309
- end
310
- end
311
- else
312
- # TODO: This code is problematic because Type.cast does not know
313
- # about all the types.
314
- Lutaml::Model::Type.cast(value, attr_rule.type)
315
- end
316
- end
317
-
318
303
  def default_mappings(format)
319
304
  klass = format == :xml ? XmlMapping : KeyValueMapping
320
305
 
@@ -330,11 +315,11 @@ module Lutaml
330
315
  end
331
316
  end
332
317
 
333
- def apply_child_mappings(hash, child_mappings)
318
+ def translate_mappings(hash, child_mappings, attr, format)
334
319
  return hash unless child_mappings
335
320
 
336
321
  hash.map do |key, value|
337
- child_mappings.to_h do |attr_name, path|
322
+ child_hash = child_mappings.to_h do |attr_name, path|
338
323
  attr_value = if path == :key
339
324
  key
340
325
  elsif path == :value
@@ -344,32 +329,59 @@ module Lutaml
344
329
  value.dig(*path.map(&:to_s))
345
330
  end
346
331
 
347
- [attr_name, attr_value]
332
+ attr_rule = attr.type.mappings_for(format).find_by_to(attr_name)
333
+ [attr_rule.from.to_s, attr_value]
348
334
  end
335
+
336
+ if child_mappings.values == [:key] && hash.values.all?(Hash)
337
+ child_hash.merge!(value)
338
+ end
339
+
340
+ attr.type.apply_hash_mapping(
341
+ child_hash,
342
+ attr.type.model.new,
343
+ format,
344
+ { mappings: attr.type.mappings_for(format).mappings },
345
+ )
349
346
  end
350
347
  end
351
348
 
352
- def generate_hash_from_child_mappings(value, child_mappings)
349
+ def generate_hash_from_child_mappings(value, format, child_mappings)
353
350
  return value unless child_mappings
354
351
 
355
352
  hash = {}
356
353
 
354
+ if child_mappings.values == [:key]
355
+ klass = value.first.class
356
+ mappings = klass.mappings_for(format)
357
+
358
+ klass.attributes.each_key do |name|
359
+ next if child_mappings.key?(name.to_sym) || child_mappings.key?(name.to_s)
360
+
361
+ child_mappings[name.to_sym] = mappings.find_by_to(name)&.name.to_s || name.to_s
362
+ end
363
+ end
364
+
357
365
  value.each do |child_obj|
358
366
  map_key = nil
359
367
  map_value = {}
360
368
  child_mappings.each do |attr_name, path|
369
+ attr_value = child_obj.send(attr_name)
370
+ attr_value = attr_value.to_yaml_hash if attr_value.is_a?(Lutaml::Model::Serialize)
371
+
361
372
  if path == :key
362
- map_key = child_obj.send(attr_name)
373
+ map_key = attr_value
363
374
  elsif path == :value
364
- map_value = child_obj.send(attr_name)
375
+ map_value = attr_value
365
376
  else
366
377
  path = [path] unless path.is_a?(Array)
367
378
  path[0...-1].inject(map_value) do |acc, k|
368
379
  acc[k.to_s] ||= {}
369
- end.public_send(:[]=, path.last.to_s, child_obj.send(attr_name))
380
+ end.public_send(:[]=, path.last.to_s, attr_value)
370
381
  end
371
382
  end
372
383
 
384
+ map_value = nil if map_value.empty?
373
385
  hash[map_key] = map_value
374
386
  end
375
387
 
@@ -397,12 +409,15 @@ module Lutaml
397
409
  def apply_mappings(doc, format, options = {})
398
410
  instance = options[:instance] || model.new
399
411
  return instance if Utils.blank?(doc)
412
+
413
+ options[:mappings] = mappings_for(format).mappings
400
414
  return apply_xml_mapping(doc, instance, options) if format == :xml
401
415
 
402
416
  apply_hash_mapping(doc, instance, format, options)
403
417
  end
404
418
 
405
419
  def apply_xml_mapping(doc, instance, options = {})
420
+ options = Utils.deep_dup(options)
406
421
  instance.encoding = options[:encoding]
407
422
  return instance unless doc
408
423
 
@@ -410,11 +425,10 @@ module Lutaml
410
425
  options[:default_namespace] =
411
426
  mappings_for(:xml)&.namespace_uri
412
427
  end
413
- mappings = mappings_for(:xml).mappings
428
+ mappings = options[:mappings] || mappings_for(:xml).mappings
414
429
 
415
430
  if doc.is_a?(Array)
416
- raise "May be `collection: true` is" \
417
- "missing for #{self} in #{options[:caller_class]}"
431
+ raise Lutaml::Model::CollectionTrueMissingError(self, option[:caller_class])
418
432
  end
419
433
 
420
434
  if instance.respond_to?(:ordered=) && doc.is_a?(Lutaml::Model::MappingHash)
@@ -438,17 +452,18 @@ module Lutaml
438
452
 
439
453
  attr = attribute_for_rule(rule)
440
454
 
455
+ namespaced_names = rule.namespaced_names(options[:default_namespace])
456
+
441
457
  value = if rule.raw_mapping?
442
458
  doc.node.inner_xml
443
459
  elsif rule.content_mapping?
444
460
  doc[rule.content_key]
445
- elsif doc.key_exist?(rule.namespaced_name(options[:default_namespace]))
446
- doc.fetch(rule.namespaced_name(options[:default_namespace]))
461
+ elsif key = (namespaced_names & doc.keys).first
462
+ doc[key]
447
463
  else
448
464
  defaults_used << rule.to
449
465
  attr&.default || rule.to_value_for(instance)
450
466
  end
451
-
452
467
  value = normalize_xml_value(value, rule, attr, options)
453
468
  rule.deserialize(instance, value, attributes, self)
454
469
  end
@@ -460,20 +475,28 @@ module Lutaml
460
475
  instance
461
476
  end
462
477
 
463
- def apply_hash_mapping(doc, instance, format, _options = {})
464
- mappings = mappings_for(format).mappings
478
+ def apply_hash_mapping(doc, instance, format, options = {})
479
+ mappings = options[:mappings] || mappings_for(format).mappings
465
480
  mappings.each do |rule|
466
481
  raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
467
482
 
468
483
  attr = attribute_for_rule(rule)
469
484
 
470
- value = if doc.key?(rule.name.to_s) || doc.key?(rule.name.to_sym)
471
- doc[rule.name.to_s] || doc[rule.name.to_sym]
472
- else
473
- attr&.default
474
- end
485
+ names = rule.multiple_mappings? ? rule.name : [rule.name]
486
+
487
+ value = names.collect do |rule_name|
488
+ if rule.root_mapping?
489
+ doc
490
+ elsif doc.key?(rule_name.to_s)
491
+ doc[rule_name.to_s]
492
+ elsif doc.key?(rule_name.to_sym)
493
+ doc[rule_name.to_sym]
494
+ else
495
+ attr&.default
496
+ end
497
+ end.compact.first
475
498
 
476
- if rule.custom_methods[:from]
499
+ if rule.using_custom_methods?
477
500
  if Utils.present?(value)
478
501
  value = new.send(rule.custom_methods[:from], instance, value)
479
502
  end
@@ -481,8 +504,9 @@ module Lutaml
481
504
  next
482
505
  end
483
506
 
484
- value = apply_child_mappings(value, rule.child_mappings)
485
- value = attr.cast(value, format)
507
+ value = translate_mappings(value, rule.hash_mappings, attr, format)
508
+ value = attr.cast(value, format) unless rule.hash_mappings
509
+ attr.valid_collection!(value, self)
486
510
 
487
511
  rule.deserialize(instance, value, attributes, self)
488
512
  end
@@ -567,7 +591,7 @@ module Lutaml
567
591
 
568
592
  self.class.attributes.each do |name, attr|
569
593
  value = if attrs.key?(name) || attrs.key?(name.to_s)
570
- self.class.attr_value(attrs, name, attr)
594
+ attr_value(attrs, name, attr)
571
595
  else
572
596
  using_default_for(name)
573
597
  attr.default
@@ -584,6 +608,32 @@ module Lutaml
584
608
  end
585
609
  end
586
610
 
611
+ def attr_value(attrs, name, attr_rule)
612
+ value = if attrs.key?(name.to_sym)
613
+ attrs[name.to_sym]
614
+ elsif attrs.key?(name.to_s)
615
+ attrs[name.to_s]
616
+ else
617
+ attr_rule.default
618
+ end
619
+
620
+ if attr_rule.collection? || value.is_a?(Array)
621
+ (value || []).map do |v|
622
+ if v.is_a?(Hash)
623
+ attr_rule.type.new(v)
624
+ else
625
+ # TODO: This code is problematic because Type.cast does not know
626
+ # about all the types.
627
+ Lutaml::Model::Type.cast(v, attr_rule.type)
628
+ end
629
+ end
630
+ else
631
+ # TODO: This code is problematic because Type.cast does not know
632
+ # about all the types.
633
+ Lutaml::Model::Type.cast(value, attr_rule.type)
634
+ end
635
+ end
636
+
587
637
  def using_default_for(attribute_name)
588
638
  @using_default[attribute_name] = true
589
639
  end
@@ -36,6 +36,11 @@ module Lutaml
36
36
  raise TypeNotEnabledError.new("Decimal", value)
37
37
  end
38
38
  end
39
+
40
+ # Override to avoid serializing ruby object in YAML
41
+ def to_yaml
42
+ value&.to_s("F")
43
+ end
39
44
  end
40
45
  end
41
46
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.1"
6
6
  end
7
7
  end
@@ -39,6 +39,7 @@ module Lutaml
39
39
  )
40
40
  add_namespace_prefix(prefix)
41
41
 
42
+ element_name = element_name.first if element_name.is_a?(Array)
42
43
  element_name = "#{element_name}_" if respond_to?(element_name)
43
44
 
44
45
  if block_given?