scimitar 1.0.3 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f44cfce4b49e3f386b14081151dc38bf5bf857939fefbad36b507770d1de2e9
4
- data.tar.gz: ba7fe68d41eb204a84f71e23931ed85a497645671170994043abab4e1b7dba73
3
+ metadata.gz: 95a2166cc921a400959f9d8d4398f6bf8ecb772f8d7a0a0a73950892e85d808a
4
+ data.tar.gz: cdf5aab3812f10f69c96304e738a150f4208850267527b66d36eeb99548d7b1f
5
5
  SHA512:
6
- metadata.gz: 1b267f42755f9fc94280ea7c84d08762056e95dd967a6d17535cefd9275e04f60e3abc1a20a7849ab7f3bbd8fa508047586f23cb33ed6d7bec56bc6c2d3fcf7d
7
- data.tar.gz: facdf0cb0566d0b323181abb0d4707c2036f85b2f958f29dc0df0471a6244595c6147a7887895b7a96689288edea3c684cbf3d32c7119959ff70c28abc610d9e
6
+ metadata.gz: f0925517599b107e44fd93db9be142aebe608892c2c5069c50d22b353c51238290710474b062a002fdb010be3d783c4dea3b314f72f47b4aca3c2385a8fc1377
7
+ data.tar.gz: eef6eebfc64bb2d4adabfca110f26e3a6c9e227f47387da6fb384925899ddf0ae260cf68176f24040a6bb356cf34f6576c920bd44264d4a1fee415aeadc237e6
@@ -131,8 +131,8 @@ module Scimitar
131
131
  raise NotImplementedError
132
132
  end
133
133
 
134
- # Find a RIP user record. Subclasses can override this if they need
135
- # special lookup behaviour.
134
+ # Find a record by ID. Subclasses can override this if they need special
135
+ # lookup behaviour.
136
136
  #
137
137
  # +record_id+:: Record ID (SCIM schema 'id' value - "our" ID).
138
138
  #
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module Scimitar
2
4
  module ComplexTypes
3
5
 
@@ -22,8 +24,48 @@ module Scimitar
22
24
  include Scimitar::Schema::DerivedAttributes
23
25
  include Scimitar::Errors
24
26
 
27
+ # Instantiates with attribute values - see ActiveModel::Model#initialize.
28
+ #
29
+ # Allows case-insensitive attributes given in options, by enumerating all
30
+ # instance methods that exist in the subclass (at the point where this
31
+ # method runs, 'self' is a subclass, unless someone instantiated this
32
+ # base class directly) and subtracting methods in the base class. Usually
33
+ # this leaves just the attribute accessors, with not much extra.
34
+ #
35
+ # Map a normalized case version of those names to the actual method names
36
+ # then for each key in the inbound options, normalize it and see if one
37
+ # of the actual case method names is available. If so, use that instead.
38
+ #
39
+ # Unmapped option keys will most likely have no corresponding writer
40
+ # method in the subclass and NoMethodError will therefore arise.
41
+ #
25
42
  def initialize(options={})
26
- super
43
+ normalized_method_map = HashWithIndifferentAccess.new
44
+ corrected_options = {}
45
+ probable_accessor_methods = self.class.instance_methods - self.class.superclass.instance_methods
46
+
47
+ unless options.empty?
48
+ probable_accessor_methods.each do | method_name |
49
+ next if method_name.end_with?('=')
50
+ normalized_method_map[method_name.downcase] = method_name
51
+ end
52
+
53
+ options.each do | ambiguous_case_name, value |
54
+ normalized_name = ambiguous_case_name.downcase
55
+ corrected_name = normalized_method_map[normalized_name]
56
+
57
+ if corrected_name.nil?
58
+ corrected_options[ambiguous_case_name] = value # Probably will lead to NoMethodError
59
+ else
60
+ corrected_options[corrected_name] = value
61
+ end
62
+ end
63
+
64
+ options = corrected_options
65
+ end
66
+
67
+ super # Calls into ActiveModel::Model
68
+
27
69
  @errors = ActiveModel::Errors.new(self)
