shale 0.9.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../error'
4
+ require_relative '../mapping/delegates'
4
5
  require_relative '../mapping/group/dict_grouping'
5
6
  require_relative '../mapping/group/xml_grouping'
6
7
  require_relative 'value'
@@ -47,6 +48,7 @@ module Shale
47
48
 
48
49
  mapping_keys = #{format}_mapping.keys
49
50
  grouping = Shale::Mapping::Group::DictGrouping.new
51
+ delegates = Shale::Mapping::Delegates.new
50
52
 
51
53
  only = to_partial_render_attributes(only)
52
54
  except = to_partial_render_attributes(except)
@@ -66,7 +68,8 @@ module Shale
66
68
  mapper.send(mapping.method_from, instance, value)
67
69
  end
68
70
  else
69
- attribute = attributes[mapping.attribute]
71
+ receiver_attributes = get_receiver_attributes(mapping)
72
+ attribute = receiver_attributes[mapping.attribute]
70
73
  next unless attribute
71
74
 
72
75
  if only
@@ -79,20 +82,22 @@ module Shale
79
82
  next if except.key?(attribute.name) && attribute_except.nil?
80
83
  end
81
84
 
85
+ casted_value = nil
86
+
82
87
  if value.nil?
83
- instance.send(attribute.setter, nil)
88
+ casted_value = nil
84
89
  elsif attribute.collection?
85
- [*value].each do |val|
86
- if val
90
+ casted_value = (value.is_a?(Array) ? value : [value]).map do |val|
91
+ unless val.nil?
87
92
  val = attribute.type.of_#{format}(
88
93
  val,
89
94
  only: attribute_only,
90
95
  except: attribute_except,
91
96
  context: context
92
97
  )
93
- end
94
98
 
95
- instance.send(attribute.name) << attribute.type.cast(val)
99
+ attribute.type.cast(val)
100
+ end
96
101
  end
97
102
  else
98
103
  val = attribute.type.of_#{format}(
@@ -101,11 +106,23 @@ module Shale
101
106
  except: attribute_except,
102
107
  context: context
103
108
  )
104
- instance.send(attribute.setter, attribute.type.cast(val))
109
+
110
+ casted_value = attribute.type.cast(val)
111
+ end
112
+
113
+ if mapping.receiver
114
+ delegates.add(attributes[mapping.receiver], attribute.setter, casted_value)
115
+ else
116
+ instance.send(attribute.setter, casted_value)
105
117
  end
106
118
  end
107
119
  end
108
120
 
121
+ delegates.each do |delegate|
122
+ receiver = get_receiver(instance, delegate.receiver_attribute)
123
+ receiver.send(delegate.setter, delegate.value)
124
+ end
125
+
109
126
  grouping.each do |group|
110
127
  mapper = new
111
128
 
@@ -167,7 +184,14 @@ module Shale
167
184
  mapper.send(mapping.method_to, instance, hash)
168
185
  end
169
186
  else
170
- attribute = attributes[mapping.attribute]
187
+ if mapping.receiver
188
+ receiver = instance.send(mapping.receiver)
189
+ else
190
+ receiver = instance
191
+ end
192
+
193
+ receiver_attributes = get_receiver_attributes(mapping)
194
+ attribute = receiver_attributes[mapping.attribute]
171
195
  next unless attribute
172
196
 
173
197
  if only
@@ -180,7 +204,7 @@ module Shale
180
204
  next if except.key?(attribute.name) && attribute_except.nil?
181
205
  end
182
206
 
183
- value = instance.send(attribute.name)
207
+ value = receiver.send(attribute.name) if receiver
184
208
 
185
209
  if value.nil?
186
210
  hash[mapping.name] = nil if mapping.render_nil?
@@ -349,6 +373,7 @@ module Shale
349
373
  #
350
374
  # @api public
351
375
  def from_csv(csv, only: nil, except: nil, context: nil, headers: false, **csv_options)
376
+ validate_csv_adapter
352
377
  data = Shale.csv_adapter.load(csv, **csv_options.merge(headers: csv_mapping.keys.keys))
353
378
 
354
379
  data.shift if headers
