scimitar 1.0.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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