28
70
  end
29
71
 
@@ -1,6 +1,6 @@
1
1
  module Scimitar
2
2
  module Errors
3
- def add_errors_from_hash(errors_hash, prefix: nil)
3
+ def add_errors_from_hash(errors_hash:, prefix: nil)
4
4
  errors_hash.each_pair do |key, value|
5
5
  new_key = prefix.nil? ? key : "#{prefix}.#{key}".to_sym
6
6
  if value.is_a?(Array)
@@ -11,10 +11,27 @@ module Scimitar
11
11
  validate :validate_resource
12
12
 
13
13
  def initialize(options = {})
14
- flattended_attributes = flatten_extension_attributes(options)
15
- attributes = flattended_attributes.with_indifferent_access.slice(*self.class.all_attributes)
16
- super(attributes)
17
- constantize_complex_types(attributes)
14
+ flattened_attributes = flatten_extension_attributes(options)
15
+ ci_all_attributes = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
16
+ camel_attributes = {}
17
+
18
+ # Create a map where values are the schema-correct-case attribute names
19
+ # and the values are set the same, but since the ci_all_attributes data
20
+ # type is HashWithIndifferentCaseInsensitiveAccess, lookups in this are
21
+ # case insensitive. Thus, arbitrary case input data can be mapped to
22
+ # the case correctness required for ActiveModel's attribute accessors.
23
+ #
24
+ self.class.all_attributes.each { |attr| ci_all_attributes[attr] = attr }
25
+
26
+ flattened_attributes.each do | key, value |
27
+ if ci_all_attributes.key?(key)
28
+ camel_attributes[ci_all_attributes[key]] = value
29
+ end
30
+ end
31
+
32
+ super(camel_attributes)
33
+ constantize_complex_types(camel_attributes)
34
+
18
35
  @errors = ActiveModel::Errors.new(self)
19
36
  end
20
37
 
@@ -109,6 +126,7 @@ module Scimitar
109
126
  def constantize_complex_types(hash)
110
127
  hash.with_indifferent_access.each_pair do |attr_name, attr_value|
111
128
  scim_attribute = self.class.complex_scim_attributes[attr_name].try(:first)
129
+
112
130
  if scim_attribute && scim_attribute.complexType
113
131
  if scim_attribute.multiValued
114
132
  self.send("#{attr_name}=", attr_value.map {|attr_for_each_item| complex_type_from_hash(scim_attribute, attr_for_each_item)})
@@ -404,10 +404,10 @@ module Scimitar
404
404
  # Call ONLY for PATCH. For POST and PUT, see #from_scim!.
405
405
  #
406
406
  def from_scim_patch!(patch_hash:)
407
- patch_hash.freeze()
408
- scim_hash = self.to_scim(location: '(unused)').as_json()
407
+ frozen_ci_patch_hash = patch_hash.with_indifferent_case_insensitive_access().freeze()
408
+ ci_scim_hash = self.to_scim(location: '(unused)').as_json().with_indifferent_case_insensitive_access()
409
409
 
410
- patch_hash['Operations'].each do |operation|
410
+ frozen_ci_patch_hash['operations'].each do |operation|
411
411
  nature = operation['op' ]&.downcase
412
412
  path_str = operation['path' ]
413
413
  value = operation['value']
@@ -440,22 +440,22 @@ module Scimitar
440
440
  if path_str.blank?
441
441
  extract_root = true
442
442
  path_str = 'root'
443
- scim_hash = { 'root' => scim_hash }
443
+ ci_scim_hash = { 'root' => ci_scim_hash }.with_indifferent_case_insensitive_access()
444
444
  end
445
445
 
446
446
  self.from_patch_backend!(
447
447
  nature: nature,
448
448
  path: (path_str || '').split('.'),
449
449
  value: value,
450
- altering_hash: scim_hash
450
+ altering_hash: ci_scim_hash
451
451
  )