@@ -374,6 +399,7 @@ module Shale
374
399
  #
375
400
  # @api public
376
401
  def to_csv(instance, only: nil, except: nil, context: nil, headers: false, **csv_options)
402
+ validate_csv_adapter
377
403
  data = as_csv([*instance], only: only, except: except, context: context)
378
404
 
379
405
  cols = csv_mapping.keys.values
@@ -414,6 +440,7 @@ module Shale
414
440
  .each { |attr| instance.send(attr.setter, attr.default.call) }
415
441
 
416
442
  grouping = Shale::Mapping::Group::XmlGrouping.new
443
+ delegates = Shale::Mapping::Delegates.new
417
444
 
418
445
  only = to_partial_render_attributes(only)
419
446
  except = to_partial_render_attributes(except)
@@ -433,16 +460,29 @@ module Shale
433
460
  mapper.send(mapping.method_from, instance, value)
434
461
  end
435
462
  else
436
- attribute = attributes[mapping.attribute]
463
+ receiver_attributes = get_receiver_attributes(mapping)
464
+ attribute = receiver_attributes[mapping.attribute]
437
465
  next unless attribute
438
466
 
439
467
  next if only && !only.key?(attribute.name)
440
468
  next if except&.key?(attribute.name)
441
469
 
470
+ casted_value = attribute.type.cast(value)
471
+
442
472
  if attribute.collection?
443
- instance.send(attribute.name) << attribute.type.cast(value)
473
+ if mapping.receiver
474
+ delegates.add_collection(
475
+ attributes[mapping.receiver],
476
+ attribute.setter,
477
+ casted_value
478
+ )
479
+ else
480
+ instance.send(attribute.name) << casted_value
481
+ end
482
+ elsif mapping.receiver
483
+ delegates.add(attributes[mapping.receiver], attribute.setter, casted_value)
444
484
  else
445
- instance.send(attribute.setter, attribute.type.cast(value))
485
+ instance.send(attribute.setter, casted_value)
446
486
  end
447
487
  end
448
488
  end
@@ -461,7 +501,8 @@ module Shale
461
501
  mapper.send(content_mapping.method_from, instance, element)
462
502
  end
463
503
  else
464
- attribute = attributes[content_mapping.attribute]
504
+ receiver_attributes = get_receiver_attributes(content_mapping)
505
+ attribute = receiver_attributes[content_mapping.attribute]
465
506
 
466
507
  if attribute
467
508
  skip = false
@@ -471,8 +512,13 @@ module Shale
471
512
  skip = true if except&.key?(attribute.name)
472
513
 
473
514
  unless skip
474
- value = attribute.type.of_xml(element)
475
- instance.send(attribute.setter, attribute.type.cast(value))
515
+ value = attribute.type.cast(attribute.type.of_xml(element))
516
+
517
+ if content_mapping.receiver
518
+ delegates.add(attributes[content_mapping.receiver], attribute.setter, value)
519
+ else
520
+ instance.send(attribute.setter, value)
521
+ end
476
522
  end
477
523
  # rubocop:enable Metrics/BlockNesting
478
524
  end
@@ -494,7 +540,8 @@ module Shale
494
540
  mapper.send(mapping.method_from, instance, node)
495
541
  end
496
542
  else
497
- attribute = attributes[mapping.attribute]
543
+ receiver_attributes = get_receiver_attributes(mapping)
544
+ attribute = receiver_attributes[mapping.attribute]
498
545
  next unless attribute
499
546
 
500
547
  if only
@@ -514,14 +561,31 @@ module Shale
514
561
  context: context
515
562
  )
516
563
 
564
+ casted_value = attribute.type.cast(value)
565
+
517
566
  if attribute.collection?
518
- instance.send(attribute.name) << attribute.type.cast(value)
567
+ if mapping.receiver
568
+ delegates.add_collection(
569
+ attributes[mapping.receiver],
570
+ attribute.setter,
571
+ casted_value
572
+ )
573
+ else
574
+ instance.send(attribute.name) << casted_value
575
+ end
576
+ elsif mapping.receiver
577
+ delegates.add(attributes[mapping.receiver], attribute.setter, casted_value)
519
578
  else
