lutaml-model 0.3.0 → 0.3.1

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,18 @@
1
+ module Lutaml
2
+ module Model
3
+ class InvalidValueError < Error
4
+ def initialize(attr_name, value, allowed_values)
5
+ @attr_name = attr_name
6
+ @value = value
7
+ @allowed_values = allowed_values
8
+
9
+ super()
10
+ end
11
+
12
+ def to_s
13
+ "#{@attr_name} is `#{@value}`, must be one of the " \
14
+ "following [#{@allowed_values.join(', ')}]"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ module Lutaml
2
+ module Model
3
+ class Error < StandardError
4
+ end
5
+ end
6
+ end
7
+
8
+ require_relative "error/invalid_value_error"
@@ -10,13 +10,21 @@ module Lutaml
10
10
  @mappings = []
11
11
  end
12
12
 
13
- def map(name, to:, render_nil: false, with: {}, delegate: nil)
13
+ def map(
14
+ name,
15
+ to:,
16
+ render_nil: false,
17
+ with: {},
18
+ delegate: nil,
19
+ child_mappings: nil
20
+ )
14
21
  @mappings << KeyValueMappingRule.new(
15
22
  name,
16
23
  to: to,
17
24
  render_nil: render_nil,
18
25
  with: with,
19
26
  delegate: delegate,
27
+ child_mappings: child_mappings,
20
28
  )
21
29
  end
22
30
 
@@ -7,7 +7,8 @@ module Lutaml
7
7
  :render_nil,
8
8
  :custom_methods,
9
9
  :delegate,
10
- :mixed_content
10
+ :mixed_content,
11
+ :child_mappings
11
12
 
12
13
  def initialize(
13
14
  name,
@@ -16,7 +17,8 @@ module Lutaml
16
17
  with: {},
17
18
  delegate: nil,
18
19
  mixed_content: false,
19
- namespace_set: false
20
+ namespace_set: false,
21
+ child_mappings: nil
20
22
  )
21
23
  @name = name
22
24
  @to = to
@@ -25,6 +27,7 @@ module Lutaml
25
27
  @delegate = delegate
26
28
  @mixed_content = mixed_content
27
29
  @namespace_set = namespace_set
30
+ @child_mappings = child_mappings
28
31
  end
29
32
 
30
33
  alias from name
@@ -25,11 +25,23 @@ module Lutaml
25
25
  def inherited(subclass)
26
26
  super
27
27
 
28
+ @mappings ||= {}
29
+ @attributes ||= {}
30
+
28
31
  subclass.instance_variable_set(:@attributes, @attributes.dup)
32
+ subclass.instance_variable_set(:@mappings, @mappings.dup)
33
+ subclass.instance_variable_set(:@model, subclass)
34
+ end
35
+
36
+ def model(klass = nil)
37
+ if klass
38
+ @model = klass
39
+ else
40
+ @model
41
+ end
29
42
  end
30
43
 
31
44
  def attribute(name, type, options = {})
32
- self.attributes ||= {}
33
45
  attr = Attribute.new(name, type, options)
34
46
  attributes[name] = attr
35
47
 
@@ -38,19 +50,22 @@ module Lutaml
38
50
  end
39
51
 
40
52
  define_method(:"#{name}=") do |value|
53
+ if options[:values] && !options[:values].include?(value)
54
+ raise Lutaml::Model::InvalidValueError.new(name, value, options[:values])
55
+ end
56
+
41
57
  instance_variable_set(:"@#{name}", value)
42
58
  end
43
59
  end
44
60
 
45
61
  FORMATS.each do |format|
46
62
  define_method(format) do |&block|
47
- self.mappings ||= {}
48
63
  klass = format == :xml ? XmlMapping : KeyValueMapping
49
- self.mappings[format] = klass.new
50
- self.mappings[format].instance_eval(&block)
64
+ mappings[format] = klass.new
65
+ mappings[format].instance_eval(&block)
51
66
 
