lutaml-model 0.5.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,12 +209,16 @@ 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)
213
217
  end
214
218
  elsif type <= Serialize
215
- type.public_send(:"as_#{format}", value, options)
219
+ if Utils.present?(value)
220
+ type.public_send(:"as_#{format}", value, options)
221
+ end
216
222
  else
217
223
  # Convert to Value instance if not already
218
224
  value = type.new(value) unless value.is_a?(Type::Value)
@@ -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,28 +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)
24
+
22
25
  @mappings << KeyValueMappingRule.new(
23
- name,
26
+ mapping_name,
24
27
  to: to,
25
28
  render_nil: render_nil,
26
29
  render_default: render_default,
27
30
  with: with,
28
31
  delegate: delegate,
29
32
  child_mappings: child_mappings,
33
+ root_mappings: root_mappings,
30
34
  )
31
35
  end
32
36
 
33
37
  alias map_element map
34
38
 
39
+ def name_for_mapping(root_mappings, name)
40
+ return "root_mapping" if root_mappings
41
+
42
+ name
43
+ end
44
+
35
45
  def validate!(key, to, with)
36
46
  if to.nil? && with.empty?
37
47
  msg = ":to or :with argument is required for mapping '#{key}'"
@@ -42,6 +52,14 @@ module Lutaml
42
52
  msg = ":with argument for mapping '#{key}' requires :to and :from keys"
43
53
  raise IncorrectMappingArgumentsError.new(msg)
44
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
45
63
  end
46
64
 
47
65
  def deep_dup
@@ -53,6 +71,10 @@ module Lutaml
53
71
  def duplicate_mappings
54
72
  @mappings.map(&:deep_dup)
55
73
  end
74
+
75
+ def find_by_to(to)
76
+ @mappings.find { |m| m.to.to_s == to.to_s }
77
+ end
56
78
  end
57
79
  end
58
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,
@@ -13,7 +14,7 @@ module Lutaml
13
14
  with: {},
14
15
  delegate: nil,
15
16
  child_mappings: nil,
16
- id: nil
17
+ root_mappings: nil
17
18
  )
18
19
  super(
19
20
  name,
@@ -21,11 +22,17 @@ module Lutaml
21
22
  render_nil: render_nil,
22
23
  render_default: render_default,
23
24
  with: with,
24
- delegate: delegate,
25
- id: id
25
+ delegate: delegate
26
26
  )
27
27
 
28
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
29
36
  end
30
37
 
31
38
  def deep_dup
@@ -38,6 +45,10 @@ module Lutaml
38
45
  child_mappings: Utils.deep_dup(child_mappings),
39
46
  )
40
47
  end
48
+
49
+ def root_mapping?
50
+ name == "root_mapping"
51
+ end
41
52
  end
42
53
  end
43
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
@@ -17,7 +17,7 @@ module Lutaml
17
17
  with: {},
18
18
  attribute: false,
19
19
  delegate: nil,
20
- id: nil
20
+ root_mappings: nil
21
21
  )
22
22
  @name = name
23
23
  @to = to
@@ -26,7 +26,7 @@ module Lutaml
26
26
  @custom_methods = with
27
27
  @attribute = attribute
28
28
  @delegate = delegate
29
- @id = id
29
+ @root_mappings = root_mappings
30
30
  end
31
31
 
32
32
  alias from name
@@ -271,8 +271,10 @@ module Lutaml
271
271
 
272
272
  attribute = attributes[name]
273
273
 
274
+ next hash.merge!(generate_hash_from_child_mappings(value, format, rule.root_mappings)) if rule.root_mapping?
275
+
274
276
  value = if rule.child_mappings
275
- generate_hash_from_child_mappings(value, rule.child_mappings)
277
+ generate_hash_from_child_mappings(value, format, rule.child_mappings)
276
278
  else
277
279
  attribute.serialize(value, format, options)
278
280
  end
@@ -313,11 +315,11 @@ module Lutaml
313
315
  end
314
316
  end
315
317
 