452
452
 
453
453
  if extract_root
454
- scim_hash = scim_hash['root']
454
+ ci_scim_hash = ci_scim_hash['root']
455
455
  end
456
456
  end
457
457
 
458
- self.from_scim!(scim_hash: scim_hash)
458
+ self.from_scim!(scim_hash: ci_scim_hash)
459
459
  return self
460
460
  end
461
461
 
@@ -542,20 +542,20 @@ module Scimitar
542
542
  # ::scim_attributes_map.
543
543
  #
544
544
  # { | {
545
- # "userName": "foo", | "id": "id",
546
- # "name": { | "externalId": :scim_uid",
547
- # "givenName": "Foo", | "userName": :username",
548
- # "familyName": "Bar" | "name": {
549
- # }, | "givenName": :first_name",
550
- # "active": true, | "familyName": :last_name"
545
+ # "userName": "foo", | 'id': :id,
546
+ # "name": { | 'externalId': :scim_uid,
547
+ # "givenName": "Foo", | 'userName': :username,
548
+ # "familyName": "Bar" | 'name': {
549
+ # }, | 'givenName': :first_name,
550
+ # "active": true, | 'familyName': :last_name
551
551
  # "emails": [ | },
552
- # { | "emails": [
552
+ # { | 'emails': [
553
553
  # "type": "work", <------\ | {
554
- # "primary": true, \------+--- "match": "type",
555
- # "value": "foo.bar@test.com" | "with": "work",
556
- # } | "using": {
557
- # ], | "value": :work_email_address",
558
- # "phoneNumbers": [ | "primary": true
554
+ # "primary": true, \------+--- 'match': 'type',
555
+ # "value": "foo.bar@test.com" | 'with': 'work',
556
+ # } | 'using': {
557
+ # ], | 'value': :work_email_address,
558
+ # "phoneNumbers": [ | 'primary': true
559
559
  # { | }
560
560
  # "type": "work", | }
561
561
  # "primary": false, | ],
@@ -568,7 +568,7 @@ module Scimitar
568
568
  # "location": "https://test.com/mock_users/42", | }
569
569
  # "resourceType": "User" | }
570
570
  # }, | ],
571
- # "schemas": [ | "active": :is_active"
571
+ # "schemas": [ | 'active': :is_active
572
572
  # "urn:ietf:params:scim:schemas:core:2.0:User" | }
573
573
  # ] |
574
574
  # } |
@@ -600,7 +600,7 @@ module Scimitar
600
600
  scim_hash_or_leaf_value:,
601
601
  path: []
602
602
  )
603
- attrs_map_or_leaf_value = attrs_map_or_leaf_value.with_indifferent_access() if attrs_map_or_leaf_value.instance_of?(Hash)
603
+ scim_hash_or_leaf_value = scim_hash_or_leaf_value.with_indifferent_case_insensitive_access() if scim_hash_or_leaf_value.is_a?(Hash)
604
604
 
605
605
  # We get the schema via this instance's class's resource type, even
606
606
  # if we end up in collections of other types - because it's *this*
@@ -668,7 +668,7 @@ module Scimitar
668
668
  end # "map_entry&.each do | mapped_array_entry |"
669
669
 
670
670
  when Symbol # Setter/getter method at leaf position in attribute map
671
- if path == ['externalId'] # Special case held only in schema base class
671
+ if path.length == 1 && path.first&.to_s&.downcase == 'externalid' # Special case held only in schema base class
672
672
  mutable = true
673
673
  else
674
674
  attribute = resource_class.find_attribute(*path)
@@ -706,7 +706,8 @@ module Scimitar
706
706
  #
707
707
  # +altering_hash+:: The Hash to operate on at the current +path+. For
708
708
  # recursive calls, this will be some way down into
709
- # the SCIM representation of 'self'.
709
+ # the SCIM representation of 'self'. MUST be a
710
+ # HashWithIndifferentCaseInsensitiveAccess.
710
711
  #