52
- if format == :xml && !self.mappings[format].root_element
53
- self.mappings[format].root(to_s)
67
+ if format == :xml && !mappings[format].root_element
68
+ mappings[format].root(model.to_s)
54
69
  end
55
70
  end
56
71
 
@@ -59,12 +74,135 @@ module Lutaml
59
74
  doc = adapter.parse(data)
60
75
  mapped_attrs = apply_mappings(doc.to_h, format)
61
76
  # apply_content_mapping(doc, mapped_attrs) if format == :xml
62
- new(mapped_attrs)
77
+ generate_model_object(self, mapped_attrs)
78
+ end
79
+
80
+ define_method(:"to_#{format}") do |instance|
81
+ unless instance.is_a?(model)
82
+ msg = "argument is a '#{instance.class}' but should be a '#{model}'"
83
+ raise Lutaml::Model::IncorrectModelError, msg
84
+ end
85
+
86
+ adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
87
+
88
+ if format == :xml
89
+ xml_options = { mapper_class: self }
90
+
91
+ adapter.new(instance).public_send(:"to_#{format}", xml_options)
92
+ else
93
+ hash = hash_representation(instance, format)
94
+ adapter.new(hash).public_send(:"to_#{format}")
95
+ end
63
96
  end
64
97
  end
65
98
 
99
+ def hash_representation(instance, format, options = {})
100
+ only = options[:only]
101
+ except = options[:except]
102
+ mappings = mappings_for(format).mappings
103
+
104
+ mappings.each_with_object({}) do |rule, hash|
105
+ name = rule.to
106
+ next if except&.include?(name) || (only && !only.include?(name))
107
+
108
+ next handle_delegate(instance, rule, hash) if rule.delegate
109
+
110
+ value = if rule.custom_methods[:to]
111
+ instance.send(rule.custom_methods[:to], instance, instance.send(name))
112
+ else
113
+ instance.send(name)
114
+ end
115
+
116
+ next if value.nil? && !rule.render_nil
117
+
118
+ attribute = attributes[name]
119
+
120
+ hash[rule.from] = if rule.child_mappings
121
+ generate_hash_from_child_mappings(value, rule.child_mappings)
122
+ elsif value.is_a?(Array)
123
+ value.map do |v|
124
+ if attribute.type <= Serialize
125
+ attribute.type.hash_representation(v, format, options)
126
+ else
127
+ attribute.type.serialize(v)
128
+ end
129
+ end
130
+ elsif attribute.type <= Serialize
131
+ attribute.type.hash_representation(value, format, options)
132
+ else
133
+ attribute.type.serialize(value)
134
+ end
135
+ end
136
+ end
137
+
138
+ def handle_delegate(instance, rule, hash)
139
+ name = rule.to
140
+ value = instance.send(rule.delegate).send(name)
141
+ return if value.nil? && !rule.render_nil
142
+
143
+ attribute = instance.send(rule.delegate).class.attributes[name]
144
+ hash[rule.from] = case value
145
+ when Array
146
+ value.map do |v|
147
+ if v.is_a?(Serialize)
148
+ hash_representation(v, format, options)
149
+ else
150
+ attribute.type.serialize(v)
151
+ end
152
+ end
153
+ else
154
+ if value.is_a?(Serialize)
155
+ hash_representation(value, format, options)
156
+ else
157
+ attribute.type.serialize(value)
158
+ end
159
+ end
160
+ end
161
+
66
162
  def mappings_for(format)