520
- instance.send(attribute.setter, attribute.type.cast(value))
579
+ instance.send(attribute.setter, casted_value)
521
580
  end
522
581
  end
523
582
  end
524
583
 
584
+ delegates.each do |delegate|
585
+ receiver = get_receiver(instance, delegate.receiver_attribute)
586
+ receiver.send(delegate.setter, delegate.value)
587
+ end
588
+
525
589
  grouping.each do |group|
526
590
  mapper = new
527
591
 
@@ -626,13 +690,20 @@ module Shale
626
690
  mapper.send(mapping.method_to, instance, element, doc)
627
691
  end
628
692
  else
629
- attribute = attributes[mapping.attribute]
693
+ if mapping.receiver
694
+ receiver = instance.send(mapping.receiver)
695
+ else
696
+ receiver = instance
697
+ end
698
+
699
+ receiver_attributes = get_receiver_attributes(mapping)
700
+ attribute = receiver_attributes[mapping.attribute]
630
701
  next unless attribute
631
702
 
632
703
  next if only && !only.key?(attribute.name)
633
704
  next if except&.key?(attribute.name)
634
705
 
635
- value = instance.send(attribute.name)
706
+ value = receiver.send(attribute.name) if receiver
636
707
 
637
708
  if mapping.render_nil? || !value.nil?
638
709
  doc.add_namespace(mapping.namespace.prefix, mapping.namespace.name)
@@ -655,7 +726,14 @@ module Shale
655
726
  mapper.send(content_mapping.method_to, instance, element, doc)
656
727
  end
657
728
  else
658
- attribute = attributes[content_mapping.attribute]
729
+ if content_mapping.receiver
730
+ receiver = instance.send(content_mapping.receiver)
731
+ else
732
+ receiver = instance
733
+ end
734
+
735
+ receiver_attributes = get_receiver_attributes(content_mapping)
736
+ attribute = receiver_attributes[content_mapping.attribute]
659
737
 
660
738
  if attribute
661
739
  skip = false
@@ -665,7 +743,7 @@ module Shale
665
743
  skip = true if except&.key?(attribute.name)
666
744
 
667
745
  unless skip
668
- value = instance.send(attribute.name)
746
+ value = receiver.send(attribute.name) if receiver
669
747
 
670
748
  if content_mapping.cdata
671
749
  doc.create_cdata(value.to_s, element)
@@ -690,7 +768,14 @@ module Shale
690
768
  mapper.send(mapping.method_to, instance, element, doc)
691
769
  end
692
770
  else
693
- attribute = attributes[mapping.attribute]
771
+ if mapping.receiver
772
+ receiver = instance.send(mapping.receiver)
773
+ else
774
+ receiver = instance
775
+ end
776
+
777
+ receiver_attributes = get_receiver_attributes(mapping)
778
+ attribute = receiver_attributes[mapping.attribute]
694
779
  next unless attribute
695
780
 
696
781
  if only
@@ -703,7 +788,7 @@ module Shale
703
788
  next if except.key?(attribute.name) && attribute_except.nil?
704
789
  end
705
790
 
706
- value = instance.send(attribute.name)
791
+ value = receiver.send(attribute.name) if receiver
707
792
 
708
793
  if mapping.render_nil? || !value.nil?
709
794
  doc.add_namespace(mapping.namespace.prefix, mapping.namespace.name)
@@ -809,6 +894,15 @@ module Shale
809
894
  raise AdapterError, XML_ADAPTER_NOT_SET_MESSAGE unless Shale.xml_adapter
810
895
  end
811
896
 
897
+ # Validate CSV adapter
898
+ #
899
+ # @raise [AdapterError]
900
+ #
901
+ # @api private
902
+ def validate_csv_adapter
903
+ raise AdapterError, CSV_ADAPTER_NOT_SET_MESSAGE unless Shale.csv_adapter
904
+ end
905
+
812
906
  # Convert array with attributes to a hash
813
907
  #
814
908
  # @param [Array] ary
@@ -827,6 +921,64 @@ module Shale
827
921
  end
828
922
  end.to_h
829
923
  end
