scimitar 1.8.1 → 1.8.2

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.
@@ -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 = {