lutaml-model 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,11 @@
1
1
  # lib/lutaml/model/serialize.rb
2
- require_relative "json_adapter/standard"
3
- require_relative "json_adapter/multi_json"
4
2
  require_relative "yaml_adapter"
5
3
  require_relative "xml_adapter"
6
- require_relative "toml_adapter/toml_rb_adapter"
7
- require_relative "toml_adapter/tomlib_adapter"
8
4
  require_relative "config"
9
5
  require_relative "type"
10
6
  require_relative "attribute"
11
7
  require_relative "mapping_rule"
8
+ require_relative "mapping_hash"
12
9
  require_relative "xml_mapping"
13
10
  require_relative "key_value_mapping"
14
11
  require_relative "json_adapter"
@@ -22,16 +19,29 @@ module Lutaml
22
19
  base.extend(ClassMethods)
23
20
  end
24
21
 
25
- # rubocop:disable Metrics/MethodLength
26
- # rubocop:disable Metrics/BlockLength
27
- # rubocop:disable Metrics/AbcSize
28
- # rubocop:disable Metrics/CyclomaticComplexity
29
- # rubocop:disable Metrics/PerceivedComplexity
30
22
  module ClassMethods
31
23
  attr_accessor :attributes, :mappings
32
24
 
25
+ def inherited(subclass)
26
+ super
27
+
28
+ @mappings ||= {}
29
+ @attributes ||= {}
30
+
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
42
+ end
43
+
33
44
  def attribute(name, type, options = {})
34
- self.attributes ||= {}
35
45
  attr = Attribute.new(name, type, options)
36
46
  attributes[name] = attr
37
47
 
@@ -40,29 +50,159 @@ module Lutaml
40
50
  end
41
51
 
42
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
+
43
57
  instance_variable_set(:"@#{name}", value)
44
58
  end
45
59
  end
46
60
 
47
61
  FORMATS.each do |format|
48
62
  define_method(format) do |&block|
49
- self.mappings ||= {}
50
63
  klass = format == :xml ? XmlMapping : KeyValueMapping
51
- self.mappings[format] = klass.new
52
- self.mappings[format].instance_eval(&block)
64
+ mappings[format] = klass.new
65
+ mappings[format].instance_eval(&block)
66
+
67
+ if format == :xml && !mappings[format].root_element
68
+ mappings[format].root(model.to_s)
69
+ end
53
70
  end
54
71
 
55
72
  define_method(:"from_#{format}") do |data|
56
73
  adapter = Lutaml::Model::Config.send(:"#{format}_adapter")
57
74
  doc = adapter.parse(data)
58
75
  mapped_attrs = apply_mappings(doc.to_h, format)
59
- apply_content_mapping(doc, mapped_attrs) if format == :xml
60
- new(mapped_attrs)
76
+ # apply_content_mapping(doc, mapped_attrs) if format == :xml
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
61
96
  end
62
97
  end
63
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
+
64
162
  def mappings_for(format)
65
- 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
66
206
  end
67
207
 
68
208
  def default_mappings(format)
@@ -75,11 +215,57 @@ module Lutaml
75
215
  end
76
216
  end
77
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
+
78
264
  def apply_mappings(doc, format)
79
265
  return apply_xml_mapping(doc) if format == :xml
80
266
 
81
267
  mappings = mappings_for(format).mappings
82
- mappings.each_with_object({}) do |rule, hash|
268
+ mappings.each_with_object(Lutaml::Model::MappingHash.new) do |rule, hash|
83
269
  attr = if rule.delegate
84
270
  attributes[rule.delegate].type.attributes[rule.to]
85
271
  else
@@ -95,15 +281,8 @@ module Lutaml
95
281
  else
96
282
  attr.default
97
283
  end
98
- # if attr.collection?
99
- # value = (value || []).map do |v|
100
- # attr.type <= Serialize ? attr.type.new(v) : v
101
- # end
102
- # elsif value.is_a?(Hash) && attr.type <= Serialize
103
- # value = attr.type.new(value)
104
- # else
105
- # value = attr.type.cast(value)
106
- # end
284
+
285
+ value = apply_child_mappings(value, rule.child_mappings)
107
286
 
108
287
  if attr.collection?
109
288
  value = (value || []).map do |v|
@@ -122,214 +301,116 @@ module Lutaml
122
301
  end
123
302
  end
124
303
 
125
- def apply_xml_mapping(doc)
304
+ def apply_xml_mapping(doc, caller_class: nil, mixed_content: false)
305
+ return unless doc
306
+
126
307
  mappings = mappings_for(:xml).mappings
127
308
 
128
- mappings.each_with_object({}) do |rule, hash|
309
+ if doc.is_a?(Array)
310
+ raise "May be `collection: true` is" \
311
+ "missing for #{self} in #{caller_class}"
312
+ end
313
+
314
+ mapping_hash = Lutaml::Model::MappingHash.new
315
+ mapping_hash.item_order = doc.item_order
316
+ mapping_hash.ordered = mappings_for(:xml).mixed_content? || mixed_content
317
+
318
+ mappings.each_with_object(mapping_hash) do |rule, hash|
129
319
  attr = attributes[rule.to]
