shale 0.9.0 → 1.1.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.
@@ -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.