lutaml-model 0.3.10 → 0.3.14

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.
@@ -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}"