lutaml-model 0.3.10 → 0.3.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,37 @@ require_relative "mapping_rule"
3
3
  module Lutaml
4
4
  module Model
5
5
  class KeyValueMappingRule < MappingRule
6
- # No additional attributes or methods required for now
6
+ attr_reader :child_mappings
7
+
8
+ def initialize(
9
+ name,
10
+ to:,
11
+ render_nil: false,
12
+ with: {},
13
+ delegate: nil,
14
+ child_mappings: nil
15
+ )
16
+ super(
17
+ name,
18
+ to: to,
19
+ render_nil: render_nil,
20
+ with: with,
21
+ delegate: delegate,
22
+ )
23
+
24
+ @child_mappings = child_mappings
25
+ end
26
+
27
+ def deep_dup
28
+ self.class.new(
29
+ name.dup,
30
+ to: to.dup,
31
+ render_nil: render_nil.dup,
32
+ with: Utils.deep_dup(custom_methods),
33
+ delegate: delegate,
34
+ child_mappings: Utils.deep_dup(child_mappings),
35
+ )
36
+ end
7
37
  end
8
38
  end
9
39
  end
@@ -14,6 +14,14 @@ module Lutaml
14
14
  @item_order&.map { |key| normalize(key) } || keys
15
15
  end
16
16
 
17
+ def fetch(key)
18
+ self[key.to_s] || self[key.to_sym]
19
+ end
20
+
21
+ def key_exist?(key)
22
+ key?(key.to_s) || key?(key.to_sym)
23
+ end
24
+
17
25
  def item_order=(order)
18
26
  raise "`item order` must be an array" unless order.is_a?(Array)
19
27
 
@@ -24,6 +32,20 @@ module Lutaml
24
32
  @ordered
25
33
  end
26
34
 
35
+ def method_missing(method_name, *args)
36
+ value = self[method_name] || self[method_name.to_s]
37
+ return value if value
38
+
39
+ super
40
+ end
41
+
42
+ def respond_to_missing?(method_name, include_private = false)
43
+ key_present = key?(method_name) || key?(method_name.to_s)
44
+ return true if key_present
45
+
46
+ super
47
+ end
48
+
27
49
  private
28
50
 
29
51
  def normalize(key)
@@ -5,59 +5,65 @@ module Lutaml
5
5
  :to,
6
6
  :render_nil,
7
7
  :custom_methods,
8
- :delegate,
9
- :mixed_content,
10
- :child_mappings
8
+ :delegate
11
9
 
12
10
  def initialize(
13
11
  name,
14
12
  to:,
15
13
  render_nil: false,
16
14
  with: {},
17
- delegate: nil,
18
- mixed_content: false,
19
- namespace_set: false,
20
- child_mappings: nil
15
+ attribute: false,
16
+ delegate: nil
21
17
  )
22
18
  @name = name
23
19
  @to = to
24
20
  @render_nil = render_nil
25
21
  @custom_methods = with
22
+ @attribute = attribute
26
23
  @delegate = delegate
27
- @mixed_content = mixed_content
28
- @namespace_set = namespace_set
29
- @child_mappings = child_mappings
30
24
  end
31
25
 
32
26
  alias from name
33
27
  alias render_nil? render_nil
34
28
 
35
- def prefixed_name
36
- if prefix
37
- "#{prefix}:#{name}"
29
+ def serialize_attribute(model, element, doc)
30
+ if custom_methods[:to]
31
+ model.send(custom_methods[:to], model, element, doc)
32
+ end
33
+ end
34
+
35
+ def to_value_for(model)
36
+ if delegate
37
+ model.public_send(delegate).public_send(to)
38
38
  else
39
- name
39
+ model.public_send(to)
40
40
  end
41
41
  end
42
42
 
43
- def serialize(model, value)
43
+ def serialize(model, parent = nil, doc = nil)
44
44
  if custom_methods[:to]
45
- model.send(custom_methods[:to], model, value)
45
+ model.send(custom_methods[:to], model, parent, doc)
46
46
  else
47
- value
47
+ to_value_for(model)
48
48
  end
49
49
  end
50
50
 
