shale 0.7.1 → 0.9.0

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