67
- self.mappings[format] || default_mappings(format)
163
+ mappings[format] || default_mappings(format)
164
+ end
165
+
166
+ def generate_model_object(type, mapped_attrs)
167
+ return type.model.new(mapped_attrs) if self == model
168
+
169
+ instance = type.model.new
170
+
171
+ type.attributes.each do |name, attr|
172
+ value = attr_value(mapped_attrs, name, attr)
173
+
174
+ instance.send(:"#{name}=", ensure_utf8(value))
175
+ end
176
+
177
+ instance
178
+ end
179
+
180
+ def attr_value(attrs, name, attr_rule)
181
+ value = if attrs.key?(name)
182
+ attrs[name]
183
+ elsif attrs.key?(name.to_sym)
184
+ attrs[name.to_sym]
185
+ elsif attrs.key?(name.to_s)
186
+ attrs[name.to_s]
187
+ else
188
+ attr_rule.default
189
+ end
190
+
191
+ if attr_rule.collection? || value.is_a?(Array)
192
+ (value || []).map do |v|
193
+ if v.is_a?(Hash)
194
+ attr_rule.type.new(v)
195
+ else
196
+ Lutaml::Model::Type.cast(
197
+ v, attr_rule.type
198
+ )
199
+ end
200
+ end
201
+ elsif value.is_a?(Hash) && attr_rule.type != Lutaml::Model::Type::Hash
202
+ generate_model_object(attr_rule.type, value)
203
+ else
204
+ Lutaml::Model::Type.cast(value, attr_rule.type)
205
+ end
68
206
  end
69
207
 
70
208
  def default_mappings(format)
@@ -77,6 +215,52 @@ module Lutaml
77
215
  end
78
216
  end
79
217
 
218
+ def apply_child_mappings(hash, child_mappings)
219
+ return hash unless child_mappings
220
+
221
+ hash.map do |key, value|
222
+ child_mappings.to_h do |attr_name, path|
223
+ attr_value = if path == :key
224
+ key
225
+ elsif path == :value
226
+ value
227
+ else
228
+ path = [path] unless path.is_a?(Array)
229
+ value.dig(*path.map(&:to_s))
230
+ end
231
+
232
+ [attr_name, attr_value]
233
+ end
234
+ end
235
+ end
236
+
237
+ def generate_hash_from_child_mappings(value, child_mappings)
238
+ return value unless child_mappings
239
+
240
+ hash = {}
241
+
242
+ value.each do |child_obj|
243
+ map_key = nil
244
+ map_value = {}
245
+ child_mappings.each do |attr_name, path|
246
+ if path == :key
247
+ map_key = child_obj.send(attr_name)
248
+ elsif path == :value
249
+ map_value = child_obj.send(attr_name)
250
+ else
251
+ path = [path] unless path.is_a?(Array)
252
+ path[0...-1].inject(map_value) do |acc, k|
253
+ acc[k.to_s] ||= {}
254
+ end.public_send(:[]=, path.last.to_s, child_obj.send(attr_name))
255
+ end
256
+ end
257
+ # hash[mapping.name] ||= {}
258
+ hash[map_key] = map_value
259
+ end
260
+
261
+ hash
262
+ end
263
+
80
264
  def apply_mappings(doc, format)
81
265
  return apply_xml_mapping(doc) if format == :xml
82
266
 
@@ -98,6 +282,8 @@ module Lutaml
98
282
  attr.default
99
283
  end
100
284
 
285
+ value = apply_child_mappings(value, rule.child_mappings)
286
+
101
287
  if attr.collection?
102
288
  value = (value || []).map do |v|
103
289
  attr.type <= Serialize ? attr.type.apply_mappings(v, format) : v
@@ -167,6 +353,21 @@ module Lutaml
167
353
  hash[rule.to] = value
168
354
  end
169
355
  end
356
+
357
+ def ensure_utf8(value)
358
+ case value
359
+ when String
360
+ value.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
361
+ when Array
362
+ value.map { |v| ensure_utf8(v) }
363
+ when Hash
364
+ value.transform_keys do |k|
365
+ ensure_utf8(k)
366
+ end.transform_values { |v| ensure_utf8(v) }
367
+ else
368
+ value
369
+ end
370
+ end
170
371
  end
171
372
 
172
373
  attr_reader :element_order
@@ -180,37 +381,9 @@ module Lutaml
180
381
  end
181
382
 
182
383
  self.class.attributes.each do |name, attr|
