lutaml-model 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) 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 +396 -23
  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 +4 -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 +24 -8
  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/version.rb +1 -1
  19. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +5 -1
  20. data/lib/lutaml/model/xml_adapter/xml_document.rb +11 -15
  21. data/lib/lutaml/model/xml_mapping.rb +4 -2
  22. data/lib/lutaml/model/xml_mapping_rule.rb +1 -4
  23. data/lib/lutaml/model.rb +1 -0
  24. data/spec/fixtures/xml/invalid_math_document.xml +4 -0
  25. data/spec/fixtures/xml/math_document_schema.xsd +56 -0
  26. data/spec/fixtures/xml/test_schema.xsd +53 -0
  27. data/spec/fixtures/xml/valid_math_document.xml +4 -0
  28. data/spec/lutaml/model/cdata_spec.rb +2 -2
  29. data/spec/lutaml/model/custom_model_spec.rb +7 -20
  30. data/spec/lutaml/model/key_value_mapping_spec.rb +27 -0
  31. data/spec/lutaml/model/map_all_spec.rb +188 -0
  32. data/spec/lutaml/model/mixed_content_spec.rb +15 -15
  33. data/spec/lutaml/model/schema/xml_compiler_spec.rb +1431 -0
  34. data/spec/lutaml/model/with_child_mapping_spec.rb +2 -2
  35. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +52 -0
  36. data/spec/lutaml/model/xml_mapping_spec.rb +108 -1
  37. metadata +12 -2
@@ -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