shale 0.7.1 → 0.9.0

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,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'descriptor/xml'
4
+ require_relative 'descriptor/xml_namespace'
5
+ require_relative 'validator'
6
+
7
+ module Shale
8
+ module Mapping
9
+ # Base class for Mapping XML serialization format
10
+ #
11
+ # @api private
12
+ class XmlBase
13
+ # Return elements mapping hash
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ # @api private
18
+ attr_reader :elements
19
+
20
+ # Return attributes mapping hash
21
+ #
22
+ # @return [Hash]
23
+ #
24
+ # @api private
25
+ attr_reader :attributes
26
+
27
+ # Return content mapping
28
+ #
29
+ # @return [Symbol]
30
+ #
31
+ # @api private
32
+ attr_reader :content
33
+
34
+ # Return default namespace
35
+ #
36
+ # @return [Shale::Mapping::Descriptor::XmlNamespace]
37
+ #
38
+ # @api private
39
+ attr_reader :default_namespace
40
+
41
+ # Return unprefixed root
42
+ #
43
+ # @return [String]
44
+ #
45
+ # @api private
46
+ def unprefixed_root
47
+ @root
48
+ end
49
+
50
+ # Return prefixed root
51
+ #
52
+ # @return [String]
53
+ #
54
+ # @api private
55
+ def prefixed_root
56
+ [default_namespace.prefix, @root].compact.join(':')
57
+ end
58
+
59
+ # Initialize instance
60
+ #
61
+ # @api private
62
+ def initialize
63
+ super
64
+ @elements = {}
65
+ @attributes = {}
66
+ @content = nil
67
+ @root = ''
68
+ @default_namespace = Descriptor::XmlNamespace.new
69
+ @finalized = false
70
+ end
71
+
72
+ # Map element to attribute
73
+ #
74
+ # @param [String] element
75
+ # @param [Symbol, nil] to
76
+ # @param [Hash, nil] using
77
+ # @param [String, nil] group
78
+ # @param [String, nil] namespace
79
+ # @param [String, nil] prefix
80
+ # @param [true, false] cdata
81
+ # @param [true, false] render_nil
82
+ #
83
+ # @raise [IncorrectMappingArgumentsError] when arguments are incorrect
84
+ #
85
+ # @api private
86
+ def map_element(
87
+ element,
88
+ to: nil,
89
+ using: nil,
90
+ group: nil,
91
+ namespace: :undefined,
92
+ prefix: :undefined,
93
+ cdata: false,
94
+ render_nil: false
95
+ )
96
+ Validator.validate_arguments(element, to, using)
97
+ Validator.validate_namespace(element, namespace, prefix)
98
+
99
+ if namespace == :undefined && prefix == :undefined
100
+ nsp = default_namespace.name
101
+ pfx = default_namespace.prefix
102
+ else
103
+ nsp = namespace
104
+ pfx = prefix
105
+ end
106
+
107
+ namespaced_element = [nsp, element].compact.join(':')
108
+
109
+ @elements[namespaced_element] = Descriptor::Xml.new(
110
+ name: element,
111
+ attribute: to,
112
+ methods: using,
113
+ group: group,
114
+ namespace: Descriptor::XmlNamespace.new(nsp, pfx),
115
+ cdata: cdata,
116
+ render_nil: render_nil
117
+ )
118
+ end
119
+
120
+ # Map document's attribute to object's attribute
121
+ #
122
+ # @param [String] attribute
123
+ # @param [Symbol, nil] to
124
+ # @param [Hash, nil] using
125
+ # @param [String, nil] namespace
126
+ # @param [String, nil] prefix
127
+ # @param [true, false] render_nil
128
+ #
129
+ # @raise [IncorrectMappingArgumentsError] when arguments are incorrect
130
+ #
131
+ # @api private
132
+ def map_attribute(
133
+ attribute,
134
+ to: nil,
135
+ using: nil,
136
+ group: nil,
137
+ namespace: nil,
138
+ prefix: nil,
139
+ render_nil: false
140
+ )
141
+ Validator.validate_arguments(attribute, to, using)
142
+ Validator.validate_namespace(attribute, namespace, prefix)
143
+
144
+ namespaced_attribute = [namespace, attribute].compact.join(':')
145
+
146
+ @attributes[namespaced_attribute] = Descriptor::Xml.new(
147
+ name: attribute,
148
+ attribute: to,
149
+ methods: using,
150
+ namespace: Descriptor::XmlNamespace.new(namespace, prefix),
151
+ cdata: false,
152
+ group: group,
153
+ render_nil: render_nil
154
+ )
155
+ end
156
+
157
+ # Map document's content to object's attribute
158
+ #
159
+ # @param [Symbol] to
160
+ # @param [Hash, nil] using
161
+ # @param [true, false] cdata
162
+ #
163
+ # @api private
164
+ def map_content(to: nil, using: nil, group: nil, cdata: false)
165
+ Validator.validate_arguments('content', to, using)
166
+
167
+ @content = Descriptor::Xml.new(
168
+ name: nil,
169
+ attribute: to,
170
+ methods: using,
171
+ namespace: Descriptor::XmlNamespace.new(nil, nil),
172
+ cdata: cdata,
173
+ group: group,
174
+ render_nil: false
175
+ )
176
+ end
177
+
178
+ # Set the name for root element
179
+ #
180
+ # @param [String] value root's name
181
+ #
182
+ # @api private
183
+ def root(value)
184
+ @root = value
185
+ end
186
+
187
+ # Set default namespace for root element
188
+ #
189
+ # @param [String] name
190
+ # @param [String] prefix
191
+ #
192
+ # @api private
193
+ def namespace(name, prefix)
194
+ @default_namespace.name = name
195
+ @default_namespace.prefix = prefix
196
+ end
197
+
198
+ # Set the "finalized" instance variable to true
199
+ #
200
+ # @api private
201
+ def finalize!
202
+ @finalized = true
203
+ end
204
+
205
+ # Query the "finalized" instance variable
206
+ #
207
+ # @return [truem false]
208
+ #
209
+ # @api private
210
+ def finalized?
211
+ @finalized
212
+ end
213
+
214
+ # @api private
215
+ def initialize_dup(other)
216
+ @elements = other.instance_variable_get('@elements').dup
217
+ @attributes = other.instance_variable_get('@attributes').dup
218
+ @content = other.instance_variable_get('@content').dup
219
+ @root = other.instance_variable_get('@root').dup
220
+ @default_namespace = other.instance_variable_get('@default_namespace').dup
221
+ @finalized = false
222
+
223
+ super
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'xml_base'
4
+
5
+ module Shale
6
+ module Mapping
7
+ # Group for XML serialization format
8
+ #
9
+ # @api private
10
+ class XmlGroup < XmlBase
11
+ # Return name of the group
12
+ #
13
+ # @return [String]
14
+ #
15
+ # @api private
16
+ attr_reader :name
17
+
18
+ # Initialize instance
19
+ #
20
+ # @api private
21
+ def initialize(from, to)
22
+ super()
23
+ @from = from
24
+ @to = to
25
+ @name = "group_#{hash}"
26
+ end
27
+
28
+ # Map element to attribute
29
+ #
30
+ # @param [String] element
31
+ # @param [String, nil] namespace
32
+ # @param [String, nil] prefix
33
+ #
34
+ # @api private
35
+ def map_element(element, namespace: :undefined, prefix: :undefined)
36
+ super(
37
+ element,
38
+ using: { from: @from, to: @to },
39
+ group: @name,
40
+ namespace: namespace,
41
+ prefix: prefix
42
+ )
43
+ end
44
+
45
+ # Map document's attribute to object's attribute
46
+ #
47
+ # @param [String] attribute
48
+ # @param [String, nil] namespace
49
+ # @param [String, nil] prefix
50
+ #
51
+ # @api private
52
+ def map_attribute(attribute, namespace: nil, prefix: nil)
53
+ super(
54
+ attribute,
55
+ using: { from: @from, to: @to },
56
+ group: @name,
57
+ namespace: namespace,
58
+ prefix: prefix
59
+ )
60
+ end
61
+
62
+ # Map document's content to object's attribute
63
+ #
64
+ # @api private
65
+ def map_content
66
+ super(using: { from: @from, to: @to }, group: @name)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../error'
4
+ require_relative '../mapping/group/dict_grouping'
5
+ require_relative '../mapping/group/xml_grouping'
4
6
  require_relative 'value'