711
712
  # Note that SCIM PATCH operations permit *no* path for 'replace' and
712
713
  # 'add' operations, meaning "apply to whole object". To avoid special
@@ -715,6 +716,7 @@ module Scimitar
715
716
  # interest and supply this key as the sole array entry in +path+.
716
717
  #
717
718
  def from_patch_backend!(nature:, path:, value:, altering_hash:)
719
+ raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
718
720
 
719
721
  # These all throw exceptions if data is not as expected / required,
720
722
  # any of which are rescued below.
@@ -752,6 +754,8 @@ module Scimitar
752
754
  # Happily throws exceptions if data is not as expected / required.
753
755
  #
754
756
  def from_patch_backend_traverse!(nature:, path:, value:, altering_hash:)
757
+ raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
758
+
755
759
  path_component, filter = extract_filter_from(path_component: path.first)
756
760
 
757
761
  # https://tools.ietf.org/html/rfc7644#section-3.5.2.1
@@ -766,7 +770,7 @@ module Scimitar
766
770
  #
767
771
  # Harmless in this context for 'remove'.
768
772
  #
769
- altering_hash[path_component] ||= {}
773
+ altering_hash[path_component] ||= Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
770
774
 
771
775
  # Unless the PATCH is bad, inner data is an Array or Hash always as
772
776
  # by definition this method is only called at path positions above
@@ -784,7 +788,7 @@ module Scimitar
784
788
  # Same reason as section 3.5.2.1 / 3.5.2.3 RFC quotes above.
785
789
  #
786
790
  if nature != 'remove' && matched_hashes.empty?
787
- new_hash = {}
791
+ new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
788
792
  altering_hash[path_component] = [new_hash]
789
793
  matched_hashes = [new_hash]
790
794
  end
@@ -815,6 +819,8 @@ module Scimitar
815
819
  # Happily throws exceptions if data is not as expected / required.
816
820
  #
817
821
  def from_patch_backend_apply!(nature:, path:, value:, altering_hash:)
822
+ raise 'Case sensitivity violation' unless altering_hash.is_a?(Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess)
823
+
818
824
  path_component, filter = extract_filter_from(path_component: path.first)
819
825
  current_data_at_path = altering_hash[path_component]
820
826
 
@@ -968,7 +974,9 @@ module Scimitar
968
974
  value = value[1..-2] if value.start_with?('"') && value.end_with?('"')
969
975
 
970
976
  within_array.each.with_index do | hash, index |
971
- matched = hash.key?(attribute) && hash[attribute]&.to_s == value&.to_s
977
+ ci_hash = hash.with_indifferent_case_insensitive_access()
978
+ matched = ci_hash.key?(attribute) && ci_hash[attribute]&.to_s == value&.to_s
979
+
972
980
  yield(hash, index) if matched
973
981
  end
974
982
  end
@@ -88,7 +88,7 @@ module Scimitar
88
88
  end
89
89
  value.class.schema.valid?(value)
90
90
  return true if value.errors.empty?
91
- add_errors_from_hash(value.errors.to_hash, prefix: self.name)
91
+ add_errors_from_hash(errors_hash: value.errors.to_hash, prefix: self.name)
92
92
  false
93
93
  end
94
94
 
@@ -26,7 +26,9 @@ module Scimitar
26
26
  #
27
27
  def self.valid?(resource)
28
28
  cloned_scim_attributes.each do |scim_attribute|
29
- resource.add_errors_from_hash(scim_attribute.errors.to_hash) unless scim_attribute.valid?(resource.send(scim_attribute.name))
29
+ unless scim_attribute.valid?(resource.send(scim_attribute.name))
30
+ resource.add_errors_from_hash(errors_hash: scim_attribute.errors.to_hash)
31
+ end
30
32
  end
31
33
  end
32
34
 
@@ -62,8 +64,10 @@ module Scimitar
62
64
  current_path_entry = path.shift()
