lutaml-model 0.3.9 → 0.3.11

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