5
7
 
6
8
  module Shale
@@ -11,11 +13,11 @@ module Shale
11
13
  # @api private
12
14
  class Complex < Value
13
15
  class << self
14
- %i[hash json yaml toml].each do |format|
16
+ %i[hash json yaml toml csv].each do |format|
15
17
  class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
16
18
  # Convert Hash to Object using Hash/JSON/YAML/TOML mapping
17
19
  #
18
- # @param [Hash] hash Hash to convert
20
+ # @param [Hash, Array] hash Hash to convert
19
21
  # @param [Array<Symbol>] only
20
22
  # @param [Array<Symbol>] except
21
23
  # @param [any] context
@@ -24,6 +26,18 @@ module Shale
24
26
  #
25
27
  # @api public
26
28
  def of_#{format}(hash, only: nil, except: nil, context: nil)
29
+ #{
30
+ if format != :toml
31
+ <<~CODE
32
+ if hash.is_a?(Array)
33
+ return hash.map do |item|
34
+ of_#{format}(item, only: only, except: except, context: context)
35
+ end
36
+ end
37
+ CODE
38
+ end
39
+ }
40
+
27
41
  instance = model.new
28
42
 
29
43
  attributes
@@ -32,6 +46,7 @@ module Shale
32
46
  .each { |attr| instance.send(attr.setter, attr.default.call) }