130
320
  raise "Attribute '#{rule.to}' not found in #{self}" unless attr
131
321
 
132
- value = if rule.name
133
- doc[rule.name.to_s] || doc[rule.name.to_sym]
134
- else
322
+ is_content_mapping = rule.name.nil?
323
+ value = if is_content_mapping
135
324
  doc["text"]
325
+ else
326
+ doc[rule.name.to_s] || doc[rule.name.to_sym]
136
327
  end
137
328
 
138
- # if attr.collection?
139
- # value = (value || []).map do |v|
140
- # attr.type <= Serialize ? attr.type.from_hash(v) : v
141
- # end
142
- # elsif value.is_a?(Hash) && attr.type <= Serialize
143
- # value = attr.type.cast(value)
144
- # elsif value.is_a?(Array)
145
- # value = attr.type.cast(value.first["text"]&.first)
146
- # end
147
-
148
329
  if attr.collection?
330
+ if value && !value.is_a?(Array)
331
+ value = [value]
332
+ end
333
+
149
334
  value = (value || []).map do |v|
150
335
  if attr.type <= Serialize
151
- attr.type.apply_xml_mapping(v)
152
- else
336
+ attr.type.apply_xml_mapping(v, caller_class: self, mixed_content: rule.mixed_content)
337
+ elsif v.is_a?(Hash)
153
338
  v["text"]
339
+ else
340
+ v
154
341
  end
155
342
  end
156
343
  elsif attr.type <= Serialize
157
- value = attr.type.apply_xml_mapping(value) if value
344
+ value = attr.type.apply_xml_mapping(value, caller_class: self, mixed_content: rule.mixed_content)
158
345
  else
159
346
  if value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash
160
347
  value = value["text"]
161
348
  end
162
349
 
163
- value = attr.type.cast(value)
350
+ value = attr.type.cast(value) unless is_content_mapping
164
351
  end
352
+
165
353
  hash[rule.to] = value
166
354
  end
167
355
  end
168
356
 
169
- def apply_content_mapping(doc, mapped_attrs)
170
- content_mapping = mappings_for(:xml).content_mapping
171
- return unless content_mapping
172
-
173
- content = doc.root.children.select(&:text?).map(&:text)
174
- mapped_attrs[content_mapping.to] = content
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
175
370
  end
176
371
  end
177
372
 
178
- # rubocop:disable Layout/LineLength
373
+ attr_reader :element_order
374
+
179
375
  def initialize(attrs = {})
180
376
  return unless self.class.attributes
181
377
 
378
+ if attrs.is_a?(Lutaml::Model::MappingHash)
379
+ @ordered = attrs.ordered?
380
+ @element_order = attrs.item_order
381
+ end
382
+
182
383
  self.class.attributes.each do |name, attr|
183
- value = if attrs.key?(name)
184
- attrs[name]
185
- elsif attrs.key?(name.to_sym)
186
- attrs[name.to_sym]
187
- elsif attrs.key?(name.to_s)
188
- attrs[name.to_s]
189
- else
190
- attr.default
191
- end
384
+ value = self.class.attr_value(attrs, name, attr)
192
385
 
193
- value = if attr.collection?
194
- (value || []).map do |v|
195
- if v.is_a?(Hash)
196
- attr.type.new(v)
197
- else
198
- Lutaml::Model::Type.cast(
199
- v, attr.type
200
- )
201
- end
202
- end
203
- elsif value.is_a?(Hash) && attr.type != Lutaml::Model::Type::Hash
204
- attr.type.new(value)
205
- else
206
- Lutaml::Model::Type.cast(value, attr.type)
207
- end
208
- send(:"#{name}=", ensure_utf8(value))
386
+ send(:"#{name}=", self.class.ensure_utf8(value))
209
387
  end
210
388
  end
211
- # rubocop:enable Layout/LineLength
212
-
213
- # TODO: Make this work
214
- # FORMATS.each do |format|
215
- # define_method("to_#{format}") do |options = {}|
216
- # adapter = Lutaml::Model::Config.send("#{format}_adapter")
217
- # representation = if format == :yaml
218
- # self
219
- # else
220
- # hash_representation(format, options)
221
- # end
222
- # adapter.new(representation).send("to_#{format}", options)
223
- # end
224
- # end
225
-
226
- def to_xml(options = {})
227
- adapter = Lutaml::Model::Config.xml_adapter
228
- adapter.new(self).to_xml(options)
229
- end
230
-
231
- def to_json(options = {})
232
- adapter = Lutaml::Model::Config.json_adapter
233
- adapter.new(hash_representation(:json, options)).to_json(options)
234
- end
235
389
 
236
- def to_yaml(options = {})
237
- adapter = Lutaml::Model::Config.yaml_adapter
238
- adapter.to_yaml(self, options)
390
+ def ordered?
391
+ @ordered
239
392
  end
240
393
 