51
- def deserialize(model, doc)
51
+ def deserialize(model, value, attributes, mapper_class = nil)
52
52
  if custom_methods[:from]
53
- model.send(custom_methods[:from], model, doc)
53
+ mapper_class.new.send(custom_methods[:from], model, value)
54
+ elsif delegate
55
+ if model.public_send(delegate).nil?
56
+ model.public_send(:"#{delegate}=", attributes[delegate].type.new)
57
+ end
58
+
59
+ model.public_send(delegate).public_send(:"#{to}=", value)
54
60
  else
55
- doc[name.to_s]
61
+ model.public_send(:"#{to}=", value)
56
62
  end
57
63
  end
58
64
 
59
- def namespace_set?
60
- @namespace_set
65
+ def deep_dup
66
+ raise NotImplementedError, "Subclasses must implement `deep_dup`."
61
67
  end
62
68
  end
63
69
  end
@@ -11,6 +11,7 @@ require_relative "json_adapter"
11
11
  require_relative "comparable_model"
12
12
  require_relative "schema_location"
13
13
  require_relative "validation"
14
+ require_relative "error"
14
15
 
15
16
  module Lutaml
16
17
  module Model
@@ -31,19 +32,50 @@ module Lutaml
31
32
  @mappings ||= {}
32
33
  @attributes ||= {}
33
34
 
34
- subclass.instance_variable_set(:@attributes, @attributes.dup)
35
- subclass.instance_variable_set(:@mappings, @mappings.dup)
35
+ subclass.instance_variable_set(:@attributes, Utils.deep_dup(@attributes))
36
+ subclass.instance_variable_set(:@mappings, Utils.deep_dup(@mappings))
36
37
  subclass.instance_variable_set(:@model, subclass)
37
38
  end
38
39
 
39
40
  def model(klass = nil)
40
41
  if klass
41
42
  @model = klass
43
+ add_order_handling_methods_to_model(klass)
42
44
  else
43
45
  @model
44
46
  end
45
47
  end
46
48
 
49
+ def add_order_handling_methods_to_model(klass)
50
+ Utils.add_method_if_not_defined(klass, :ordered=) do |ordered|
51
+ @ordered = ordered
52
+ end
53
+
54
+ Utils.add_method_if_not_defined(klass, :ordered?) do
55
+ !!@ordered
56
+ end
57
+
58
+ Utils.add_method_if_not_defined(klass, :mixed=) do |mixed|
59
+ @mixed = mixed
60
+ end
61
+
62
+ Utils.add_method_if_not_defined(klass, :mixed?) do
63
+ !!@mixed
64
+ end
65
+
66
+ Utils.add_method_if_not_defined(klass, :element_order=) do |order|
67
+ @element_order = order
68
+ end
69
+
70
+ Utils.add_method_if_not_defined(klass, :element_order) do
71
+ @element_order
72
+ end
73
+ end
74
+
75
+ def cast(value)
76
+ value
77
+ end
78
+
47
79
  # Define an attribute for the model
48
80
  def attribute(name, type, options = {})
49
81
  attr = Attribute.new(name, type, options)
@@ -54,15 +86,14 @@ module Lutaml
54
86
  end
55
87
 
56
88
  define_method(:"#{name}=") do |value|
57
- instance_variable_set(:"@#{name}", value)
58
- # validate!(name)
89
+ instance_variable_set(:"@#{name}", attr.cast_value(value))
59
90
  end
60
91
  end
61
92
 
62
93
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
63
94
  define_method(format) do |&block|
64
95
  klass = format == :xml ? XmlMapping : KeyValueMapping
65
- mappings[format] = klass.new
96
+ mappings[format] ||= klass.new
66
97
  mappings[format].instance_eval(&block)
67
98
 
68
99
  if format == :xml && !mappings[format].root_element
@@ -74,17 +105,22 @@ module Lutaml
74
105
  adapter = Lutaml::Model::Config.send(:"#{format}_adapter")
75
106
 
76
107
  doc = adapter.parse(data)
77
- public_send(:"of_#{format}", doc.to_h)
108
+ public_send(:"of_#{format}", doc)
78
109
  end
79
110
 