183
- value = attr_value(attrs, name, attr)
184
-
185
- send(:"#{name}=", ensure_utf8(value))
186
- end
187
- end
188
-
189
- def attr_value(attrs, name, attr_rule)
190
- value = if attrs.key?(name)
191
- attrs[name]
192
- elsif attrs.key?(name.to_sym)
193
- attrs[name.to_sym]
194
- elsif attrs.key?(name.to_s)
195
- attrs[name.to_s]
196
- else
197
- attr_rule.default
198
- end
384
+ value = self.class.attr_value(attrs, name, attr)
199
385
 
200
- if attr_rule.collection? || value.is_a?(Array)
201
- (value || []).map do |v|
202
- if v.is_a?(Hash)
203
- attr_rule.type.new(v)
204
- else
205
- Lutaml::Model::Type.cast(
206
- v, attr_rule.type
207
- )
208
- end
209
- end
210
- elsif value.is_a?(Hash) && attr_rule.type != Lutaml::Model::Type::Hash
211
- attr_rule.type.new(value)
212
- else
213
- Lutaml::Model::Type.cast(value, attr_rule.type)
386
+ send(:"#{name}=", self.class.ensure_utf8(value))
214
387
  end
215
388
  end
216
389
 
@@ -226,119 +399,16 @@ module Lutaml
226
399
  hash[key] || hash[key.to_sym] || hash[key.to_s]
227
400
  end
228
401
 
229
- # TODO: Make this work
230
- # FORMATS.each do |format|
231
- # define_method("to_#{format}") do |options = {}|
232
- # adapter = Lutaml::Model::Config.send("#{format}_adapter")
233
- # representation = if format == :yaml
234
- # self
235
- # else
236
- # hash_representation(format, options)
237
- # end
238
- # adapter.new(representation).send("to_#{format}", options)
239
- # end
240
- # end
241
-
242
- def to_xml(options = {})
243
- adapter = Lutaml::Model::Config.xml_adapter
244
- adapter.new(self).to_xml(options)
245
- end
246
-
247
- def to_json(options = {})
248
- adapter = Lutaml::Model::Config.json_adapter
249
- adapter.new(hash_representation(:json, options)).to_json(options)
250
- end
251
-
252
- def to_yaml(options = {})
253
- adapter = Lutaml::Model::Config.yaml_adapter
254
- adapter.to_yaml(self, options)
255
- end
256
-
257
- def to_toml(options = {})
258
- adapter = Lutaml::Model::Config.toml_adapter
259
- adapter.new(hash_representation(:toml, options)).to_toml
260
- end
261
-
262
- # TODO: END Make this work
263
-
264
- def hash_representation(format, options = {})
265
- only = options[:only]
266
- except = options[:except]
267
- mappings = self.class.mappings_for(format).mappings
268
-
269
- mappings.each_with_object({}) do |rule, hash|
270
- name = rule.to
271
- next if except&.include?(name) || (only && !only.include?(name))
272
-
273
- next handle_delegate(self, rule, hash) if rule.delegate
274
-
275
- value = if rule.custom_methods[:to]
276
- send(rule.custom_methods[:to], self, send(name))
277
- else
278
- send(name)
279
- end
280
-
281
- next if value.nil? && !rule.render_nil
282
-
283
- attribute = self.class.attributes[name]
284
-
285
- hash[rule.from] = case value
286
- when Array
287
- value.map do |v|
288
- if v.is_a?(Serialize)
289
- v.hash_representation(format, options)
290
- else
291
- attribute.type.serialize(v)
292
- end
293
- end
294
- else
295
- if value.is_a?(Serialize)
296
- value.hash_representation(format, options)
297
- else
298
- attribute.type.serialize(value)
299
- end
300
- end
301
- end
302
- end
303
-
304
- private
305
-
306
- def handle_delegate(_obj, rule, hash)
307
- name = rule.to
308
- value = send(rule.delegate).send(name)
309
- return if value.nil? && !rule.render_nil
310
-
311
- attribute = send(rule.delegate).class.attributes[name]
312
- hash[rule.from] = case value
313
- when Array
314
- value.map do |v|
315
- if v.is_a?(Serialize)
316
- v.hash_representation(format, options)
317
- else
318
- attribute.type.serialize(v)
319
- end
320
- end
321
- else
322
- if value.is_a?(Serialize)
323
- value.hash_representation(format, options)
324
- else
325
- attribute.type.serialize(value)
326
- end
327
- end
328
- end
402
+ FORMATS.each do |format|
403
+ define_method(:"to_#{format}") do |options = {}|
404
+ adapter = Lutaml::Model::Config.public_send(:"#{format}_adapter")
405
+ representation = if format == :xml
406
+ self
407
+ else
408
+ self.class.hash_representation(self, format, options)
409
+ end
329
410
 
