scimitar 1.8.1 → 1.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -23,8 +23,8 @@ class Hash
23
23
  #
24
24
  def self.deep_indifferent_case_insensitive_access(object)
25
25
  if object.is_a?(Hash)
26
- new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new(object)
27
- new_hash.each do | key, value |
26
+ new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
27
+ object.each do | key, value |
28
28
  new_hash[key] = deep_indifferent_case_insensitive_access(value)
29
29
  end
30
30
  new_hash
@@ -49,34 +49,164 @@ module Scimitar
49
49
  # in a case-insensitive fashion too.
50
50
  #
51
51
  # During enumeration, Hash keys will always be returned in whatever case
52
- # they were originally set.
52
+ # they were originally set. Just as with
53
+ # ActiveSupport::HashWithIndifferentAccess, though, the type of the keys is
54
+ # always returned as a String, even if originally set as a Symbol - only
55
+ # the upper/lower case nature of the original key is preserved.
56
+ #
57
+ # If a key is written more than once with the same effective meaning in a
58
+ # to-string, to-downcase form, then whatever case was used *first* wins;
59
+ # e.g. if you did hash['User'] = 23, then hash['USER'] = 42, the result
60
+ # would be {"User" => 42}.
61
+ #
62
+ # It's important to remember that Hash#merge is shallow and replaces values
63
+ # found at existing keys in the target ("this") hash with values in the
64
+ # inbound Hash. If that new value that is itself a Hash, this *replaces*
65
+ # the value. For example:
66
+ #
67
+ # * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
68
+ # * Merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
69
+ #
70
+ # ...results in "this" target hash's key +Foo+ being addressed in the merge
71
+ # by inbound key +FOO+, so the case doesn't change. But the value for +Foo+
72
+ # is _replaced_ by the merging-in Hash completely:
73
+ #
74
+ # * Result: <tt>'Foo' => { 'BAR' => 24 }</tt>
75
+ #
76
+ # ...and of course we might've replaced with a totally different type, such
77
+ # as +true+:
78
+ #
79
+ # * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
80
+ # * Merge: <tt>'FOO' => true</tt>
81
+ # * Result: <tt>'Foo' => true</tt>
82
+ #
83
+ # If you're intending to merge nested Hashes, then use ActiveSupport's
84
+ # #deep_merge or an equivalent. This will have the expected outcome, where
85
+ # the hash with 'BAR' is _merged_ into the existing value and, therefore,
86
+ # the original 'Bar' key case is preserved:
87
+ #
88
+ # * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
89
+ # * Deep merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
90
+ # * Result: <tt>'Foo' => { 'Bar' => 24 }</tt>
53
91
  #
54
92
  class HashWithIndifferentCaseInsensitiveAccess < ActiveSupport::HashWithIndifferentAccess
55
93
  def with_indifferent_case_insensitive_access
56
94
  self
57
95
  end
58
96
 
97
+ def initialize(constructor = nil)
98
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map = {}
99
+ super
100
+ end
101
+
102
+ # It's vital that the attribute map is carried over when one of these
103
+ # objects is duplicated. Duplication of this ivar state does *not* happen
104
+ # when 'dup' is called on our superclass, so we have to do that manually.
105
+ #
106
+ def dup
107
+ duplicate = super
108
+ duplicate.instance_variable_set(
109
+ '@scimitar_hash_with_indifferent_case_insensitive_access_key_map',
110
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map
111
+ )
112
+
113
+ return duplicate
114
+ end
115
+
116
+ # Override the individual key writer.
117
+ #
118
+ def []=(key, value)
119
+ string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
120
+ indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
121
+ converted_value = convert_value(value, conversion: :assignment)
122
+
123
+ # Note '||=', as there might have been a prior use of the "same" key in
124
+ # a different case. The earliest one is preserved since the actual Hash
125
+ # underneath all this is already using that variant of the key.
126
+ #
127
+ key_for_writing = (
128
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] ||= string_key
129
+ )
130
+
131
+ regular_writer(key_for_writing, converted_value)
132
+ end
133
+
134
+ # Override #merge to express it in terms of #merge! (also overridden), so
135
+ # that merged hashes can have their keys treated indifferently too.
136
+ #
137
+ def merge(*other_hashes, &block)
138
+ dup.merge!(*other_hashes, &block)
139
+ end
140
+
141
+ # Modifies-self version of #merge, overriding Hash#merge!.
142
+ #
143
+ def merge!(*hashes_to_merge_to_self, &block)
144
+ if block_given?
145
+ hashes_to_merge_to_self.each do |hash_to_merge_to_self|
146
+ hash_to_merge_to_self.each_pair do |key, value|
147
+ value = block.call(key, self[key], value) if self.key?(key)
148
+ self[key] = value
149
+ end
150
+ end
151
+ else
152
+ hashes_to_merge_to_self.each do |hash_to_merge_to_self|
153
+ hash_to_merge_to_self.each_pair do |key, value|
154
+ self[key] = value
155
+ end
156
+ end
157
+ end
158
+
159
+ self
160
+ end
161
+
162
+ # =======================================================================
163
+ # PRIVATE INSTANCE METHODS
164
+ # =======================================================================
165
+ #
59
166
  private
