lutaml-model 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +2 -0
  3. data/.rubocop_todo.yml +86 -23
  4. data/Gemfile +2 -0
  5. data/README.adoc +1441 -220
  6. data/lib/lutaml/model/attribute.rb +33 -10
  7. data/lib/lutaml/model/choice.rb +56 -0
  8. data/lib/lutaml/model/config.rb +1 -0
  9. data/lib/lutaml/model/constants.rb +7 -0
  10. data/lib/lutaml/model/error/choice_lower_bound_error.rb +9 -0
  11. data/lib/lutaml/model/error/choice_upper_bound_error.rb +9 -0
  12. data/lib/lutaml/model/error/import_model_with_root_error.rb +9 -0
  13. data/lib/lutaml/model/error/incorrect_sequence_error.rb +9 -0
  14. data/lib/lutaml/model/error/invalid_choice_range_error.rb +20 -0
  15. data/lib/lutaml/model/error/no_root_mapping_error.rb +9 -0
  16. data/lib/lutaml/model/error/no_root_namespace_error.rb +9 -0
  17. data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
  18. data/lib/lutaml/model/error/unknown_sequence_mapping_error.rb +9 -0
  19. data/lib/lutaml/model/error.rb +9 -0
  20. data/lib/lutaml/model/json_adapter/standard_json_adapter.rb +6 -1
  21. data/lib/lutaml/model/key_value_mapping.rb +34 -3
  22. data/lib/lutaml/model/key_value_mapping_rule.rb +4 -2
  23. data/lib/lutaml/model/liquefiable.rb +59 -0
  24. data/lib/lutaml/model/mapping_hash.rb +9 -1
  25. data/lib/lutaml/model/mapping_rule.rb +19 -2
  26. data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
  27. data/lib/lutaml/model/schema/xml_compiler.rb +762 -0
  28. data/lib/lutaml/model/schema.rb +5 -0
  29. data/lib/lutaml/model/schema_location.rb +7 -0
  30. data/lib/lutaml/model/sequence.rb +71 -0
  31. data/lib/lutaml/model/serialize.rb +139 -33
  32. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
  33. data/lib/lutaml/model/type/decimal.rb +0 -4
  34. data/lib/lutaml/model/type/hash.rb +11 -11
  35. data/lib/lutaml/model/type/time.rb +3 -3
  36. data/lib/lutaml/model/utils.rb +19 -15
  37. data/lib/lutaml/model/validation.rb +12 -1
  38. data/lib/lutaml/model/version.rb +1 -1
  39. data/lib/lutaml/model/xml_adapter/builder/oga.rb +10 -7
  40. data/lib/lutaml/model/xml_adapter/builder/ox.rb +20 -13
  41. data/lib/lutaml/model/xml_adapter/element.rb +32 -0
  42. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +13 -9
  43. data/lib/lutaml/model/xml_adapter/oga/element.rb +14 -13
  44. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +86 -19
  45. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +19 -15
  46. data/lib/lutaml/model/xml_adapter/xml_document.rb +82 -25
  47. data/lib/lutaml/model/xml_adapter/xml_element.rb +57 -3
  48. data/lib/lutaml/model/xml_mapping.rb +53 -9
  49. data/lib/lutaml/model/xml_mapping_rule.rb +8 -6
  50. data/lib/lutaml/model.rb +2 -0
  51. data/lutaml-model.gemspec +5 -0
  52. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +75 -0
  53. data/spec/ceramic_spec.rb +39 -0
  54. data/spec/fixtures/ceramic.rb +23 -0
  55. data/spec/fixtures/xml/address_example_260.xsd +9 -0
  56. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  57. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  58. data/spec/fixtures/xml/test_schema.xsd +53 -0
  59. data/spec/fixtures/xml/user.xsd +10 -0
  60. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  61. data/spec/lutaml/model/cdata_spec.rb +4 -5
  62. data/spec/lutaml/model/choice_spec.rb +168 -0
  63. data/spec/lutaml/model/collection_spec.rb +1 -1
  64. data/spec/lutaml/model/custom_model_spec.rb +7 -21
  65. data/spec/lutaml/model/custom_serialization_spec.rb +74 -2
  66. data/spec/lutaml/model/defaults_spec.rb +3 -1
  67. data/spec/lutaml/model/delegation_spec.rb +7 -5
  68. data/spec/lutaml/model/enum_spec.rb +35 -0
  69. data/spec/lutaml/model/group_spec.rb +160 -0
  70. data/spec/lutaml/model/inheritance_spec.rb +25 -0
  71. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  72. data/spec/lutaml/model/liquefiable_spec.rb +121 -0
  73. data/spec/lutaml/model/map_all_spec.rb +188 -0
  74. data/spec/lutaml/model/mixed_content_spec.rb +95 -56
  75. data/spec/lutaml/model/multiple_mapping_spec.rb +22 -10
  76. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1624 -0
  77. data/spec/lutaml/model/sequence_spec.rb +216 -0
  78. data/spec/lutaml/model/transformation_spec.rb +230 -0
  79. data/spec/lutaml/model/type_spec.rb +138 -31
  80. data/spec/lutaml/model/utils_spec.rb +32 -0
  81. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  82. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -7
  83. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  84. data/spec/lutaml/model/xml_mapping_rule_spec.rb +51 -0
  85. data/spec/lutaml/model/xml_mapping_spec.rb +250 -112
  86. metadata +77 -2
