lutaml-model 0.3.9 → 0.3.11

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.
@@ -0,0 +1,29 @@
1
+ module Lutaml
2
+ module Model
3
+ class CollectionCountOutOfRangeError < Error
4
+ def initialize(attr_name, value, range)
5
+ @attr_name = attr_name
6
+ @value = value
7
+ @range = range
8
+
9
+ super()
10
+ end
11
+
12
+ def to_s
13
+ "#{@attr_name} count is #{@value.size}, must be #{range_to_string}"
14
+ end
15
+
16
+ private
17
+
18
+ def range_to_string
19
+ if @range.end.nil?
20
+ "at least #{@range.begin}"
21
+ elsif @range.begin == @range.end
22
+ "exactly #{@range.begin}"
23
+ else
24
+ "between #{@range.begin} and #{@range.end}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Lutaml
2
+ module Model
3
+ class IncorrectMappingArgumentsError < Error
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class TypeNotEnabledError < Error
4
+ def initialize(type_name, value)
5
+ super("#{type_name} type is not enabled. Value: #{value}")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # lib/lutaml/model/error/validation_error.rb
2
+ module Lutaml
3
+ module Model
4
+ class ValidationError < Error
5
+ attr_reader :errors
6
+
7
+ def initialize(errors)
8
+ @errors = errors
9
+ super(errors.join(", "))
10
+ end
11
+
12
+ def include?(error_class)
13
+ errors.any?(error_class)
14
+ end
15
+
16
+ def error_messages
17
+ errors.map(&:message)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -6,4 +6,8 @@ module Lutaml
6
6
  end
7
7
 
8
8
  require_relative "error/invalid_value_error"
9
+ require_relative "error/incorrect_mapping_argument_error"
9
10
  require_relative "error/unknown_adapter_type_error"
11
+ require_relative "error/collection_count_out_of_range_error"
12
+ require_relative "error/validation_error"
13
+ require_relative "error/type_not_enabled_error"
@@ -6,8 +6,7 @@ module Lutaml
6
6
  module JsonAdapter
7
7
  class StandardJsonAdapter < JsonDocument
8
8
  def self.parse(json)
9
- attributes = JSON.parse(json, create_additions: false)
10
- new(attributes)
9
+ JSON.parse(json, create_additions: false)
11
10
  end
12
11
 
13
12
  def to_json(*args)
@@ -11,12 +11,14 @@ module Lutaml
11
11
 
12
12
  def map(
13
13
  name,
14
- to:,
14
+ to: nil,
15
15
  render_nil: false,
16
16
  with: {},
17
17
  delegate: nil,
18
18
  child_mappings: nil
19
19
  )
20
+ validate!(name, to, with)
21
+
20
22
  @mappings << KeyValueMappingRule.new(
21
23
  name,
22
24
  to: to,
@@ -28,6 +30,18 @@ module Lutaml
28
30
  end
29
31
 
30
32
  alias map_element map
33
+
34
+ def validate!(key, to, with)
35
+ if to.nil? && with.empty?
36
+ msg = ":to or :with argument is required for mapping '#{key}'"
37
+ raise IncorrectMappingArgumentsError.new(msg)
38
+ end
39
+
40
+ if !with.empty? && (with[:from].nil? || with[:to].nil?)
41
+ msg = ":with argument for mapping '#{key}' requires :to and :from keys"
42
+ raise IncorrectMappingArgumentsError.new(msg)
43
+ end
44
+ end
31
45
  end
32
46
  end
33
47
  end
@@ -24,6 +24,20 @@ module Lutaml
24
24
  @ordered
25
25
  end
26
26
 
27
+ def method_missing(method_name, *args)
28
+ value = self[method_name] || self[method_name.to_s]
29
+ return value if value
30
+
31
+ super
32
+ end
33
+
34
+ def respond_to_missing?(method_name, include_private = false)
35
+ key_present = key?(method_name) || key?(method_name.to_s)
36
+ return true if key_present
37
+
38
+ super
39
+ end
40
+
27
41
  private
28
42
 
29
43
  def normalize(key)
@@ -17,6 +17,7 @@ module Lutaml
17
17
  delegate: nil,
18
18
  mixed_content: false,
19
19
  namespace_set: false,
20
+ prefix_set: false,
20
21
  child_mappings: nil
21
22
  )