80
- define_method(:"of_#{format}") do |hash|
81
- if hash.is_a?(Array)
82
- return hash.map do |item|
83
- apply_mappings(item, format)
111
+ define_method(:"of_#{format}") do |doc|
112
+ if doc.is_a?(Array)
113
+ return doc.map do |item|
114
+ send(:"of_#{format}", item)
84
115
  end
85
116
  end
86
117
 
87
- apply_mappings(hash, format)
118
+ if format == :xml
119
+ doc_hash = doc.parse_element(doc.root, self, :xml)
120
+ apply_mappings(doc_hash, format)
121
+ else
122
+ apply_mappings(doc.to_h, format)
123
+ end
88
124
  end
89
125
 
90
126
  define_method(:"to_#{format}") do |instance|
@@ -185,6 +221,7 @@ module Lutaml
185
221
 
186
222
  def default_mappings(format)
187
223
  klass = format == :xml ? XmlMapping : KeyValueMapping
224
+
188
225
  klass.new.tap do |mapping|
189
226
  attributes&.each do |name, attr|
190
227
  mapping.map_element(
@@ -193,6 +230,8 @@ module Lutaml
193
230
  render_nil: attr.render_nil?,
194
231
  )
195
232
  end
233
+
234
+ mapping.root(to_s.split("::").last) if format == :xml
196
235
  end
197
236
  end
198
237
 
@@ -242,20 +281,34 @@ module Lutaml
242
281
  hash
243
282
  end
244
283
 
284
+ def valid_rule?(rule)
285
+ attribute = attribute_for_rule(rule)
286
+
287
+ !!attribute || rule.custom_methods[:from]
288
+ end
289
+
290
+ def attribute_for_rule(rule)
291
+ return attributes[rule.to] unless rule.delegate
292
+
293
+ attributes[rule.delegate].type.attributes[rule.to]
294
+ end
295
+
296
+ def attribute_for_child(child_name, format)
297
+ mapping_rule = mappings_for(format).find_by_name(child_name)
298
+
299
+ attribute_for_rule(mapping_rule) if mapping_rule
300
+ end
301
+
245
302
  def apply_mappings(doc, format, options = {})
246
303
  instance = options[:instance] || model.new
247
- return instance if !doc || doc.empty?
304
+ return instance if Utils.blank?(doc)
248
305
  return apply_xml_mapping(doc, instance, options) if format == :xml
249
306
 
250
307
  mappings = mappings_for(format).mappings
251
308
  mappings.each do |rule|
252
- attr = if rule.delegate
253
- attributes[rule.delegate].type.attributes[rule.to]
254
- else
255
- attributes[rule.to]
256
- end
309
+ raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
257
310
 
258
- raise "Attribute '#{rule.to}' not found in #{self}" unless attr
311
+ attr = attribute_for_rule(rule)
259
312
 
260
313
  value = if doc.key?(rule.name) || doc.key?(rule.name.to_sym)
261
314
  doc[rule.name] || doc[rule.name.to_sym]
@@ -264,26 +317,17 @@ module Lutaml
264
317
  end
265
318
 
266
319
  if rule.custom_methods[:from]
267
- if value && !value.empty?
268
- value = new.send(rule.custom_methods[:from], instance,
269
- value)
320
+ if Utils.present?(value)
321
+ value = new.send(rule.custom_methods[:from], instance, value)
270
322
  end
323
+
271
324
  next
272
325
  end
273
326
 
274
327
  value = apply_child_mappings(value, rule.child_mappings)
275
328
  value = attr.cast(value, format)
276
329
 
277
- if rule.delegate
278
- if instance.public_send(rule.delegate).nil?
279
- instance.public_send(:"#{rule.delegate}=",
280
- attributes[rule.delegate].type.new)
281
- end
282
- instance.public_send(rule.delegate).public_send(:"#{rule.to}=",
283
- value)
284
- else
285
- instance.public_send(:"#{rule.to}=", value)
286
- end
330
+ rule.deserialize(instance, value, attributes, self)
287
331
  end
288
332
 
289
333
  instance
@@ -301,7 +345,8 @@ module Lutaml
301
345
 
302
346
  if instance.respond_to?(:ordered=) && doc.is_a?(Lutaml::Model::MappingHash)