33
47
 
34
48
  mapping_keys = #{format}_mapping.keys
49
+ grouping = Shale::Mapping::Group::DictGrouping.new
35
50
 
36
51
  only = to_partial_render_attributes(only)
37
52
  except = to_partial_render_attributes(except)
@@ -40,7 +55,9 @@ module Shale
40
55
  mapping = mapping_keys[key]
41
56
  next unless mapping
42
57
 
43
- if mapping.method_from
58
+ if mapping.group
59
+ grouping.add(mapping, value)
60
+ elsif mapping.method_from
44
61
  mapper = new
45
62
 
46
63
  if mapper.method(mapping.method_from).arity == 3
@@ -89,12 +106,22 @@ module Shale
89
106
  end
90
107
  end
91
108
 
109
+ grouping.each do |group|
110
+ mapper = new
111
+
112
+ if mapper.method(group.method_from).arity == 3
113
+ mapper.send(group.method_from, instance, group.dict, context)
114
+ else
115
+ mapper.send(group.method_from, instance, group.dict)
116
+ end
117
+ end
118
+
92
119
  instance
93
120
  end
94
121
 
95
122
  # Convert Object to Hash using Hash/JSON/YAML/TOML mapping
96
123
  #
97
- # @param [any] instance Object to convert
124
+ # @param [any, Array<any>] instance Object to convert
98
125
  # @param [Array<Symbol>] only
99
126
  # @param [Array<Symbol>] except
100
127
  # @param [any] context
@@ -105,18 +132,33 @@ module Shale
105
132
  #
106
133
  # @api public
107
134
  def as_#{format}(instance, only: nil, except: nil, context: nil)
135
+ #{
136
+ if format != :toml
137
+ <<~CODE
138
+ if instance.is_a?(Array)
139
+ return instance.map do |item|
140
+ as_#{format}(item, only: only, except: except, context: context)
141
+ end
142
+ end
143
+ CODE
144
+ end
145
+ }
146
+
108
147
  unless instance.is_a?(model)
109
148
  msg = "argument is a '\#{instance.class}' but should be a '\#{model}'"
110
149
  raise IncorrectModelError, msg
111
150
  end
112
151
 
113
152
  hash = {}
153
+ grouping = Shale::Mapping::Group::DictGrouping.new
114
154
 
115
155
  only = to_partial_render_attributes(only)
116
156
  except = to_partial_render_attributes(except)
117
157
 
118
158
  #{format}_mapping.keys.each_value do |mapping|
119
- if mapping.method_to
159
+ if mapping.group
160
+ grouping.add(mapping, nil)
161
+ elsif mapping.method_to
120
162
  mapper = new
121
163
 
122
164
  if mapper.method(mapping.method_to).arity == 3
@@ -166,6 +208,16 @@ module Shale
166
208
  end
167
209
  end
168
210
 
211
+ grouping.each do |group|
212
+ mapper = new
213
+
214
+ if mapper.method(group.method_to).arity == 3
215
+ mapper.send(group.method_to, instance, hash, context)
216
+ else
217
+ mapper.send(group.method_to, instance, hash)
218
+ end
219
+ end
220
+
169
221
  hash
170
222
  end
171
223
  RUBY
@@ -284,6 +336,65 @@ module Shale
284
336
  )
285
337
  end
286
338
 