22
23
  @name = name
@@ -26,6 +27,7 @@ module Lutaml
26
27
  @delegate = delegate
27
28
  @mixed_content = mixed_content
28
29
  @namespace_set = namespace_set
30
+ @prefix_set = prefix_set
29
31
  @child_mappings = child_mappings
30
32
  end
31
33
 
@@ -40,25 +42,47 @@ module Lutaml
40
42
  end
41
43
  end
42
44
 
43
- def serialize(model, value)
45
+ def serialize_attribute(model, element, doc)
44
46
  if custom_methods[:to]
45
- model.send(custom_methods[:to], model, value)
47
+ model.send(custom_methods[:to], model, element, doc)
48
+ end
49
+ end
50
+
51
+ def serialize(model, parent = nil, doc = nil)
52
+ if custom_methods[:to]
53
+ model.send(custom_methods[:to], model, parent, doc)
54
+ elsif delegate
55
+ model.public_send(delegate).public_send(to)
46
56
  else
47
- value
57
+ model.public_send(to)
48
58
  end
49
59
  end
50
60
 
51
- def deserialize(model, doc)
61
+ def deserialize(model, value, attributes, mapper_class = nil)
52
62
  if custom_methods[:from]
53
- model.send(custom_methods[:from], model, doc)
63
+ mapper_class.new.send(custom_methods[:from], model, value)
64
+ elsif delegate
65
+ if model.public_send(delegate).nil?
66
+ model.public_send(:"#{delegate}=", attributes[delegate].type.new)
67
+ end
68
+
69
+ model.public_send(delegate).public_send(:"#{to}=", value)
54
70
  else
55
- doc[name.to_s]
71
+ model.public_send(:"#{to}=", value)
56
72
  end
57
73
  end
58
74
 
59
75
  def namespace_set?
60
76
  @namespace_set
61
77
  end
78
+
79
+ def prefix_set?
80
+ @prefix_set
81
+ end
82
+
83
+ def content_mapping?
84
+ name.nil?
85
+ end
62
86
  end
63
87
  end
64
88
  end
@@ -0,0 +1,59 @@
1
+ module Lutaml
2
+ module Model
3
+ class Location
4
+ attr_reader :namespace, :location
5
+
6
+ def initialize(namespace:, location:)
7
+ @namespace = namespace
8
+ @location = location
9
+ end
10
+
11
+ def to_xml_attribute
12
+ "#{@namespace} #{@location}".strip
13
+ end
14
+ end
15
+
16
+ class SchemaLocation
17
+ DEFAULT_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance".freeze
18
+
19
+ attr_reader :namespace, :prefix, :schema_location
20
+
21
+ def initialize(schema_location:, prefix: "xsi",
22
+ namespace: DEFAULT_NAMESPACE)
23
+ @original_schema_location = schema_location
24
+ @schema_location = parsed_schema_locations(schema_location)
25
+ @prefix = prefix
26
+ @namespace = namespace
27
+ end
28
+
29
+ def to_xml_attributes
30
+ {
31
+ "xmlns:#{prefix}" => namespace,
32
+ "#{prefix}:schemaLocation" => schema_location.map(&:to_xml_attribute).join(" "),
33
+ }
34
+ end
35
+
36
+ def [](index)
37
+ @schema_location[index]
38
+ end
39
+
40
+ def size
41
+ @schema_location.size
42
+ end
43
+
44
+ private
45
+
46
+ def parsed_schema_locations(schema_location)
47
+ locations = if schema_location.is_a?(Hash)
48
+ schema_location
49
+ else
50
+ schema_location.split.each_slice(2)
51
+ end
52
+
53
+ locations.map do |n, l|
54
+ Location.new(namespace: n, location: l)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -9,11 +9,15 @@ require_relative "xml_mapping"
9
9
  require_relative "key_value_mapping"
10
10
  require_relative "json_adapter"
11
11
  require_relative "comparable_model"