@@ -10,6 +10,9 @@ module Lutaml
10
10
  collection
11
11
  values
12
12
  pattern
13
+ transform
14
+ choice
15
+ sequence
13
16
  ].freeze
14
17
 
15
18
  def initialize(name, type, options = {})
@@ -33,6 +36,10 @@ module Lutaml
33
36
  @options[:delegate]
34
37
  end
35
38
 
39
+ def transform
40
+ @options[:transform] || {}
41
+ end
42
+
36
43
  def cast_type!(type)
37
44
  case type
38
45
  when Symbol
@@ -81,15 +88,17 @@ module Lutaml
81
88
  end
82
89
 
83
90
  def default
84
- value = if delegate
85
- type.attributes[to].default
86
- elsif options[:default].is_a?(Proc)
87
- options[:default].call
88
- else
89
- options[:default]
90
- end
91
-
92
- cast_value(value)
91
+ cast_value(default_value)
92
+ end
93
+
94
+ def default_value
95
+ if delegate
96
+ type.attributes[to].default
97
+ elsif options[:default].is_a?(Proc)
98
+ options[:default].call
99
+ else
100
+ options[:default]
101
+ end
93
102
  end
94
103
 
95
104
  def pattern
@@ -100,8 +109,16 @@ module Lutaml
100
109
  @options.key?(:values) ? @options[:values] : []
101
110
  end
102
111
 
112
+ def transform_import_method
113
+ transform[:import]
114
+ end
115
+
116
+ def transform_export_method
117
+ transform[:export]
118
+ end
119
+
103
120
  def valid_value!(value)
104
- return true if value.nil? && !collection?
121
+ return true if value.nil? && singular?
105
122
  return true unless enum?
106
123
 
107
124
  unless valid_value?(value)
@@ -153,6 +170,10 @@ module Lutaml
153
170
  raise ArgumentError, "Invalid collection range: #{range}"
154
171
  end
155
172
 
173
+ validate_range!(range)
174
+ end
175
+
176
+ def validate_range!(range)
156
177
  if range.begin.nil?
157
178
  raise ArgumentError,
158
179
  "Invalid collection range: #{range}. Begin must be specified."
@@ -235,6 +256,8 @@ module Lutaml
235
256
  end
236
257
  elsif type <= Serialize && value.is_a?(Hash)
237
258
  type.apply_mappings(value, format, options)
259
+ elsif !value.nil? && !value.is_a?(type)
260
+ type.send(:"from_#{format}", value)
238
261
  else
239
262
  type.cast(value)
240
263
  end