339
+ # Convert CSV to Object
340
+ #
341
+ # @param [String] csv CSV to convert
342
+ # @param [Array<Symbol>] only
343
+ # @param [Array<Symbol>] except
344
+ # @param [any] context
345
+ # @param [true, false] headers
346
+ # @param [Hash] csv_options
347
+ #
348
+ # @return [model instance]
349
+ #
350
+ # @api public
351
+ def from_csv(csv, only: nil, except: nil, context: nil, headers: false, **csv_options)
352
+ data = Shale.csv_adapter.load(csv, **csv_options.merge(headers: csv_mapping.keys.keys))
353
+
354
+ data.shift if headers
355
+
356
+ of_csv(
357
+ data,
358
+ only: only,
359
+ except: except,
360
+ context: context
361
+ )
362
+ end
363
+
364
+ # Convert Object to CSV
365
+ #
366
+ # @param [model instance] instance Object to convert
367
+ # @param [Array<Symbol>] only
368
+ # @param [Array<Symbol>] except
369
+ # @param [any] context
370
+ # @param [true, false] headers
371
+ # @param [Hash] csv_options
372
+ #
373
+ # @return [String]
374
+ #
375
+ # @api public
376
+ def to_csv(instance, only: nil, except: nil, context: nil, headers: false, **csv_options)
377
+ data = as_csv([*instance], only: only, except: except, context: context)
378
+
379
+ cols = csv_mapping.keys.values
380
+
381
+ if only
382
+ cols = cols.select { |e| only.include?(e.attribute) }
383
+ end
384
+
385
+ if except
386
+ cols = cols.reject { |e| except.include?(e.attribute) }
387
+ end
388
+
389
+ cols = cols.map(&:name)
390
+
391
+ if headers
392
+ data.prepend(cols.to_h { |e| [e, e] })
393
+ end
394
+
395
+ Shale.csv_adapter.dump(data, **csv_options.merge(headers: cols))
396
+ end
397
+
287
398
  # Convert XML document to Object
288
399
  #
289
400
  # @param [Shale::Adapter::<XML adapter>::Node] element
@@ -302,6 +413,8 @@ module Shale
302
413
  .select { |attr| attr.default }
303
414
  .each { |attr| instance.send(attr.setter, attr.default.call) }
304
415
 
416
+ grouping = Shale::Mapping::Group::XmlGrouping.new
417
+
305
418
  only = to_partial_render_attributes(only)
306
419
  except = to_partial_render_attributes(except)
307
420
 
@@ -309,7 +422,9 @@ module Shale
309
422
  mapping = xml_mapping.attributes[key.to_s]
310
423
  next unless mapping
311
424
 
312
- if mapping.method_from
425
+ if mapping.group
426
+ grouping.add(mapping, :attribute, value)
427
+ elsif mapping.method_from
313
428
  mapper = new
314
429
 
315
430
  if mapper.method(mapping.method_from).arity == 3
@@ -335,7 +450,9 @@ module Shale
335
450
  content_mapping = xml_mapping.content
336
451
 
337
452
  if content_mapping
338
- if content_mapping.method_from
453
+ if content_mapping.group
454
+ grouping.add(content_mapping, :content, element)
455
+ elsif content_mapping.method_from
339
456
  mapper = new
340
457
 
341
458
  if mapper.method(content_mapping.method_from).arity == 3
@@ -366,7 +483,9 @@ module Shale
366
483
  mapping = xml_mapping.elements[node.name]
367
484
  next unless mapping
368
485
 
369
- if mapping.method_from
486
+ if mapping.group
487
+ grouping.add(mapping, :element, node)
488
+ elsif mapping.method_from
370
489
  mapper = new
371
490
 
372
491
  if mapper.method(mapping.method_from).arity == 3
@@ -403,6 +522,16 @@ module Shale
403
522
  end
404
523
  end
405
524
 
525
+ grouping.each do |group|
526
+ mapper = new
527
+
528
+ if mapper.method(group.method_from).arity == 3
529
+ mapper.send(group.method_from, instance, group.dict, context)
530
+ else
531
+ mapper.send(group.method_from, instance, group.dict)
532
+ end
533
+ end
534
+
406
535
  instance
407
536
  end
408
537
 
@@ -449,7 +578,8 @@ module Shale
449
578
  _cdata = nil,
450
579
  only: nil,
451
580
  except: nil,
452
- context: nil
581
+ context: nil,
582
+ version: nil
453
583
  )
454
584
  unless instance.is_a?(model)
455
585
  msg = "argument is a '#{instance.class}' but should be a '#{model}'"
@@ -457,7 +587,7 @@ module Shale
457
587
  end
458
588
 
459
589
  unless doc
460
- doc = Shale.xml_adapter.create_document
590
+ doc = Shale.xml_adapter.create_document(version)
461
591
 
462
592
  element = as_xml(
463
593
  instance,
@@ -479,11 +609,15 @@ module Shale
479
609
  xml_mapping.default_namespace.name
480
610
  )