330
- def ensure_utf8(value)
331
- case value
332
- when String
333
- value.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
334
- when Array
335
- value.map { |v| ensure_utf8(v) }
336
- when Hash
337
- value.transform_keys do |k|
338
- ensure_utf8(k)
339
- end.transform_values { |v| ensure_utf8(v) }
340
- else
341
- value
411
+ adapter.new(representation).public_send(:"to_#{format}", options)
342
412
  end
343
413
  end
344
414
  end
@@ -11,8 +11,8 @@ module Lutaml
11
11
  new(data)
12
12
  end
13
13
 
14
- def to_toml(*args)
15
- TomlRB.dump(to_h, *args)
14
+ def to_toml(*)
15
+ TomlRB.dump(to_h)
16
16
  end
17
17
  end
18
18
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
@@ -13,11 +13,12 @@ module Lutaml
13
13
  end
14
14
 
15
15
  def to_xml(options = {})
16
- builder = Nokogiri::XML::Builder.new do |xml|
16
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
17
17
  if root.is_a?(Lutaml::Model::XmlAdapter::NokogiriElement)
18
18
  root.to_xml(xml)
19
19
  else
20
- options[:xml_attributes] = build_namespace_attributes(@root.class)
20
+ mapper_class = options[:mapper_class] || @root.class
21
+ options[:xml_attributes] = build_namespace_attributes(mapper_class)
21
22
  build_element(xml, @root, options)
22
23
  end
23
24
  end
@@ -32,7 +33,8 @@ module Lutaml
32
33
  private
33
34
 
34
35
  def build_unordered_element(xml, element, options = {})
35
- xml_mapping = element.class.mappings_for(:xml)
36
+ mapper_class = options[:mapper_class] || element.class
37
+ xml_mapping = mapper_class.mappings_for(:xml)
36
38
  return xml unless xml_mapping
37
39
 
38
40
  attributes = options[:xml_attributes] ||= {}
@@ -53,7 +55,7 @@ module Lutaml
53
55
  end
54
56
 
55
57
  xml_mapping.elements.each do |element_rule|
56
- attribute_def = attribute_definition_for(element, element_rule)
58
+ attribute_def = attribute_definition_for(element, element_rule, mapper_class: mapper_class)
57
59
  value = attribute_value_for(element, element_rule)
58
60
 
59
61
  next if value.nil? && !element_rule.render_nil?
@@ -79,7 +81,8 @@ module Lutaml
79
81
  end
80
82
 
81
83
  def build_ordered_element(xml, element, options = {})
82
- xml_mapping = element.class.mappings_for(:xml)
84
+ mapper_class = options[:mapper_class] || element.class
85
+ xml_mapping = mapper_class.mappings_for(:xml)
83
86
  return xml unless xml_mapping
84
87
 
85
88
  attributes = build_attributes(element, xml_mapping)&.compact
@@ -106,7 +109,7 @@ module Lutaml
106
109
  element_rule = xml_mapping.find_by_name(name)
107
110
  next if element_rule.nil?
108
111
 
109
- attribute_def = attribute_definition_for(element, element_rule)
112
+ attribute_def = attribute_definition_for(element, element_rule, mapper_class: mapper_class)
110
113
  value = attribute_value_for(element, element_rule)
111
114
  nsp_xml = element_rule.prefix ? xml[element_rule.prefix] : xml
