lutaml-model 0.5.2 → 0.5.4

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +2 -0
  3. data/.rubocop_todo.yml +39 -13
  4. data/Gemfile +1 -0
  5. data/README.adoc +430 -52
  6. data/lib/lutaml/model/constants.rb +7 -0
  7. data/lib/lutaml/model/error/type/invalid_value_error.rb +19 -0
  8. data/lib/lutaml/model/error.rb +1 -0
  9. data/lib/lutaml/model/key_value_mapping.rb +31 -2
  10. data/lib/lutaml/model/mapping_hash.rb +8 -0
  11. data/lib/lutaml/model/mapping_rule.rb +8 -0
  12. data/lib/lutaml/model/schema/templates/simple_type.rb +247 -0
  13. data/lib/lutaml/model/schema/xml_compiler.rb +720 -0
  14. data/lib/lutaml/model/schema.rb +5 -0
  15. data/lib/lutaml/model/serialize.rb +33 -13
  16. data/lib/lutaml/model/toml_adapter/toml_rb_adapter.rb +1 -2
  17. data/lib/lutaml/model/type/hash.rb +11 -11
  18. data/lib/lutaml/model/utils.rb +7 -0
  19. data/lib/lutaml/model/version.rb +1 -1
  20. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +5 -1
  21. data/lib/lutaml/model/xml_adapter/xml_document.rb +11 -15
  22. data/lib/lutaml/model/xml_mapping.rb +4 -2
  23. data/lib/lutaml/model/xml_mapping_rule.rb +1 -4
  24. data/lib/lutaml/model.rb +1 -0
  25. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  26. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  27. data/spec/fixtures/xml/test_schema.xsd +53 -0
  28. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  29. data/spec/lutaml/model/cdata_spec.rb +2 -2
  30. data/spec/lutaml/model/custom_model_spec.rb +7 -20
  31. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  32. data/spec/lutaml/model/map_all_spec.rb +188 -0
  33. data/spec/lutaml/model/mixed_content_spec.rb +15 -15
  34. data/spec/lutaml/model/render_nil_spec.rb +29 -0
  35. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1431 -0
  36. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  37. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  38. data/spec/lutaml/model/xml_mapping_spec.rb +108 -1
  39. metadata +12 -2
@@ -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
@@ -34,6 +34,10 @@ module Lutaml
34
34
  alias render_default? render_default
35
35
  alias attribute? attribute
36
36
 
37
+ def render?(value)
38
+ render_nil? || (!value.nil? && !Utils.empty_collection?(value))
39
+ end
40
+
37
41
  def serialize_attribute(model, element, doc)
38
42
  if custom_methods[:to]
39
43
  model.send(custom_methods[:to], model, element, doc)
@@ -80,6 +84,10 @@ module Lutaml
80
84
  name.is_a?(Array)
81
85
  end
82
86
 
87
+ def raw_mapping?
88
+ name == Constants::RAW_MAPPING_KEY
89
+ end
90
+
83
91
  def deep_dup
84
92
  raise NotImplementedError, "Subclasses must implement `deep_dup`."
85
93
  end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Model
