lutaml-model 0.3.0 → 0.3.1

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