12
+ require_relative "schema_location"
13
+ require_relative "validation"
14
+ require_relative "error"
12
15
 
13
16
  module Lutaml
14
17
  module Model
15
18
  module Serialize
16
19
  include ComparableModel
20
+ include Validation
17
21
 
18
22
  def self.included(base)
19
23
  base.extend(ClassMethods)
@@ -36,11 +40,34 @@ module Lutaml
36
40
  def model(klass = nil)
37
41
  if klass
38
42
  @model = klass
43
+ add_order_handling_methods_to_model(klass)
39
44
  else
40
45
  @model
41
46
  end
42
47
  end
43
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, :element_order=) do |order|
59
+ @element_order = order
60
+ end
61
+
62
+ Utils.add_method_if_not_defined(klass, :element_order) do
63
+ @element_order
64
+ end
65
+ end
66
+
67
+ def cast(value)
68
+ value
69
+ end
70
+
44
71
  # Define an attribute for the model
45
72
  def attribute(name, type, options = {})
46
73
  attr = Attribute.new(name, type, options)
@@ -51,26 +78,10 @@ module Lutaml
51
78
  end
52
79
 
53
80
  define_method(:"#{name}=") do |value|
54
- instance_variable_set(:"@#{name}", value)
55
- validate
81
+ instance_variable_set(:"@#{name}", attr.cast_value(value))
56
82
  end
57
83
  end
58
84
 
59
- # Check if the value to be assigned is valid for the attribute
60
- def attr_value_valid?(name, value)
61
- attr = attributes[name]
62
-
63
- return true unless attr.options[:values]
64
-
65
- # Allow nil values if there's no default
66
- return true if value.nil? && !attr.default
67
-
68
- # Use the default value if the value is nil
69
- value = attr.default if value.nil?
70
-
71
- attr.options[:values].include?(value)
72
- end
73
-
74
85
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
75
86
  define_method(format) do |&block|
76
87
  klass = format == :xml ? XmlMapping : KeyValueMapping
@@ -86,17 +97,17 @@ module Lutaml
86
97
  adapter = Lutaml::Model::Config.send(:"#{format}_adapter")
87
98
 
88
99
  doc = adapter.parse(data)
89
- public_send(:"of_#{format}", doc.to_h)
100
+ public_send(:"of_#{format}", doc)
90
101
  end
91
102
 
92
- define_method(:"of_#{format}") do |hash|
93
- if hash.is_a?(Array)
94
- return hash.map do |item|
95
- apply_mappings(item, format)
96
- end
103
+ define_method(:"of_#{format}") do |doc|
104
+ if doc.is_a?(Array)
105
+ return doc.map do |item|
106
+ send(:"of_#{format}", item)
107
+ end
97
108
  end
98
109
 
99
- apply_mappings(hash, format)
110
+ apply_mappings(doc.to_h, format)
100
111
  end
101
112
 
102
113
  define_method(:"to_#{format}") do |instance|
@@ -197,6 +208,7 @@ module Lutaml
197
208
 
198
209
  def default_mappings(format)
199
210
  klass = format == :xml ? XmlMapping : KeyValueMapping
211
+
200
212
  klass.new.tap do |mapping|
201
213
  attributes&.each do |name, attr|
202
214
  mapping.map_element(
@@ -205,6 +217,8 @@ module Lutaml
205
217
  render_nil: attr.render_nil?,
206
218
  )
207
219
  end
220
+
221
+ mapping.root(to_s.split("::").last) if format == :xml
208
222
  end
209
223
  end
210
224
 
@@ -254,20 +268,28 @@ module Lutaml
254
268
  hash
255
269
  end
256
270
 
271
+ def valid_rule?(rule)
272
+ attribute = attribute_for_rule(rule)
273
+
274
+ !!attribute || rule.custom_methods[:from]
275
+ end
276
+
277
+ def attribute_for_rule(rule)
278
+ return attributes[rule.to] unless rule.delegate
279
+
280
+ attributes[rule.delegate].type.attributes[rule.to]
281
+ end
282
+
257
283
  def apply_mappings(doc, format, options = {})