303
347
  instance.element_order = doc.item_order
304
- instance.ordered = mappings_for(:xml).mixed_content? || options[:mixed_content]
348
+ instance.ordered = mappings_for(:xml).ordered? || options[:ordered]
349
+ instance.mixed = mappings_for(:xml).mixed_content? || options[:mixed_content]
305
350
  end
306
351
 
307
352
  if doc["__schema_location"]
@@ -313,44 +358,55 @@ module Lutaml
313
358
  end
314
359
 
315
360
  mappings.each do |rule|
316
- attr = attributes[rule.to]
317
- raise "Attribute '#{rule.to}' not found in #{self}" unless attr
318
-
319
- is_content_mapping = rule.name.nil?
361
+ raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
320
362
 
321
- value = if is_content_mapping
363
+ value = if rule.content_mapping?
322
364
  doc["text"]
365
+ elsif doc.key_exist?(rule.namespaced_name)
366
+ doc.fetch(rule.namespaced_name)
323
367
  else
324
- doc[rule.name.to_s] || doc[rule.name.to_sym]
368
+ rule.to_value_for(instance)
325
369
  end
326
370
 
327
- value = [value].compact if attr.collection? && !value.is_a?(Array)
371
+ value = normalize_xml_value(value, rule)
372
+ rule.deserialize(instance, value, attributes, self)
373
+ end
328
374
 
329
- if value.is_a?(Array)
330
- value = value.map do |v|
331
- v.is_a?(Hash) && !(attr.type <= Serialize) ? v["text"] : v
332
- end
333
- elsif !(attr.type <= Serialize) && value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash
334
- value = value["text"]
335
- end
375
+ instance
376
+ end
336
377
 
337
- unless is_content_mapping
338
- value = attr.cast(
339
- value,
340
- :xml,
341
- caller_class: self,
342
- mixed_content: rule.mixed_content,
343
- )
344
- end
378
+ def normalize_xml_value(value, rule)
379
+ attr = attribute_for_rule(rule)
345
380
 
346
- if rule.custom_methods[:from]
347
- new.send(rule.custom_methods[:from], instance, value)
348
- else
349
- instance.public_send(:"#{rule.to}=", value)
350
- end
381
+ value = [value].compact if attr&.collection? && !value.is_a?(Array)
382
+
383
+ value = if value.is_a?(Array)
384
+ value.map do |v|
385
+ text_hash?(attr, v) ? v["text"] : v
386
+ end
387
+ elsif text_hash?(attr, value)
388
+ value["text"]
389
+ else
390
+ value
391
+ end
392
+
393
+ if attr && !rule.content_mapping?
394
+ value = attr.cast(
395
+ value,
396
+ :xml,
397
+ caller_class: self,
398
+ mixed_content: rule.mixed_content,
399
+ )
351
400
  end
352
401
 
353
- instance
402
+ value
403
+ end
404
+
405
+ def text_hash?(attr, value)
406
+ return false unless value.is_a?(Hash)
407
+ return value.keys == ["text"] unless attr
408
+
409
+ !(attr.type <= Serialize) && attr.type != Lutaml::Model::Type::Hash
354
410
  end
355
411
 
356
412
  def ensure_utf8(value)
@@ -373,6 +429,7 @@ module Lutaml
373
429
  end
374
430
 
375
431
  attr_accessor :element_order, :schema_location
432
+ attr_writer :ordered, :mixed
376
433
 
377
434
  def initialize(attrs = {})
378
435
  @validate_on_set = attrs.delete(:validate_on_set) || false
@@ -422,11 +479,11 @@ module Lutaml
422
479
  end
423
480
 
424
481
  def ordered?
425
- @ordered
482
+ !!@ordered
426
483
  end
427
484
 
428
- def ordered=(ordered)
429
- @ordered = ordered
485
+ def mixed?
486
+ !!@mixed
430
487
  end
431
488
 
432
489
  def key_exist?(hash, key)
@@ -439,7 +496,6 @@ module Lutaml
439
496
 
440
497
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
441
498
  define_method(:"to_#{format}") do |options = {}|
442
- validate!
443
499
  adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
