lutaml-model 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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?