316
- def apply_child_mappings(hash, child_mappings)
318
+ def translate_mappings(hash, child_mappings, attr, format)
317
319
  return hash unless child_mappings
318
320
 
319
321
  hash.map do |key, value|
320
- child_mappings.to_h do |attr_name, path|
322
+ child_hash = child_mappings.to_h do |attr_name, path|
321
323
  attr_value = if path == :key
322
324
  key
323
325
  elsif path == :value
@@ -327,32 +329,68 @@ module Lutaml
327
329
  value.dig(*path.map(&:to_s))
328
330
  end
329
331
 
330
- [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]
334
+ end
335
+
336
+ if child_mappings.values == [:key] && hash.values.all?(Hash)
337
+ child_hash.merge!(value)
331
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
+ )
332
346
  end
333
347
  end
334
348
 
335
- def generate_hash_from_child_mappings(value, child_mappings)
349
+ def generate_hash_from_child_mappings(value, format, child_mappings)
336
350
  return value unless child_mappings
337
351
 
338
352
  hash = {}
339
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
+
340
365
  value.each do |child_obj|
341
366
  map_key = nil
342
367
  map_value = {}
343
368
  child_mappings.each do |attr_name, path|
369
+ attr_value = child_obj.send(attr_name)
370
+
371
+ attr_value = if attr_value.is_a?(Lutaml::Model::Serialize)
372
+ attr_value.to_yaml_hash
373
+ elsif attr_value.is_a?(Array) && attr_value.first.is_a?(Lutaml::Model::Serialize)
374
+ attr_value.map(&:to_yaml_hash)
375
+ else
376
+ attr_value
377
+ end
378
+
379
+ next if Utils.blank?(attr_value)
380
+
344
381
  if path == :key
345
- map_key = child_obj.send(attr_name)
382
+ map_key = attr_value
346
383
  elsif path == :value
347
- map_value = child_obj.send(attr_name)
384
+ map_value = attr_value
348
385
  else
349
386
  path = [path] unless path.is_a?(Array)
350
387
  path[0...-1].inject(map_value) do |acc, k|
351
388
  acc[k.to_s] ||= {}
352
- end.public_send(:[]=, path.last.to_s, child_obj.send(attr_name))
389
+ end.public_send(:[]=, path.last.to_s, attr_value)
353
390
  end
354
391
  end
355
392
 
393
+ map_value = nil if map_value.empty?
356
394
  hash[map_key] = map_value
357
395
  end
358
396
 
@@ -380,12 +418,15 @@ module Lutaml
380
418
  def apply_mappings(doc, format, options = {})
381
419
  instance = options[:instance] || model.new
382
420
  return instance if Utils.blank?(doc)
421
+
422
+ options[:mappings] = mappings_for(format).mappings
383
423
  return apply_xml_mapping(doc, instance, options) if format == :xml
384
424
 
385
425
  apply_hash_mapping(doc, instance, format, options)
386
426
  end
387
427
 
388
428
  def apply_xml_mapping(doc, instance, options = {})
429
+ options = Utils.deep_dup(options)
389
430
  instance.encoding = options[:encoding]
390
431
  return instance unless doc
391
432
 
@@ -393,10 +434,10 @@ module Lutaml
393
434
  options[:default_namespace] =
394
435
  mappings_for(:xml)&.namespace_uri
395
436
  end
396
- mappings = mappings_for(:xml).mappings
437
+ mappings = options[:mappings] || mappings_for(:xml).mappings
397
438
 
398
439
  if doc.is_a?(Array)
399
- raise "May be `collection: true` is missing for #{self} in #{options[:caller_class]}"
440
+ raise Lutaml::Model::CollectionTrueMissingError(self, option[:caller_class])
400
441
  end
401
442
 
402
443
  if instance.respond_to?(:ordered=) && doc.is_a?(Lutaml::Model::MappingHash)
@@ -443,8 +484,8 @@ module Lutaml
443
484
  instance
444
485
  end
445
486
 
