lutaml-model 0.2.1 → 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.
@@ -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