481
611
 
612
+ grouping = Shale::Mapping::Group::XmlGrouping.new
613
+
482
614
  only = to_partial_render_attributes(only)
483
615
  except = to_partial_render_attributes(except)
484
616
 
485
617
  xml_mapping.attributes.each_value do |mapping|
486
- if mapping.method_to
618
+ if mapping.group
619
+ grouping.add(mapping, :attribute, nil)
620
+ elsif mapping.method_to
487
621
  mapper = new
488
622
 
489
623
  if mapper.method(mapping.method_to).arity == 4
@@ -510,7 +644,9 @@ module Shale
510
644
  content_mapping = xml_mapping.content
511
645
 
512
646
  if content_mapping
513
- if content_mapping.method_to
647
+ if content_mapping.group
648
+ grouping.add(content_mapping, :content, nil)
649
+ elsif content_mapping.method_to
514
650
  mapper = new
515
651
 
516
652
  if mapper.method(content_mapping.method_to).arity == 4
@@ -543,7 +679,9 @@ module Shale
543
679
  end
544
680
 
545
681
  xml_mapping.elements.each_value do |mapping|
546
- if mapping.method_to
682
+ if mapping.group
683
+ grouping.add(mapping, :element, nil)
684
+ elsif mapping.method_to
547
685
  mapper = new
548
686
 
549
687
  if mapper.method(mapping.method_to).arity == 4
@@ -605,6 +743,16 @@ module Shale
605
743
  end
606
744
  end
607
745
 
746
+ grouping.each do |group|
747
+ mapper = new
748
+
749
+ if mapper.method(group.method_to).arity == 4
750
+ mapper.send(group.method_to, instance, element, doc, context)
751
+ else
752
+ mapper.send(group.method_to, instance, element, doc)
753
+ end
754
+ end
755
+
608
756
  element
609
757
  end
610
758
 
@@ -616,6 +764,7 @@ module Shale
616
764
  # @param [any] context
617
765
  # @param [true, false] pretty
618
766
  # @param [true, false] declaration
767
+ # @param [true, false, String] encoding
619
768
  #
620
769
  # @raise [AdapterError]
621
770
  #
@@ -628,13 +777,15 @@ module Shale
628
777
  except: nil,
629
778
  context: nil,
630
779
  pretty: false,
631
- declaration: false
780
+ declaration: false,
781
+ encoding: false
632
782
  )
633
783
  validate_xml_adapter
634
784
  Shale.xml_adapter.dump(
635
- as_xml(instance, only: only, except: except, context: context),
785
+ as_xml(instance, only: only, except: except, context: context, version: declaration),
636
786
  pretty: pretty,
637
- declaration: declaration
787
+ declaration: declaration,
788
+ encoding: encoding
638
789
  )
639
790
  end
640
791
 
@@ -737,6 +888,26 @@ module Shale
737
888
  self.class.to_toml(self, only: only, except: except, context: context)
738
889
  end
739
890
 
891
+ # Convert Object to CSV
892
+ #
893
+ # @param [Array<Symbol>] only
894
+ # @param [Array<Symbol>] except
895
+ # @param [any] context
896
+ #
897
+ # @return [String]
898
+ #
899
+ # @api public
900
+ def to_csv(only: nil, except: nil, context: nil, headers: false, **csv_options)
901
+ self.class.to_csv(
902
+ self,
903
+ only: only,
904
+ except: except,
905
+ context: context,
906
+ headers: headers,
907
+ **csv_options
908
+ )
909
+ end
910
+
740
911
  # Convert Object to XML
741
912
  #
742
913
  # @param [Array<Symbol>] only
@@ -744,18 +915,27 @@ module Shale
744
915
  # @param [any] context
745
916
  # @param [true, false] pretty
746
917
  # @param [true, false] declaration
918
+ # @param [true, false, String] encoding
747
919
  #
748
920
  # @return [String]
749
921
  #
750
922
  # @api public
751
- def to_xml(only: nil, except: nil, context: nil, pretty: false, declaration: false)
923
+ def to_xml(
924
+ only: nil,
925
+ except: nil,
926
+ context: nil,
927
+ pretty: false,
928
+ declaration: false,
929
+ encoding: false
930
+ )
752
931
  self.class.to_xml(
753
932
  self,
754
933
  only: only,
755
934
  except: except,
756
935
  context: context,
757
936
  pretty: pretty,
758
- declaration: declaration
937
+ declaration: declaration,
938
+ encoding: encoding
759
939
  )
760
940
  end
761
941
  end