60
167
 
61
168
  if Symbol.method_defined?(:name)
62
- def convert_key(key)
63
- key.kind_of?(Symbol) ? key.name.downcase : key.downcase
169
+ def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
170
+ key.kind_of?(Symbol) ? key.name : key
64
171
  end
65
172
  else
66
- def convert_key(key)
67
- key.kind_of?(Symbol) ? key.to_s.downcase : key.downcase
173
+ def scimitar_hash_with_indifferent_case_insensitive_access_string(key)
174
+ key.kind_of?(Symbol) ? key.to_s : key
175
+ end
176
+ end
177
+
178
+ def scimitar_hash_with_indifferent_case_insensitive_access_downcase(key)
179
+ key.kind_of?(String) ? key.downcase : key
180
+ end
181
+
182
+ def convert_key(key)
183
+ string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
184
+ indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
185
+
186
+ @scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] || string_key
187
+ end
188
+
189
+ def convert_value(value, conversion: nil)
190
+ if value.is_a?(Hash)
191
+ if conversion == :to_hash
192
+ value.to_hash
193
+ else
194
+ value.with_indifferent_case_insensitive_access
195
+ end
196
+ else
197
+ super
68
198
  end
69
199
  end
70
200
 
71
201
  def update_with_single_argument(other_hash, block)
72
- if other_hash.is_a? HashWithIndifferentCaseInsensitiveAccess
202
+ if other_hash.is_a?(HashWithIndifferentCaseInsensitiveAccess)
73
203
  regular_update(other_hash, &block)
74
204
  else
75
205
  other_hash.to_hash.each_pair do |key, value|
76
206
  if block && key?(key)
77
- value = block.call(convert_key(key), self[key], value)
207
+ value = block.call(self.convert_key(key), self[key], value)
78
208
  end
79
- regular_writer(convert_key(key), convert_value(value))
209
+ self.[]=(key, value)
80
210
  end
81
211
  end
82
212
  end
@@ -3,11 +3,11 @@ module Scimitar
3
3
  # Gem version. If this changes, be sure to re-run "bundle install" or
4
4
  # "bundle update".
5
5
  #
6
- VERSION = '1.8.1'
6
+ VERSION = '1.8.2'
7
7
 
8
8
  # Date for VERSION. If this changes, be sure to re-run "bundle install"
9
9
  # or "bundle update".
10
10
  #
11
- DATE = '2024-01-16'
11
+ DATE = '2024-03-27'
12
12
 
13
13
  end
@@ -16,7 +16,7 @@ RSpec.describe Scimitar::Resources::Base do
16
16
  name: 'names', multiValued: true, complexType: Scimitar::ComplexTypes::Name, required: false
17
17
  ),
18
18
  Scimitar::Schema::Attribute.new(
19
- name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: false
19
+ name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: 'never'
20
20
  ),
21
21
  ]
22
22
  end
@@ -27,6 +27,14 @@ RSpec.describe Scimitar::Resources::Base do
27
27
  end
28
28
 
29
29
  context '#initialize' do
30
+ it 'accepts nil for non-required attributes' do
31
+ resource = CustomResourse.new(name: nil, names: nil, privateName: nil)
32
+
33
+ expect(resource.name).to be_nil
34
+ expect(resource.names).to be_nil
35
+ expect(resource.privateName).to be_nil
36
+ end
37
+
30
38
  shared_examples 'an initializer' do | force_upper_case: |
31
39
  it 'which builds the nested type' do
32
40
  attributes = {