446
- def apply_hash_mapping(doc, instance, format, _options = {})
447
- mappings = mappings_for(format).mappings
487
+ def apply_hash_mapping(doc, instance, format, options = {})
488
+ mappings = options[:mappings] || mappings_for(format).mappings
448
489
  mappings.each do |rule|
449
490
  raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
450
491
 
@@ -453,7 +494,9 @@ module Lutaml
453
494
  names = rule.multiple_mappings? ? rule.name : [rule.name]
454
495
 
455
496
  value = names.collect do |rule_name|
456
- if doc.key?(rule_name.to_s)
497
+ if rule.root_mapping?
498
+ doc
499
+ elsif doc.key?(rule_name.to_s)
457
500
  doc[rule_name.to_s]
458
501
  elsif doc.key?(rule_name.to_sym)
459
502
  doc[rule_name.to_sym]
@@ -470,8 +513,9 @@ module Lutaml
470
513
  next
471
514
  end
472
515
 
473
- value = apply_child_mappings(value, rule.child_mappings)
474
- value = attr.cast(value, format)
516
+ value = translate_mappings(value, rule.hash_mappings, attr, format)
517
+ value = attr.cast(value, format) unless rule.hash_mappings
518
+ attr.valid_collection!(value, self)
475
519
 
476
520
  rule.deserialize(instance, value, attributes, self)
477
521
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.5.0"
5
+ VERSION = "0.5.2"
6
6
  end
7
7
  end
@@ -109,9 +109,9 @@ module Lutaml
109
109
  prefix: attr.namespace_prefix,
110
110
  schema_location: attr.value,
111
111
  }
112
- else
113
- result[attr.namespaced_name] = attr.value
114
112
  end
113
+
114
+ result[attr.namespaced_name] = attr.value
115
115
  end
116
116
 
117
117
  result
@@ -328,7 +328,7 @@ module Lutaml
328
328
  {}
329
329
  end
330
330
 
331
- if element.respond_to?(:schema_location) && element.schema_location && !options[:except]&.include?(:schema_location)
331
+ if element.respond_to?(:schema_location) && element.schema_location.is_a?(Lutaml::Model::SchemaLocation) && !options[:except]&.include?(:schema_location)
332
332
  attrs.merge!(element.schema_location.to_xml_attributes)
333
333
  end
334
334
 
@@ -3,6 +3,8 @@ require_relative "xml_mapping_rule"
3
3
  module Lutaml
4
4
  module Model
5
5
  class XmlMapping
6
+ include Lutaml::Model::Loggable
7
+
6
8
  TYPES = {
7
9
  attribute: :map_attribute,
8
10
  element: :map_element,
@@ -92,6 +94,8 @@ module Lutaml
92
94
  nil)
93
95
  )
94
96
  validate!(name, to, with, type: TYPES[:attribute])
97
+ warn_auto_handling(name) if name == "schemaLocation"
98
+
95
99
  rule = XmlMappingRule.new(
96
100
  name,
97
101
  to: to,
@@ -19,8 +19,7 @@ module Lutaml
19
19
  namespace_set: false,
20
20
  prefix_set: false,
21
21
  attribute: false,
22
- default_namespace: nil,
23
- id: nil
22
+ default_namespace: nil
24
23
  )
25
24
  super(
26
25
  name,
@@ -29,8 +28,7 @@ module Lutaml
29
28
  render_default: render_default,
30
29
  with: with,
31
30
  delegate: delegate,
32
- attribute: attribute,
33
- id: id
31
+ attribute: attribute
34
32
  )
35
33
 
36
34
  @namespace = if namespace.to_s == "inherit"
@@ -47,7 +45,6 @@ module Lutaml
47
45
 
48
46
  @namespace_set = namespace_set
49
47
  @prefix_set = prefix_set
50
- @id = id
51
48
  end
52
49
 
53
50
  def namespace_set?
data/lib/lutaml/model.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "model/version"
4
+ require_relative "model/loggable"
4
5
  require_relative "model/type"
5
6
  require_relative "model/utils"
6
7
  require_relative "model/serializable"