444
500
  representation = if format == :xml
445
501
  self
@@ -62,6 +62,10 @@ module Lutaml
62
62
  when "Boolean"
63
63
  to_boolean(value)
64
64
  when "Decimal"
65
+ unless defined?(BigDecimal)
66
+ raise Lutaml::Model::TypeNotEnabledError.new("Decimal", value)
67
+ end
68
+
65
69
  BigDecimal(value.to_s)
66
70
  when "Hash"
67
71
  normalize_hash(Hash(value))
@@ -85,6 +89,10 @@ module Lutaml
85
89
  when "Boolean"
86
90
  to_boolean(value)
87
91
  when "Decimal"
92
+ unless defined?(BigDecimal)
93
+ raise Lutaml::Model::TypeNotEnabledError.new("Decimal", value)
94
+ end
95
+
88
96
  value.to_s("F")
89
97
  when "Hash"
90
98
  Hash(value)
@@ -108,6 +116,8 @@ module Lutaml
108
116
  def self.normalize_hash(hash)
109
117
  return hash["text"] if hash.keys == ["text"]
110
118
 
119
+ hash = hash.to_h if hash.is_a?(Lutaml::Model::MappingHash)
120
+
111
121
  hash.filter_map do |key, value|
112
122
  next if key == "text"
113
123
 
@@ -34,6 +34,40 @@ module Lutaml
34
34
  .downcase
35
35
  end
36
36
 
37
+ def present?(value)
38
+ !blank?(value)
39
+ end
40
+
41
+ def blank?(value)
42
+ value.respond_to?(:empty?) ? value.empty? : !value
43
+ end
44
+
45
+ def add_method_if_not_defined(klass, method_name, &block)
46
+ unless klass.method_defined?(method_name)
47
+ klass.class_eval do
48
+ define_method(method_name, &block)
49
+ end
50
+ end
51
+ end
52
+
53
+ def deep_dup(hash)
54
+ return hash if hash.nil?
55
+
56
+ new_hash = {}
57
+
58
+ hash.each do |key, value|
59
+ new_hash[key] = if value.is_a?(Hash)
60
+ deep_dup(value)
61
+ elsif value.respond_to?(:deep_dup)
62
+ value.deep_dup
63
+ else
64
+ value.dup
65
+ end
66
+ end
67
+
68
+ new_hash
69
+ end
70
+
37
71
  private
38
72
 
39
73
  def camelize_part(part)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.3.10"
5
+ VERSION = "0.3.14"
6
6
  end
7
7
  end
@@ -27,11 +27,21 @@ module Lutaml
27
27
  element.add_child(child)
28
28
  end
29
29
 
30
- def create_and_add_element(element_name, prefix: nil, attributes: {})
31
- add_namespace_prefix(prefix) if prefix
30
+ def add_attribute(element, name, value)
31
+ element[name] = value
32
+ end
33
+
34
+ def create_and_add_element(
35
+ element_name,
36
+ prefix: (prefix_unset = true
37
+ nil),
38
+ attributes: {}
39
+ )
40
+ add_namespace_prefix(prefix)
32
41
 
33
42
  if block_given?
34
43
  public_send(element_name, attributes) do
44
+ xml.parent.namespace = nil if prefix.nil? && !prefix_unset
35
45
  yield(self)
36
46
  end
37
47
  else
@@ -53,11 +63,9 @@ module Lutaml
53
63
  self
54
64
  end
55
65
 
56
- def method_missing(method_name, *args)
66
+ def method_missing(method_name, *args, &block)
57
67
  if block_given?
58
- xml.public_send(method_name, *args) do
59
- yield(xml)
60
- end
68
+ xml.public_send(method_name, *args, &block)
61
69
  else
62
70
  xml.public_send(method_name, *args)
63
71
  end
@@ -38,6 +38,10 @@ module Lutaml
38
38
  element << child
39
39
  end
40
40
 
41
+ def add_attribute(element, name, value)
42
+ element[name] = value
43
+ end
44
+
41
45
  def create_and_add_element(element_name, prefix: nil, attributes: {})
42
46
  prefixed_name = if prefix
43
47
  "#{prefix}:#{element_name}"