924
+
925
+ # Get receiver attributes for given mapping
926
+ #
927
+ # @param [Shale::Mapping::Descriptor::Dict] mapping
928
+ #
929
+ # @raise [AttributeNotDefinedError]
930
+ # @raise [NotAShaleMapperError]
931
+ #
932
+ # @return [Hash<Symbol, Shale::Attribute>]
933
+ #
934
+ # @api private
935
+ def get_receiver_attributes(mapping)
936
+ return attributes unless mapping.receiver
937
+
938
+ receiver_attribute = attributes[mapping.receiver]
939
+
940
+ unless receiver_attribute
941
+ msg = "attribute '#{mapping.receiver}' is not defined on #{self} mapper"
942
+ raise AttributeNotDefinedError, msg
943
+ end
944
+
945
+ unless receiver_attribute.type < Mapper
946
+ msg = "attribute '#{mapping.receiver}' is not a descendant of Shale::Mapper type"
947
+ raise NotAShaleMapperError, msg
948
+ end
949
+
950
+ if receiver_attribute.collection?
951
+ msg = "attribute '#{mapping.receiver}' can't be a collection"
952
+ raise NotAShaleMapperError, msg
953
+ end
954
+
955
+ receiver_attribute.type.attributes
956
+ end
957
+
958
+ # Get receiver for given mapping
959
+ #
960
+ # @param [any] instance
961
+ # @param [Shale::Attribute] receiver_attribute
962
+ #
963
+ # @return [Array]
964
+ #
965
+ # @api private
966
+ def get_receiver(instance, receiver_attribute)
967
+ receiver = instance.send(receiver_attribute.name)
968
+
969
+ unless receiver
970
+ receiver = receiver_attribute.type.model.new
971
+
972
+ receiver_attribute.type.attributes
973
+ .values
974
+ .select { |attr| attr.default }
975
+ .each { |attr| receiver.send(attr.setter, attr.default.call) }
976
+
977
+ instance.send(receiver_attribute.setter, receiver)
978
+ end
979
+
980
+ receiver
981
+ end
830
982
  end
831
983
 
832
984
  # Convert Object to Hash
data/lib/shale/utils.rb CHANGED
@@ -5,6 +5,21 @@ module Shale
5
5
  #
6
6
  # @api private
7
7
  module Utils
8
+ # Upcase first letter of a string
9
+ #
10
+ # @param [String] val
11
+ #
12
+ # @example
13
+ # Shale::Utils.upcase_first('fooBar')
14
+ # # => 'FooBar'
15
+ #
16
+ # @api private
17
+ def self.upcase_first(str)
18
+ return '' unless str
19
+ return '' if str.empty?
20
+ str[0].upcase + str[1..-1]
21
+ end
22
+
8
23
  # Convert string to Ruby's class naming convention
9
24
  #
10
25
  # @param [String] val
@@ -15,13 +30,14 @@ module Shale
15
30
  #
16
31
  # @api private
17
32
  def self.classify(str)
18
- str = str.to_s.sub(/.*\./, '')
33
+ # names may include a period, which will need to be stripped out
34
+ str = str.to_s.gsub('.', '')
19
35
 
20
- str = str.sub(/^[a-z\d]*/) { |match| match.capitalize || match }
36
+ str = str.sub(/^[a-z\d]*/) { |match| upcase_first(match) || match }
21
37
 
22
- str.gsub(%r{(?:_|-|(/))([a-z\d]*)}i) do
38
+ str.gsub('::', '/').gsub(%r{(?:_|-|(/))([a-z\d]*)}i) do
23
39
  word = Regexp.last_match(2)
24
- substituted = word.capitalize || word
40
+ substituted = upcase_first(word) || word
25
41
  Regexp.last_match(1) ? "::#{substituted}" : substituted
26
42
  end
27
43
  end
@@ -36,6 +52,8 @@ module Shale
36
52
  #
37
53
  # @api private
38
54
  def self.snake_case(str)
55
+ # XML elements allow periods and hyphens
56
+ str = str.to_s.gsub('.', '_')
39
57
  return str.to_s unless /[A-Z-]|::/.match?(str)
40
58
  word = str.to_s.gsub('::', '/')