@@ -0,0 +1,56 @@
1
+ module Lutaml
2
+ module Model
3
+ class Choice
4
+ attr_reader :attributes,
5
+ :model,
6
+ :min,
7
+ :max
8
+
9
+ def initialize(model, min, max)
10
+ @attributes = []
11
+ @model = model
12
+ @min = min
13
+ @max = max
14
+
15
+ raise Lutaml::Model::InvalidChoiceRangeError.new(@min, @max) if @min.negative? || @max.negative?
16
+ end
17
+
18
+ def attribute(name, type, options = {})
19
+ options[:choice] = self
20
+ @attributes << @model.attribute(name, type, options)
21
+ end
22
+
23
+ def choice(min: 1, max: 1, &block)
24
+ @attributes << Choice.new(@model, min, max).tap do |c|
25
+ c.instance_eval(&block)
26
+ end
27
+ end
28
+
29
+ def validate_content!(object)
30
+ validated_attributes = []
31
+ valid = valid_attributes(object, validated_attributes)
32
+
33
+ raise Lutaml::Model::ChoiceUpperBoundError.new(validated_attributes, @max) if valid.count > @max
34
+ raise Lutaml::Model::ChoiceLowerBoundError.new(validated_attributes, @min) if valid.count < @min
35
+ end
36
+
37
+ private
38
+
39
+ def valid_attributes(object, validated_attributes)
40
+ @attributes.each do |attribute|
41
+ if attribute.is_a?(Choice)
42
+ begin
43
+ attribute.validate_content!(object)
44
+ validated_attributes << attribute
45
+ rescue Lutaml::Model::ChoiceLowerBoundError
46
+ end
47
+ elsif Utils.present?(object.public_send(attribute.name))
48
+ validated_attributes << attribute.name
49
+ end
50
+ end
51
+
52
+ validated_attributes
53
+ end
54
+ end
55
+ end
56
+ end
@@ -59,6 +59,7 @@ module Lutaml
59
59
  cause: nil,
60
60
  )
61
61
  end
62
+ Moxml::Adapter.load(type_name) unless KEY_VALUE_FORMATS.include?(adapter_name)
62
63
 
63
64
  instance_variable_set(
64
65
  :"@#{adapter}",
@@ -0,0 +1,7 @@
1
+ module Lutaml
2
+ module Model
3
+ module Constants
4
+ RAW_MAPPING_KEY = "__raw_mapping".freeze
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class ChoiceLowerBoundError < Error
4
+ def initialize(validated_attributes, min)
5
+ super("Attributes `#{validated_attributes}` count is less than the lower bound `#{min}`")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class ChoiceUpperBoundError < Error
4
+ def initialize(validated_attributes, max)
5
+ super("Attributes `#{validated_attributes}` count exceeds the upper bound `#{max}`")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class ImportModelWithRootError < Error
4
+ def initialize(model)
5
+ super("Cannot import a model `#{model}` with a root element")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class IncorrectSequenceError < Error
4
+ def initialize(defined_order_element, expected_order_element)
5
+ super("Element `#{expected_order_element}` does not match the expected sequence order element `#{defined_order_element}`")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ module Lutaml
2
+ module Model
3
+ class InvalidChoiceRangeError < Error
4
+ def initialize(min, max)
5
+ @min = min
6
+ @max = max
7
+
8
+ super()
9
+ end
10
+
11
+ def to_s
12
+ if @min.negative?
13
+ "Choice lower bound `#{@min}` must be positive"
14
+ else
15
+ "Choice upper bound `#{@max}` must be positive"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class NoRootMappingError < Error
4
+ def initialize(model)
5
+ super("#{model} has `no_root`, it allowed only for reusable models")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class NoRootNamespaceError < Error
4
+ def to_s
5
+ "Cannot assign namespace to `no_root`"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Type
6
+ class InvalidValueError < Error
7
+ def initialize(message)
8
+ @message = message
9
+
10
+ super()
11
+ end
12
+
13
+ def to_s
14
+ @message
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module Lutaml
2
+ module Model
3
+ class UnknownSequenceMappingError < Error
4
+ def initialize(method_name)
5
+ super("#{method_name} is not allowed in sequence")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -16,3 +16,12 @@ require_relative "error/type_error"
16
16
  require_relative "error/unknown_type_error"
17
17
  require_relative "error/multiple_mappings_error"
18
18
  require_relative "error/collection_true_missing_error"
19
+ require_relative "error/type/invalid_value_error"
20
+ require_relative "error/incorrect_sequence_error"
21
+ require_relative "error/choice_upper_bound_error"
22
+ require_relative "error/no_root_mapping_error"
23
+ require_relative "error/import_model_with_root_error"
24
+ require_relative "error/invalid_choice_range_error"
25
+ require_relative "error/unknown_sequence_mapping_error"
26
+ require_relative "error/choice_lower_bound_error"
27
+ require_relative "error/no_root_namespace_error"
@@ -10,7 +10,12 @@ module Lutaml
10
10
  end
11
11
 
12
12
  def to_json(*args)
13
- JSON.generate(@attributes, *args)
13
+ options = args.first || {}
14
+ if options[:pretty]
15
+ JSON.pretty_generate(@attributes, *args)
16
+ else
17
+ JSON.generate(@attributes, *args)
18
+ end
14
19
  end
15
20
  end
16
21
  end
@@ -17,7 +17,8 @@ module Lutaml
17
17
  with: {},
18
18
  delegate: nil,
19
19
  child_mappings: nil,
20
- root_mappings: nil
20
+ root_mappings: nil,
21
+ transform: {}
21
22
  )