112
115
 
@@ -127,7 +130,12 @@ module Lutaml
127
130
 
128
131
  def add_to_xml(xml, value, attribute, rule)
129
132
  if value && (attribute&.type&.<= Lutaml::Model::Serialize)
130
- handle_nested_elements(xml, value, rule)
133
+ handle_nested_elements(
134
+ xml,
135
+ value,
136
+ rule: rule,
137
+ attribute: attribute,
138
+ )
131
139
  else
132
140
  xml.public_send(rule.name) do
133
141
  if !value.nil?
@@ -14,9 +14,10 @@ module Lutaml
14
14
 
15
15
  def to_xml(options = {})
16
16
  builder = Ox::Builder.new
17
+
17
18
  if @root.is_a?(Lutaml::Model::XmlAdapter::OxElement)
18
19
  @root.to_xml(builder)
19
- elsif @root.ordered?
20
+ elsif ordered?(@root, options)
20
21
  build_ordered_element(builder, @root, options)
21
22
  else
22
23
  build_element(builder, @root, options)
@@ -30,7 +31,8 @@ module Lutaml
30
31
  private
31
32
 
32
33
  def build_unordered_element(builder, element, options = {})
33
- xml_mapping = element.class.mappings_for(:xml)
34
+ mapper_class = options[:mapper_class] || element.class
35
+ xml_mapping = mapper_class.mappings_for(:xml)
34
36
  return xml unless xml_mapping
35
37
 
36
38
  attributes = build_attributes(element, xml_mapping).compact
@@ -45,7 +47,7 @@ module Lutaml
45
47
 
46
48
  builder.element(prefixed_name, attributes) do |el|
47
49
  xml_mapping.elements.each do |element_rule|
48
- attribute_def = attribute_definition_for(element, element_rule)
50
+ attribute_def = attribute_definition_for(element, element_rule, mapper_class: mapper_class)
49
51
  value = attribute_value_for(element, element_rule)
50
52
 
51
53
  val = if attribute_def.collection?
@@ -58,7 +60,7 @@ module Lutaml
58
60
 
59
61
  val.each do |v|
60
62
  if attribute_def&.type&.<= Lutaml::Model::Serialize
61
- handle_nested_elements(el, v, element_rule)
63
+ handle_nested_elements(el, v, rule: element_rule, attribute: attribute_def)
62
64
  else
63
65
  builder.element(element_rule.prefixed_name) do |el|
64
66
  el.text(attribute_def.type.serialize(v)) if v
@@ -76,8 +78,9 @@ module Lutaml
76
78
  end
77
79
  end
78
80
 
79
- def build_ordered_element(builder, element, _options = {})
80
- xml_mapping = element.class.mappings_for(:xml)
81
+ def build_ordered_element(builder, element, options = {})
82
+ mapper_class = options[:mapper_class] || element.class
83
+ xml_mapping = mapper_class.mappings_for(:xml)
81
84
  return xml unless xml_mapping
82
85
 
83
86
  attributes = build_attributes(element, xml_mapping).compact
@@ -91,7 +94,7 @@ module Lutaml
91
94
 
92
95
  element_rule = xml_mapping.find_by_name(name)
93
96
 
94
- attribute_def = attribute_definition_for(element, element_rule)
97
+ attribute_def = attribute_definition_for(element, element_rule, mapper_class: mapper_class)
95
98
  value = attribute_value_for(element, element_rule)
96
99
 
97
100
  if element_rule == xml_mapping.content_mapping
@@ -110,7 +113,12 @@ module Lutaml
110
113
 
111
114
  def add_to_xml(xml, value, attribute, rule)
112
115
  if value && (attribute&.type&.<= Lutaml::Model::Serialize)
113
- handle_nested_elements(xml, value, rule)
116
+ handle_nested_elements(
117
+ xml,
118
+ value,
119
+ rule: rule,
120
+ attribute: attribute,
121
+ )
114
122
  else
115
123
  xml.element(rule.name) do |el|
116
124
  if !value.nil?