5
+ module Schema
6
+ module Templates
7
+ module SimpleType
8
+ extend self
9
+ attr_accessor :simple_types
10
+
11
+ DEFAULT_CLASSES = %w[int integer string boolean].freeze
12
+
13
+ SUPPORTED_DATA_TYPES = {
14
+ nonNegativeInteger: { class_name: "Lutaml::Model::Type::String", validations: { pattern: /\+?[0-9]+/ } },
15
+ positiveInteger: { class_name: "Lutaml::Model::Type::Integer", validations: { min: 0 } },
16
+ base64Binary: { class_name: "Lutaml::Model::Type::String", validations: { pattern: /\A([A-Za-z0-9+\/]+={0,2}|\s)*\z/ } },
17
+ unsignedLong: { class_name: "Lutaml::Model::Type::Integer", validations: { min: 0, max: 18446744073709551615 } },
18
+ unsignedInt: { class_name: "Lutaml::Model::Type::Integer", validations: { min: 0, max: 4294967295 } },
19
+ hexBinary: { class_name: "Lutaml::Model::Type::String", validations: { pattern: /([0-9a-fA-F]{2})*/ } },
20
+ dateTime: { skippable: true, class_name: "Lutaml::Model::Type::DateTime" },
21
+ boolean: { skippable: true, class_name: "Lutaml::Model::Type::Boolean" },
22
+ integer: { skippable: true, class_name: "Lutaml::Model::Type::Integer" },
23
+ string: { skippable: true, class_name: "Lutaml::Model::Type::String" },
24
+ token: { class_name: "Lutaml::Model::Type::String", validations: { pattern: /\A[^\t\n\f\r ]+(?: [^\t\n\f\r ]+)*\z/ } },
25
+ long: { class_name: "Lutaml::Model::Type::Decimal" },
26
+ int: { skippable: true, class_name: "Lutaml::Model::Type::Integer" },
27
+ }.freeze
28
+
29
+ REF_TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: "-")
30
+ # frozen_string_literal: true
31
+
32
+ require "lutaml/model"
33
+ <%= "require_relative \#{Utils.snake_case(parent_class).inspect}\n" if require_parent -%>
34
+
35
+ class <%= klass_name %> < <%= parent_class %>; end
36
+ TEMPLATE
37
+
38
+ SUPPORTED_TYPES_TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: "-")
39
+ # frozen_string_literal: true
40
+
41
+ require "lutaml/model"
42
+
43
+ class <%= Utils.camel_case(klass_name.to_s) %> < <%= properties[:class_name].to_s %>
44
+ def self.cast(value)
45
+ return nil if value.nil?
46
+
47
+ value = super(value)
48
+ <%=
49
+ if pattern_exist = validations.key?(:pattern)
50
+ " pattern = %r{\#{validations[:pattern]}}\n\#{indent}raise Lutaml::Model::Type::InvalidValueError, \\"The value \\\#{value} does not match the required pattern: \\\#{pattern}\\" unless value.match?(pattern)\n"
51
+ end
52
+ -%>
53
+ <%=
54
+ if min_exist = validations.key?(:min)
55
+ " min = \#{validations[:min]}\n\#{indent}raise Lutaml::Model::Type::InvalidValueError, \\"The value \\\#{value} is less than the set limit: \\\#{min}\\" if value < min\n"
56
+ end
57
+ -%>
58
+ <%=
59
+ if max_exist = validations.key?(:max)
60
+ " max = \#{validations[:max]}\n\#{indent}raise Lutaml::Model::Type::InvalidValueError, \\"The value \\\#{value} is greater than the set limit: \\\#{max}\\" if value > max\n"
61
+ end
62
+ -%>
63
+ value
64
+ end
65
+ end
66
+ TEMPLATE
67
+
68
+ UNION_TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: "-")
69
+ # frozen_string_literal: true
70
+
71
+ require "lutaml/model"
72
+ <%=
73
+ resolve_required_files(unions)&.map do |file|
74
+ next if file.nil? || file.empty?
75
+
76
+ "require_relative \\\"\#{file}\\\""
77
+ end.compact.join("\n") + "\n"
78
+ -%>
79
+
80
+ class <%= klass_name %> < Lutaml::Model::Type::Value
81
+ def self.cast(value)
82
+ return nil if value.nil?
83
+
84
+ <%= unions.map do |union|
85
+ base_class = union.base_class.split(':').last
86
+ if DEFAULT_CLASSES.include?(base_class)
87
+ "\#{SUPPORTED_DATA_TYPES.dig(base_class.to_sym, :class_name)}.cast(value)"
88
+ else
89
+ "\#{Utils.camel_case(base_class)}.cast(value)"
90
+ end
91
+ end.join(" || ") %>
92
+ end
93
+ end
94
+ TEMPLATE
95
+
96
+ MODEL_TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: "-")
97
+ # frozen_string_literal: true
98
+ require "lutaml/model"
99
+ <%= "require_relative '\#{Utils.snake_case(parent_class)}'\n" if require_parent -%>
100
+
101
+ class <%= klass_name %> < <%= parent_class %>
102
+ <%= " VALUES = \#{values}.freeze\n\n" if values_exist = values&.any? -%>
103
+ <%= " LENGTHS = \#{properties[:length]&.map(&:value)}\n\n" if length_exist = properties&.key?(:length) -%>
104
+ def self.cast(value)
105
+ return nil if value.nil?
106
+
107
+ value = super(value)
108
+ <%= " raise_values_error(value) unless VALUES.include?(value)\n" if values_exist -%>
109
+ <%= " raise_length_error(value) unless LENGTHS.all?(value.length)\n" if length_exist -%>
110
+ <%=
111
+ if pattern_exist = properties.key?(:pattern)
112
+ " pattern = %r{\#{properties[:pattern]}}\n raise_pattern_error(value, pattern) unless value.match?(pattern)\n"
113
+ end
114
+ -%>
115
+ <%=
116
+ if min_length_exist = properties&.key_exist?(:min_length)
117
+ " min_length = \#{properties.min_length}\n raise_min_length_error(value, min_length) unless value.length >= min_length\n"
118
+ end
119
+ -%>
120
+ <%=
121
+ if max_length_exist = properties&.key_exist?(:max_length)
122
+ " max_length = \#{properties.max_length}\n raise_max_length_error(value, max_length) unless value.length <= max_length\n"
123
+ end
124
+ -%>
125
+ <%=
126
+ if min_bound_exist = (properties&.key_exist?(:min_inclusive) || properties&.key_exist?(:min_exclusive))
127
+ " min_bound = \#{properties[:min_inclusive] || properties[:min_exclusive]}\n raise_min_bound_error(value, min_bound) unless value >\#{'=' if properties.key?(:min_inclusive)} min_bound \n"
128
+ end
129
+ -%>
130
+ <%=
131
+ if max_bound_exist = (properties&.key_exist?(:max_inclusive) || properties&.key_exist?(:max_exclusive))
132
+ " max_bound = \#{properties[:max_inclusive] || properties[:max_exclusive]}\n raise_max_bound_error(value, max_bound) unless value <\#{'=' if properties.key?(:max_inclusive)} max_bound \n"
133
+ end
134
+ -%>
135
+ <%= "value" %>
136
+ end
137
+ <%= "\n private\n" if pattern_exist || values_exist || length_exist || min_length_exist || max_length_exist || min_bound_exist || max_bound_exist -%>
138
+ <%=
139
+ if pattern_exist
140
+ "\n def self.raise_pattern_error(value, pattern)\n raise Lutaml::Model::Type::InvalidValueError, \\"The value \\\#{value} does not match the required pattern: \\\#{pattern}\\"\n end\n"
141
+ end
142
+ -%>
143
+ <%=
144
+ if values_exist
145
+ "\n def self.raise_values_error(input_value)\n raise Lutaml::Model::InvalidValueError.new(self, input_value, VALUES)\n end\n"
146
+ end
147
+ -%>
148
+ <%=
149
+ if length_exist
150
+ "\n def self.raise_length_error(input_value)\n raise Lutaml::Model::Type::InvalidValueError, \\"The provided value \\\\\\"\\\#{input_value}\\\\\\" should match the specified lengths: \\\#{LENGTHS.join(',')}\\"\n end\n"
151
+ end
152
+ -%>
153
+ <%=
154
+ if min_length_exist
155
+ "\n def self.raise_min_length_error(input_value, min_length)\n raise Lutaml::Model::Type::InvalidValueError, \\"The provided value \\\\\\"\\\#{input_value}\\\\\\" has fewer characters than the minimum allowed \\\#{min_length}\\"\n end\n"
156
+ end
157
+ -%>
158
+ <%=
159
+ if max_length_exist
160
+ "\n def self.raise_max_length_error(input_value, max_length)\n raise Lutaml::Model::Type::InvalidValueError, \\"The provided value \\\\\\"\\\#{input_value}\\\\\\" exceeds the maximum allowed length of \\\#{max_length}\\"\n end\n"
161
+ end
162
+ -%>
163
+ <%=
164
+ if min_bound_exist
165
+ "\n def self.raise_min_bound_error(input_value, min_bound)\n raise Lutaml::Model::Type::InvalidValueError, \\"The provided value \\\\\\"\\\#{input_value}\\\\\\" is less than the minimum allowed value of \\\#{min_bound}\\"\n end\n"
166
+ end
167
+ -%>
168
+ <%=
169
+ if max_bound_exist
170
+ "\n def self.raise_max_bound_error(input_value, max_bound)\n raise Lutaml::Model::Type::InvalidValueError, \\"The provided value \\\\\\"\\\#{input_value}\\\\\\" exceeds the maximum allowed value of \\\#{max_bound}\\"\n end\n"
171
+ end
172
+ -%>
173
+ end
174
+ TEMPLATE
175
+
176
+ def create_simple_types(simple_types)
177
+ setup_supported_types
178
+ simple_types.each do |name, properties|
179
+ klass_name = Utils.camel_case(name)
180
+ @simple_types[name] = if @simple_types.key?(properties[:base_class]) && properties.one?
181
+ ref_template(properties, klass_name)
182
+ elsif properties&.key_exist?(:union)
183
+ union_template(properties, klass_name)
184
+ else
185
+ model_template(properties, klass_name)
186
+ end
187
+ end
188
+ @simple_types
189
+ end
190
+
191
+ private
192
+
193
+ # klass_name is used in template using `binding`
194
+ def model_template(properties, klass_name)
195
+ base_class = properties.base_class.split(":")&.last
196
+ parent_class, require_parent = extract_parent_class(base_class)
197
+ values = properties[:values] if properties.key_exist?(:values)
198
+ MODEL_TEMPLATE.result(binding)
199
+ end
200
+
201
+ def extract_parent_class(base_class)
202
+ klass = if SUPPORTED_DATA_TYPES[base_class.to_sym]&.key?(:class_name)
203
+ parent = false
204
+ SUPPORTED_DATA_TYPES.dig(base_class.to_sym, :class_name)
205
+ else
206
+ parent = true
207
+ Utils.camel_case(base_class.to_s)
208
+ end
209
+ [klass, parent]
210
+ end
211
+
212
+ # klass_name is used in template using `binding`
213
+ def union_template(properties, klass_name)
214
+ unions = properties.union
215
+ UNION_TEMPLATE.result(binding)
216
+ end
217
+
218
+ # klass_name is used in template using `binding`
219
+ def ref_template(properties, klass_name)
220
+ parent_class = properties.base_class
221
+ require_parent = true unless properties[:base_class].include?("Lutaml::Model::")
222
+ REF_TEMPLATE.result(binding)
223
+ end
224
+
225
+ def setup_supported_types
226
+ @simple_types = MappingHash.new
227
+ indent = " "
228
+ SUPPORTED_DATA_TYPES.each do |klass_name, properties|
229
+ validations = properties[:validations] || {}
230
+ next if properties[:skippable]
231
+
232
+ @simple_types[klass_name] = SUPPORTED_TYPES_TEMPLATE.result(binding)
233
+ end
234
+ end
235
+
236
+ def resolve_required_files(unions)
237
+ unions.map do |union|
238
+ next if DEFAULT_CLASSES.include?(union.base_class.split(":").last)
239
+
240
+ Utils.snake_case(union.base_class.split(":").last)
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end