22
23
  mapping_name = name_for_mapping(root_mappings, name)
23
24
  validate!(mapping_name, to, with)
@@ -31,11 +32,33 @@ module Lutaml
31
32
  delegate: delegate,
32
33
  child_mappings: child_mappings,
33
34
  root_mappings: root_mappings,
35
+ transform: transform,
34
36
  )
35
37
  end
36
38
 
37
39
  alias map_element map
38
40
 
41
+ def map_all(
42
+ to: nil,
43
+ render_nil: false,
44
+ render_default: false,
45
+ with: {},
46
+ delegate: nil
47
+ )
48
+ @raw_mapping = true
49
+ validate!(Constants::RAW_MAPPING_KEY, to, with)
50
+ @mappings << KeyValueMappingRule.new(
51
+ Constants::RAW_MAPPING_KEY,
52
+ to: to,
53
+ render_nil: render_nil,
54
+ render_default: render_default,
55
+ with: with,
56
+ delegate: delegate,
57
+ )
58
+ end
59
+
60
+ alias map_all_content map_all
61
+
39
62
  def name_for_mapping(root_mappings, name)
40
63
  return "root_mapping" if root_mappings
41
64
 
@@ -43,12 +66,14 @@ module Lutaml
43
66
  end
44
67
 
45
68
  def validate!(key, to, with)
46
- if to.nil? && with.empty?
69
+ validate_mappings!(key)
70
+
71
+ if to.nil? && with.empty? && !@raw_mapping
47
72
  msg = ":to or :with argument is required for mapping '#{key}'"
48
73
  raise IncorrectMappingArgumentsError.new(msg)
49
74
  end
50
75
 
51
- if !with.empty? && (with[:from].nil? || with[:to].nil?)
76
+ if !with.empty? && (with[:from].nil? || with[:to].nil?) && !@raw_mapping
52
77
  msg = ":with argument for mapping '#{key}' requires :to and :from keys"
53
78
  raise IncorrectMappingArgumentsError.new(msg)
54
79
  end
@@ -62,6 +87,12 @@ module Lutaml
62
87
  end
63
88
  end
64
89
 
90
+ def validate_mappings!(_type)
91
+ if (@raw_mapping && Utils.present?(@mappings)) || (!@raw_mapping && @mappings.any?(&:raw_mapping?))
92
+ raise StandardError, "map_all is not allowed with other mappings"
93
+ end
94
+ end
95
+
65
96
  def deep_dup
66
97
  self.class.new.tap do |new_mapping|
67
98
  new_mapping.instance_variable_set(:@mappings, duplicate_mappings)
@@ -14,7 +14,8 @@ module Lutaml
14
14
  with: {},
15
15
  delegate: nil,
16
16
  child_mappings: nil,
17
- root_mappings: nil
17
+ root_mappings: nil,
18
+ transform: {}
18
19
  )