63
65
  next if current_path_entry.is_a?(Integer) # Skip array indicies arising from multi-value attributes
64
66
 
67
+ current_path_entry = current_path_entry.to_s.downcase
68
+
65
69
  found_attribute = current_attributes.find do | attribute_to_check |
66
- attribute_to_check.name == current_path_entry
70
+ attribute_to_check.name.to_s.downcase == current_path_entry
67
71
  end
68
72
 
69
73
  if found_attribute && path.present? # Any sub-attributes to check?...
@@ -2,81 +2,85 @@
2
2
  #
3
3
  # For supporting information and rationale, please see README.md.
4
4
 
5
- # =============================================================================
6
- # SERVICE PROVIDER CONFIGURATION
7
- # =============================================================================
8
- #
9
- # This is a Ruby abstraction over a SCIM entity that declares the capabilities
10
- # supported by a particular implementation.
11
- #
12
- # Typically this is used to declare parts of the standard unsupported, if you
13
- # don't need them and don't want to provide subclass support.
14
- #
15
- Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
5
+ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
16
6
 
17
- # See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
7
+ # ===========================================================================
8
+ # SERVICE PROVIDER CONFIGURATION
9
+ # ===========================================================================
18
10
  #
19
- # See Gem source file 'app/models/scimitar/service_provider_configuration.rb'
20
- # for defaults. Define Hash keys here that override defaults; e.g. to declare
21
- # that filters are not supported so that calling clients shouldn't use them:
11
+ # This is a Ruby abstraction over a SCIM entity that declares the
12
+ # capabilities supported by a particular implementation.
22
13
  #
23
- # filter: Scimitar::Supported.unsupported
14
+ # Typically this is used to declare parts of the standard unsupported, if you
15
+ # don't need them and don't want to provide subclass support.
16
+ #
17
+ Scimitar.service_provider_configuration = Scimitar::ServiceProviderConfiguration.new({
24
18
 
25
- })
19
+ # See https://tools.ietf.org/html/rfc7643#section-8.5 for properties.
20
+ #
21
+ # See Gem file 'app/models/scimitar/service_provider_configuration.rb'
22
+ # for defaults. Define Hash keys here that override defaults; e.g. to
23
+ # declare that filters are not supported so that calling clients shouldn't
24
+ # use them:
25
+ #
26
+ # filter: Scimitar::Supported.unsupported
26
27
 
27
- # =============================================================================
28
- # ENGINE CONFIGURATION
29
- # =============================================================================
30
- #
31
- # This is where you provide callbacks for things like authorisation or mixins
32
- # that get included into all Scimitar-derived controllers (for things like
33
- # before-actions that apply to all Scimitar controller-based routes).
34
- #
35
- Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
28
+ })
36
29
 
37
- # If you have filters you want to run for any Scimitar action/route, you can
38
- # define them here. For example, you might use a before-action to set up some
39
- # multi-tenancy related state, or skip Rails CSRF token verification/
40
- #
41
- # For example:
30
+ # ===========================================================================
31
+ # ENGINE CONFIGURATION
32
+ # ===========================================================================
42
33
  #
43
- # application_controller_mixin: Module.new do
44
- # def self.included(base)
45
- # base.class_eval do
34
+ # This is where you provide callbacks for things like authorisation or mixins
35
+ # that get included into all Scimitar-derived controllers (for things like
36
+ # before-actions that apply to all Scimitar controller-based routes).
46
37
  #
47
- # # Anything here is written just as you'd write it at the top of
48
- # # one of your controller classes, but it gets included in all
49
- # # Scimitar classes too.
50
- #
51
- # skip_before_action :verify_authenticity_token
52
- # prepend_before_action :setup_some_kind_of_multi_tenancy_data
53
- # end
54
- # end
55
- # end, # ...other configuration entries might follow...
38
+ Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
56
39
 