241
- def to_toml(options = {})
242
- adapter = Lutaml::Model::Config.toml_adapter
243
- adapter.new(hash_representation(:toml, options)).to_toml
394
+ def key_exist?(hash, key)
395
+ hash.key?(key) || hash.key?(key.to_sym) || hash.key?(key.to_s)
244
396
  end
245
397
 
246
- # TODO: END Make this work
247
-
248
- def hash_representation(format, options = {})
249
- only = options[:only]
250
- except = options[:except]
251
- mappings = self.class.mappings_for(format).mappings
252
-
253
- mappings.each_with_object({}) do |rule, hash|
254
- name = rule.to
255
- next if except&.include?(name) || (only && !only.include?(name))
256
-
257
- next handle_delegate(self, rule, hash) if rule.delegate
258
-
259
- value = if rule.custom_methods[:to]
260
- send(rule.custom_methods[:to], self, send(name))
261
- else
262
- send(name)
263
- end
264
-
265
- next if value.nil? && !rule.render_nil
266
-
267
- attribute = self.class.attributes[name]
268
-
269
- hash[rule.from] = case value
270
- when Array
271
- value.map do |v|
272
- if v.is_a?(Serialize)
273
- v.hash_representation(format, options)
274
- else
275
- attribute.type.serialize(v)
276
- end
277
- end
278
- else
279
- if value.is_a?(Serialize)
280
- value.hash_representation(format, options)
281
- else
282
- attribute.type.serialize(value)
283
- end
284
- end
285
- end
398
+ def key_value(hash, key)
399
+ hash[key] || hash[key.to_sym] || hash[key.to_s]
286
400
  end
287
401
 
288
- private
289
-
290
- def handle_delegate(_obj, rule, hash)
291
- name = rule.to
292
- value = send(rule.delegate).send(name)
293
- return if value.nil? && !rule.render_nil
294
-
295
- attribute = send(rule.delegate).class.attributes[name]
296
- hash[rule.from] = case value
297
- when Array
298
- value.map do |v|
299
- if v.is_a?(Serialize)
300
- v.hash_representation(format, options)
301
- else
302
- attribute.type.serialize(v)
303
- end
304
- end
305
- else
306
- if value.is_a?(Serialize)
307
- value.hash_representation(format, options)
308
- else
309
- attribute.type.serialize(value)
310
- end
311
- end
312
- 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
313
410
 
314
- def ensure_utf8(value)
315
- case value
316
- when String
317
- value.encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
318
- when Array
319
- value.map { |v| ensure_utf8(v) }
320
- when Hash
321
- value.transform_keys do |k|
322
- ensure_utf8(k)
323
- end.transform_values { |v| ensure_utf8(v) }
324
- else
325
- value
411
+ adapter.new(representation).public_send(:"to_#{format}", options)
326
412
  end
327
413
  end
328
- # rubocop:enable Metrics/MethodLength
329
- # rubocop:enable Metrics/BlockLength
330
- # rubocop:enable Metrics/AbcSize
331
- # rubocop:enable Metrics/CyclomaticComplexity
332
- # rubocop:enable Metrics/PerceivedComplexity
333
414
  end
334
415
  end
335
416
  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
@@ -74,6 +74,24 @@ module Lutaml
74
74
  end
75
75
  end
76
76
 
77
+ class TextWithTags
78
+ attr_reader :content
79
+
80
+ def initialize(ordered_text_with_tags)
81
+ @content = ordered_text_with_tags
82
+ end
83
+
84
+ def self.cast(value)
85
+ return value if value.is_a?(self)
86
+
87
+ new(value)
88
+ end
89
+
90
+ def self.serialize(value)
91
+ value.content.join
92
+ end
93
+ end
94
+
77
95
  class JSON
78
96
  attr_reader :value
79
97
 
@@ -94,7 +112,7 @@ module Lutaml
94
112
  end
95
113
 
96
114
  def self.cast(value)
97
- return value if value.is_a?(self)
115
+ return value if value.is_a?(self) || value.nil?
98
116
 
99
117
  new(::JSON.parse(value))
100
118
  end
@@ -104,15 +122,8 @@ module Lutaml
104
122
  end
105
123
  end
106
124
 
107
- # rubocop:disable Layout/LineLength
108
125
  UUID_REGEX = /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/
109
- # rubocop:enable Layout/LineLength
110
126
 
111
- # rubocop:disable Metrics/MethodLength
112
- # rubocop:disable Layout/LineLength
113
- # rubocop:disable Metrics/AbcSize
114
- # rubocop:disable Metrics/CyclomaticComplexity
115
- # rubocop:disable Metrics/PerceivedComplexity
116
127
  def self.cast(value, type)
117
128
  return if value.nil?
118
129
 
@@ -203,11 +214,6 @@ module Lutaml
203
214
  end
204
215
  end.to_h
205
216
  end
206
- # rubocop:enable Metrics/MethodLength
207
- # rubocop:enable Layout/LineLength
208
- # rubocop:enable Metrics/AbcSize
209
- # rubocop:enable Metrics/CyclomaticComplexity
210
- # rubocop:enable Metrics/PerceivedComplexity
211
217
  end
212
218
  end
213
219
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end