258
284
  instance = options[:instance] || model.new
259
- return instance if !doc || doc.empty?
285
+ return instance if Utils.blank?(doc)
260
286
  return apply_xml_mapping(doc, instance, options) if format == :xml
261
287
 
262
288
  mappings = mappings_for(format).mappings
263
289
  mappings.each do |rule|
264
- attr = if rule.delegate
265
- attributes[rule.delegate].type.attributes[rule.to]
266
- else
267
- attributes[rule.to]
268
- end
290
+ raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
269
291
 
270
- raise "Attribute '#{rule.to}' not found in #{self}" unless attr
292
+ attr = attribute_for_rule(rule)
271
293
 
272
294
  value = if doc.key?(rule.name) || doc.key?(rule.name.to_sym)
273
295
  doc[rule.name] || doc[rule.name.to_sym]
@@ -276,26 +298,17 @@ module Lutaml
276
298
  end
277
299
 
278
300
  if rule.custom_methods[:from]
279
- if value && !value.empty?
280
- value = new.send(rule.custom_methods[:from], instance,
281
- value)
301
+ if Utils.present?(value)
302
+ value = new.send(rule.custom_methods[:from], instance, value)
282
303
  end
304
+
283
305
  next
284
306
  end
285
307
 
286
308
  value = apply_child_mappings(value, rule.child_mappings)
287
309
  value = attr.cast(value, format)
288
310
 
289
- if rule.delegate
290
- if instance.public_send(rule.delegate).nil?
291
- instance.public_send(:"#{rule.delegate}=",
292
- attributes[rule.delegate].type.new)
293
- end
294
- instance.public_send(rule.delegate).public_send(:"#{rule.to}=",
295
- value)
296
- else
297
- instance.public_send(:"#{rule.to}=", value)
298
- end
311
+ rule.deserialize(instance, value, attributes, self)
299
312
  end
300
313
 
301
314
  instance
@@ -316,45 +329,62 @@ module Lutaml
316
329
  instance.ordered = mappings_for(:xml).mixed_content? || options[:mixed_content]
317
330
  end
318
331
 
319
- mappings.each do |rule|
320
- attr = attributes[rule.to]
321
- raise "Attribute '#{rule.to}' not found in #{self}" unless attr
332
+ if doc["__schema_location"]
333
+ instance.schema_location = Lutaml::Model::SchemaLocation.new(
334
+ schema_location: doc["__schema_location"][:schema_location],
335
+ prefix: doc["__schema_location"][:prefix],
336
+ namespace: doc["__schema_location"][:namespace],
337
+ )
338
+ end
322
339
 
323
- is_content_mapping = rule.name.nil?
340
+ mappings.each do |rule|
341
+ raise "Attribute '#{rule.to}' not found in #{self}" unless valid_rule?(rule)
324
342
 
325
- value = if is_content_mapping
343
+ value = if rule.content_mapping?
326
344
  doc["text"]
327
345
  else
328
346
  doc[rule.name.to_s] || doc[rule.name.to_sym]
329
347
  end
330
348
 
331
- value = [value].compact if attr.collection? && !value.is_a?(Array)
349
+ value = normalize_xml_value(value, rule)
350
+ rule.deserialize(instance, value, attributes, self)
351
+ end
332
352
 
333
- if value.is_a?(Array)
334
- value = value.map do |v|
335
- v.is_a?(Hash) && !(attr.type <= Serialize) ? v["text"] : v
336
- end
337
- elsif !(attr.type <= Serialize) && value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash
338
- value = value["text"]
339
- end
353
+ instance
354
+ end
340
355
 
341
- unless is_content_mapping
342
- value = attr.cast(
343
- value,
344
- :xml,
345
- caller_class: self,
346
- mixed_content: rule.mixed_content,
347
- )
348
- end
356
+ def normalize_xml_value(value, rule)
357
+ attr = attribute_for_rule(rule)
349
358
 