41
59
  word = word.gsub(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do
data/lib/shale/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Shale
4
4
  # @api private
5
- VERSION = '0.9.0'
5
+ VERSION = '1.1.0'
6
6
  end
data/lib/shale.rb CHANGED
@@ -3,7 +3,6 @@
3
3
  require 'yaml'
4
4
 
5
5
  require_relative 'shale/mapper'
6
- require_relative 'shale/adapter/csv'
7
6
  require_relative 'shale/adapter/json'
8
7
  require_relative 'shale/type/boolean'
9
8
  require_relative 'shale/type/date'
@@ -52,6 +51,12 @@ require_relative 'shale/version'
52
51
  # Shale.xml_adapter = Shale::Adapter::Ox
53
52
  # Shale.xml_adapter # => Shale::Adapter::Ox
54
53
  #
54
+ # @example setting CSV adapter for handling CSV documents
55
+ # require 'shale/adapter/csv'
56
+ #
57
+ # Shale.csv_adapter = Shale::Adapter::CSV
58
+ # Shale.csv_adapter # => Shale::Adapter::CSV
59
+ #
55
60
  # @api public
56
61
  module Shale
57
62
  class << self
@@ -93,11 +98,15 @@ module Shale
93
98
  #
94
99
  # @param [.load, .dump] adapter
95
100
  #
96
- # @example
101
+ # @example setting adapter
97
102
  # Shale.csv_adapter = Shale::Adapter::CSV
98
103
  #
104
+ # @example getting adapter
105
+ # Shale.csv_adapter
106
+ # # => Shale::Adapter::CSV
107
+ #
99
108
  # @api public
100
- attr_writer :csv_adapter
109
+ attr_accessor :csv_adapter
101
110
 
102
111
  # XML adapter accessor. Available adapters are Shale::Adapter::REXML,
103
112
  # Shale::Adapter::Nokogiri and Shale::Adapter::Ox
@@ -139,18 +148,5 @@ module Shale
139
148
  def yaml_adapter
140
149
  @yaml_adapter || YAML
141
150
  end
142
-
143
- # Return CSV adapter. By default CSV is used
144
- #
145
- # @return [.load, .dump]
146
- #
147
- # @example
148
- # Shale.csv_adapter
149
- # # => Shale::Adapter::CSV
150
- #
151
- # @api public
152
- def csv_adapter
153
- @csv_adapter || Adapter::CSV
154
- end
155
151
  end
156
152
  end
data/shale.gemspec CHANGED
@@ -26,5 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.bindir = 'exe'
27
27
  spec.executables = 'shaleb'
28
28
 
29
- spec.required_ruby_version = '>= 2.6.0'
29
+ spec.add_runtime_dependency 'bigdecimal'
30
+
31
+ spec.required_ruby_version = '>= 3.0.0'
30
32
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shale
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kamil Giszczak
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-31 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-02-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bigdecimal
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: Ruby object mapper and serializer for XML, JSON, TOML, CSV and YAML.
14
28
  email:
15
29
  - beerkg@gmail.com
@@ -38,6 +52,7 @@ files:
38
52
  - lib/shale/attribute.rb
39
53
  - lib/shale/error.rb
40
54
  - lib/shale/mapper.rb
55
+ - lib/shale/mapping/delegates.rb
41
56
  - lib/shale/mapping/descriptor/dict.rb
42
57
  - lib/shale/mapping/descriptor/xml.rb
43
58
  - lib/shale/mapping/descriptor/xml_namespace.rb
@@ -117,14 +132,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
117
132
  requirements:
118
133
  - - ">="
119
134
  - !ruby/object:Gem::Version
120
- version: 2.6.0
135
+ version: 3.0.0
121
136
  required_rubygems_version: !ruby/object:Gem::Requirement
122
137
  requirements:
123
138
  - - ">="
124
139
  - !ruby/object:Gem::Version
125
140
  version: '0'
126
141
  requirements: []
127
- rubygems_version: 3.3.7
142
+ rubygems_version: 3.5.3
128
143
  signing_key:
129
144
  specification_version: 4
130
145
  summary: Ruby object mapper and serializer for XML, JSON, TOML, CSV and YAML.