19
20
  super(
20
21
  name,
@@ -22,7 +23,8 @@ module Lutaml
22
23
  render_nil: render_nil,
23
24
  render_default: render_default,
24
25
  with: with,
25
- delegate: delegate
26
+ delegate: delegate,
27
+ transform: transform
26
28
  )
27
29
 
28
30
  @child_mappings = child_mappings
@@ -0,0 +1,59 @@
1
+ require "liquid"
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Liquefiable
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def register_liquid_drop_class
12
+ if drop_class
13
+ raise "#{drop_class_name} Already exists!"
14
+ end
15
+
16
+ const_set(drop_class_name,
17
+ Class.new(Liquid::Drop) do
18
+ def initialize(object)
19
+ super()
20
+ @object = object
21
+ end
22
+ end)
23
+ end
24
+
25
+ def drop_class_name
26
+ @drop_class_name ||= if name
27
+ "#{to_s.split('::').last}Drop"
28
+ else
29
+ "Drop"
30
+ end
31
+ end
32
+
33
+ def drop_class
34
+ const_get(drop_class_name)
35
+ rescue StandardError
36
+ nil
37
+ end
38
+
39
+ def register_drop_method(method_name)
40
+ register_liquid_drop_class unless drop_class
41
+
42
+ drop_class.define_method(method_name) do
43
+ value = @object.public_send(method_name)
44
+
45
+ if value.is_a?(Array)
46
+ value.map(&:to_liquid)
47
+ else
48
+ value.to_liquid
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def to_liquid
55
+ self.class.drop_class.new(self)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -11,7 +11,7 @@ module Lutaml
11
11
  end
12
12
 
13
13
  def item_order
14
- @item_order&.map { |key| normalize(key) } || keys
14
+ @item_order
15
15
  end
16
16
 
17
17
  def fetch(key)
@@ -36,6 +36,14 @@ module Lutaml
36
36
  key?("#cdata-section") || key?("text")
37
37
  end
38
38
 
39
+ def assign_or_append_value(key, value)
40
+ self[key] = if self[key]
41
+ [self[key], value].flatten
42
+ else
43
+ value
44
+ end
45
+ end
46
+
39
47
  def ordered?
40
48
  @ordered
41
49
  end
@@ -7,7 +7,8 @@ module Lutaml
7
7
  :render_default,
8
8
  :attribute,
9
9
  :custom_methods,
10
- :delegate
10
+ :delegate,
11
+ :transform
11
12
 
12
13
  def initialize(
13
14
  name,
@@ -17,7 +18,8 @@ module Lutaml
17
18
  with: {},
18
19
  attribute: false,
19
20
  delegate: nil,
20
- root_mappings: nil
21
+ root_mappings: nil,
22
+ transform: {}
21
23
  )
22
24
  @name = name
23
25
  @to = to
@@ -27,6 +29,7 @@ module Lutaml
27
29
  @attribute = attribute
28
30
  @delegate = delegate
29
31
  @root_mappings = root_mappings
32
+ @transform = transform
30
33
  end
31
34
 
32
35
  alias from name
@@ -71,6 +74,8 @@ module Lutaml
71
74
  end
72
75
 
73
76
  model.public_send(delegate).public_send(:"#{to}=", value)
77
+ elsif transform_method = transform[:import] || attributes[to].transform_import_method
78
+ model.public_send(:"#{to}=", transform_method.call(value))
74
79
  else
75
80
  model.public_send(:"#{to}=", value)
76
81
  end
@@ -84,6 +89,18 @@ module Lutaml
84
89
  name.is_a?(Array)
85
90
  end
86
91
 
92
+ def raw_mapping?
93
+ name == Constants::RAW_MAPPING_KEY
94
+ end
95
+
96
+ def eql?(other)
97
+ other.class == self.class &&
98
+ instance_variables.all? do |var|
99
+ instance_variable_get(var) == other.instance_variable_get(var)
100
+ end
101
+ end
102
+ alias == eql?
103
+
87
104
  def deep_dup
88
105
  raise NotImplementedError, "Subclasses must implement `deep_dup`."
89
106
  end