57
- # If you want to support username/password authentication:
58
- #
59
- # basic_authenticator: Proc.new do | username, password |
60
- # # Check username/password and return 'true' if valid, else 'false'.
61
- # end, # ...other configuration entries might follow...
62
- #
63
- # The 'username' and 'password' parameters come from Rails:
64
- #
65
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
66
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
40
+ # If you have filters you want to run for any Scimitar action/route, you
41
+ # can define them here. For example, you might use a before-action to set
42
+ # up some multi-tenancy related state, or skip Rails CSRF token
43
+ # verification. For example:
44
+ #
45
+ # application_controller_mixin: Module.new do
46
+ # def self.included(base)
47
+ # base.class_eval do
48
+ #
49
+ # # Anything here is written just as you'd write it at the top of
50
+ # # one of your controller classes, but it gets included in all
51
+ # # Scimitar classes too.
52
+ #
53
+ # skip_before_action :verify_authenticity_token
54
+ # prepend_before_action :setup_some_kind_of_multi_tenancy_data
55
+ # end
56
+ # end
57
+ # end, # ...other configuration entries might follow...
67
58
 
68
- # If you want to support HTTP bearer token (OAuth-style) authentication:
69
- #
70
- # token_authenticator: Proc.new do | token, options |
71
- # # Check token and return 'true' if valid, else 'false'.
72
- # end, # ...other configuration entries might follow...
73
- #
74
- # The 'token' and 'options' parameters come from Rails:
75
- #
76
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
77
- # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
78
- #
79
- # Note that both basic and token authentication can be declared, with the
80
- # parameters in the inbound HTTP request determining which is invoked.
59
+ # If you want to support username/password authentication:
60
+ #
61
+ # basic_authenticator: Proc.new do | username, password |
62
+ # # Check username/password and return 'true' if valid, else 'false'.
63
+ # end, # ...other configuration entries might follow...
64
+ #
65
+ # The 'username' and 'password' parameters come from Rails:
66
+ #
67
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
68
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic/ControllerMethods.html#method-i-authenticate_with_http_basic
69
+
70
+ # If you want to support HTTP bearer token (OAuth-style) authentication:
71
+ #
72
+ # token_authenticator: Proc.new do | token, options |
73
+ # # Check token and return 'true' if valid, else 'false'.
74
+ # end, # ...other configuration entries might follow...
75
+ #
76
+ # The 'token' and 'options' parameters come from Rails:
77
+ #
78
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html
79
+ # https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token/ControllerMethods.html#method-i-authenticate_with_http_token
80
+ #
81
+ # Note that both basic and token authentication can be declared, with the
82
+ # parameters in the inbound HTTP request determining which is invoked.
83
+
84
+ })
81
85
 