350
- if rule.custom_methods[:from]
351
- new.send(rule.custom_methods[:from], instance, value)
352
- else
353
- instance.public_send(:"#{rule.to}=", value)
354
- end
359
+ value = [value].compact if attr&.collection? && !value.is_a?(Array)
360
+
361
+ value = if value.is_a?(Array)
362
+ value.map do |v|
363
+ text_hash?(attr, v) ? v["text"] : v
364
+ end
365
+ elsif text_hash?(attr, value)
366
+ value["text"]
367
+ else
368
+ value
369
+ end
370
+
371
+ if attr && !rule.content_mapping?
372
+ value = attr.cast(
373
+ value,
374
+ :xml,
375
+ caller_class: self,
376
+ mixed_content: rule.mixed_content,
377
+ )
355
378
  end
356
379
 
357
- instance
380
+ value
381
+ end
382
+
383
+ def text_hash?(attr, value)
384
+ return false unless value.is_a?(Hash)
385
+ return value.keys == ["text"] unless attr
386
+
387
+ !(attr.type <= Serialize) && attr.type != Lutaml::Model::Type::Hash
358
388
  end
359
389
 
360
390
  def ensure_utf8(value)
@@ -376,9 +406,11 @@ module Lutaml
376
406
  end
377
407
  end
378
408
 
379
- attr_accessor :element_order
409
+ attr_accessor :element_order, :schema_location
380
410
 
381
411
  def initialize(attrs = {})
412
+ @validate_on_set = attrs.delete(:validate_on_set) || false
413
+
382
414
  return unless self.class.attributes
383
415
 
384
416
  if attrs.is_a?(Lutaml::Model::MappingHash)
@@ -386,13 +418,41 @@ module Lutaml
386
418
  @element_order = attrs.item_order
387
419
  end
388
420
 
421
+ if attrs.key?(:schema_location)
422
+ self.schema_location = attrs[:schema_location]
423
+ end
424
+
389
425
  self.class.attributes.each do |name, attr|
390
- value = self.class.attr_value(attrs, name, attr)
426
+ value = if attrs.key?(name) || attrs.key?(name.to_s)
427
+ self.class.attr_value(attrs, name, attr)
428
+ else
429
+ attr.default
430
+ end
391
431
 
392
- send(:"#{name}=", self.class.ensure_utf8(value))
432
+ # Initialize collections with an empty array if no value is provided
433
+ if attr.collection? && value.nil?
434
+ value = []
435
+ end
436
+
437
+ instance_variable_set(:"@#{name}", self.class.ensure_utf8(value))
393
438
  end
439
+ end
394
440
 
395
- validate
441
+ def method_missing(method_name, *args)
442
+ if method_name.to_s.end_with?("=") && self.class.attributes.key?(method_name.to_s.chomp("=").to_sym)
443
+ define_singleton_method(method_name) do |value|
444
+ instance_variable_set(:"@#{method_name.to_s.chomp('=')}", value)
445
+ end
446
+ send(method_name, *args)
447
+ else
448
+ super
449
+ end
450
+ end
451
+
452
+ def validate_attribute!(attr_name)
453
+ attr = self.class.attributes[attr_name]
454
+ value = instance_variable_get(:"@#{attr_name}")
455
+ attr.validate_value!(value)
396
456
  end
397
457
 
398
458
  def ordered?
@@ -413,7 +473,6 @@ module Lutaml
413
473
 
414
474
  Lutaml::Model::Config::AVAILABLE_FORMATS.each do |format|
415
475
  define_method(:"to_#{format}") do |options = {}|
416
- validate
417
476
  adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
418
477
  representation = if format == :xml
419
478
  self
@@ -425,16 +484,6 @@ module Lutaml
425
484
  adapter.new(representation).public_send(:"to_#{format}", options)
426
485
  end
427
486
  end
428
-
429
- def validate
430
- self.class.attributes.each do |name, attr|
431
- value = send(name)
432
- unless self.class.attr_value_valid?(name, value)
433
- raise Lutaml::Model::InvalidValueError.new(name, value,
434
- attr.options[:values])
435
- end
436
- end
437
- end
438
487
  end
439
488
  end
440
489
  end
@@ -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)
@@ -34,6 +34,22 @@ 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
+
37
53
  private
38
54
 
39
55
  def camelize_part(part)