82
- })
86
+ end
@@ -0,0 +1,86 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ class Hash
4
+
5
+ # Converts this Hash to an instance of
6
+ # Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess, which is
7
+ # a subclass of ActiveSupport::HashWithIndifferentAccess with the addition of
8
+ # case-insensitive lookup.
9
+ #
10
+ # Note that this is more thorough than the ActiveSupport counterpart. It
11
+ # converts recursively, so that all Hashes to arbitrary depth, including any
12
+ # hashes inside Arrays, are converted. This is an expensive operation.
13
+ #
14
+ def with_indifferent_case_insensitive_access
15
+ self.class.deep_indifferent_case_insensitive_access(self)
16
+ end
17
+
18
+ # Supports #with_indifferent_case_insensitive_access. Converts the given item
19
+ # to indifferent, case-insensitive access as a Hash; or converts Array items
20
+ # if given an Array; or returns the given object.
21
+ #
22
+ # Hashes and Arrays at all depths are duplicated as a result.
23
+ #
24
+ def self.deep_indifferent_case_insensitive_access(object)
25
+ if object.is_a?(Hash)
26
+ new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new(object)
27
+ new_hash.each do | key, value |
28
+ new_hash[key] = deep_indifferent_case_insensitive_access(value)
29
+ end
30
+ new_hash
31
+
32
+ elsif object.is_a?(Array)
33
+ object.map do | array_entry |
34
+ deep_indifferent_case_insensitive_access(array_entry)
35
+ end
36
+
37
+ else
38
+ object
39
+
40
+ end
41
+ end
42
+ end
43
+
44
+ module Scimitar
45
+ module Support
46
+
47
+ # A subclass of ActiveSupport::HashWithIndifferentAccess where not only
48
+ # can Hash keys be queried as Symbols or Strings, but they are looked up
49
+ # in a case-insensitive fashion too.
50
+ #
51
+ # During enumeration, Hash keys will always be returned in whatever case
52
+ # they were originally set.
53
+ #
54
+ class HashWithIndifferentCaseInsensitiveAccess < ActiveSupport::HashWithIndifferentAccess
55
+ def with_indifferent_case_insensitive_access
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ if Symbol.method_defined?(:name)
62
+ def convert_key(key)
63
+ key.kind_of?(Symbol) ? key.name.downcase : key.downcase
64
+ end
65
+ else
66
+ def convert_key(key)
67
+ key.kind_of?(Symbol) ? key.to_s.downcase : key.downcase
68
+ end
69
+ end
70
+
71
+ def update_with_single_argument(other_hash, block)
72
+ if other_hash.is_a? HashWithIndifferentCaseInsensitiveAccess
73
+ regular_update(other_hash, &block)
74
+ else
75
+ other_hash.to_hash.each_pair do |key, value|
76
+ if block && key?(key)
77
+ value = block.call(convert_key(key), self[key], value)
78
+ end
79
+ regular_writer(convert_key(key), convert_value(value))
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ 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.0.3'
6
+ VERSION = '2.0.0'
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 = '2021-03-24'
11
+ DATE = '2022-03-04'
12
12
 
13
13
  end
data/lib/scimitar.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'scimitar/version'
2
+ require 'scimitar/support/hash_with_indifferent_case_insensitive_access'
2
3
  require 'scimitar/engine'
3
4
 
4
5
  module Scimitar
@@ -12,6 +12,7 @@ require 'scimitar'
12
12
 
13
13
  module Dummy
14
14
  class Application < Rails::Application
15
+ config.load_defaults 7.0
15
16
  end
16
17
  end
17
18
 
@@ -1,15 +1,38 @@
1
+ require 'active_support/core_ext/integer/time'
2
+
1
3
  Rails.application.configure do
2
4
  config.cache_classes = true
3
5
  config.eager_load = false
4
- config.serve_static_files = true
5
- config.static_cache_control = 'public, max-age=3600'
6
- config.consider_all_requests_local = true
7
6
 
8
- config.action_dispatch.show_exceptions = false
7
+ # Configure public file server for tests with Cache-Control for performance.
8
+ config.public_file_server.enabled = true
9
+ config.public_file_server.headers = {
10
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
11
+ }
9
12
 
13
+ # Show full error reports and disable caching.
14
+ config.consider_all_requests_local = true
10
15
  config.action_controller.perform_caching = false
16
+ config.cache_store = :null_store
17
+
18
+ # Raise exceptions instead of rendering exception templates.
19
+ config.action_dispatch.show_exceptions = false
20
+
21
+ # Disable request forgery protection in test environment.
11
22
  config.action_controller.allow_forgery_protection = false
12
23
 
13
- config.active_support.test_order = :random
24
+ # Print deprecation notices to the stderr.
14
25
  config.active_support.deprecation = :stderr
26
+
27
+ # Raise exceptions for disallowed deprecations.
28
+ config.active_support.disallowed_deprecation = :raise
29
+
30
+ # Tell Active Support which deprecation messages to disallow.
31
+ config.active_support.disallowed_deprecation_warnings = []
32
+
33
+ # Raises error for missing translations.
34
+ config.i18n.raise_on_missing_translations = true
35
+
36
+ # Annotate rendered view with file names.
37
+ # config.action_view.annotate_rendered_view_with_filenames = true
15
38
  end