scimitar 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7c0cb7d5ff4346954d9aab0d83ae22fcd75fcc456b9be54d74e22334cf4e273
4
- data.tar.gz: 47adaec88f0418f18294fb19374be773cbfafa6c94b8870e38a349f70a4c53b4
3
+ metadata.gz: 834a7c3f5dba88856dfea8fdfcc1807f49b36739d1c5886e0dc96e7ca5621642
4
+ data.tar.gz: 7aaa9fae826b8142c3c3418b825ca86c7de46bb543dcdfb19b042caea15f988e
5
5
  SHA512:
6
- metadata.gz: 59a7f529e86667e14de8a6c0a0b0bc9a26cd53b21b53656be8b2f37d31e4cddcc1d0fccdefe2ff695d31baf19764489903a59e61f31e863e78c544f5dd24d550
7
- data.tar.gz: '09d32ab29c325fa047f9e77daababa2bc763b79ee97e3b0b961202b60901b3ace406f5f66f63fd5c56b015a86e368294d4ec1e4b98b3588e7f4b75c72280e9e3'
6
+ metadata.gz: c3db5de7ca04d57be95f3638183936ab1cd4621b3aba22e0bcd6eaa3b2e876dfe2e79f86711da6de1106908ae96fd444c060b6bc10055e0b8f476faff6e18ca0
7
+ data.tar.gz: 701c4cb7d93f9dcfa89906bcfb222ba734d4ed83f0f2404969d8375df2a963c2376629508733d7e2852250d8652dac8e9ec07a6482f3a382fa64aaf2ac4ef9e0
@@ -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
 
@@ -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
 
@@ -945,15 +951,32 @@ module Scimitar
945
951
  # Happily throws exceptions if data is not as expected / required.
946
952
  #
947
953
  def all_matching_filter(filter:, within_array:, &block)
948
- filter_components = filter.split(' ')
949
- raise "Unsupported matcher #{filter.inspect}" unless filter_components.size == 3 && filter_components[1].downcase == 'eq'
954
+ filter_components = filter.split(' ', 3)
950
955
 
951
956
  attribute = filter_components[0]
957
+ operator = filter_components[1]
952
958
  value = filter_components[2]
953
- value = value[1..-2] if value.start_with?('"') && value.end_with?('"')
959
+
960
+ # Quoted value includes closing quote but has data afterwards?
961
+ # Bad; implies extra conditions, e.g. '...eq "one two" and..." or
962
+ # just junk data.
963
+ #
964
+ # Value is *not* quoted but contains a space? Means there must be
965
+ # again either extra conditions or trailing junk data.
966
+ #
967
+ raise "Unsupported matcher #{filter.inspect}" if (
968
+ filter_components.size != 3 ||
969
+ operator.downcase != 'eq' ||
970
+ value.strip.match?(/\".+[^\\]\".+/) || # Literal '"', any data, no-backslash-then-literal (unescaped) '"', more data
971
+ (!value.start_with?('"') && value.strip.include?(' '))
972
+ )
973
+
974
+ value = value[1..-2] if value.start_with?('"') && value.end_with?('"')
954
975
 
955
976
  within_array.each.with_index do | hash, index |
956
- 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
+
957
980
  yield(hash, index) if matched
958
981
  end
959
982
  end
@@ -62,8 +62,10 @@ module Scimitar
62
62
  current_path_entry = path.shift()
63
63
  next if current_path_entry.is_a?(Integer) # Skip array indicies arising from multi-value attributes
64
64
 
65
+ current_path_entry = current_path_entry.to_s.downcase
66
+
65
67
  found_attribute = current_attributes.find do | attribute_to_check |
66
- attribute_to_check.name == current_path_entry
68
+ attribute_to_check.name.to_s.downcase == current_path_entry
67
69
  end
68
70
 
69
71
  if found_attribute && path.present? # Any sub-attributes to check?...
@@ -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.0'
6
+ VERSION = '1.1.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 = '2021-09-15'
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
@@ -24,53 +24,78 @@ RSpec.describe Scimitar::Resources::Base do
24
24
  end
25
25
 
26
26
  context '#initialize' do
27
- it 'builds the nested type' do
28
- resource = CustomResourse.new(name: {
29
- givenName: 'John',
30
- familyName: 'Smith'
31
- })
27
+ shared_examples 'an initializer' do | force_upper_case: |
28
+ it 'which builds the nested type' do
29
+ attributes = {
30
+ name: {
31
+ givenName: 'John',
32
+ familyName: 'Smith'
33
+ }
34
+ }
32
35
 
33
- expect(resource.name.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
34
- expect(resource.name.givenName).to eql('John')
35
- expect(resource.name.familyName).to eql('Smith')
36
- end
36
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
37
+ resource = CustomResourse.new(attributes)
38
+
39
+ expect(resource.name.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
40
+ expect(resource.name.givenName).to eql('John')
41
+ expect(resource.name.familyName).to eql('Smith')
42
+ end
37
43
 
38
- it 'builds an array of nested resources' do
39
- resource = CustomResourse.new(names: [
40
- {
41
- givenName: 'John',
42
- familyName: 'Smith'
43
- },
44
- {
45
- givenName: 'Jane',
46
- familyName: 'Snow'
44
+ it 'which builds an array of nested resources' do
45
+ attributes = {
46
+ names:[
47
+ {
48
+ givenName: 'John',
49
+ familyName: 'Smith'
50
+ },
51
+ {
52
+ givenName: 'Jane',
53
+ familyName: 'Snow'
54
+ }
55
+ ]
47
56
  }
48
- ])
49
-
50
- expect(resource.names.is_a?(Array)).to be(true)
51
- expect(resource.names.first.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
52
- expect(resource.names.first.givenName).to eql('John')
53
- expect(resource.names.first.familyName).to eql('Smith')
54
- expect(resource.names.second.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
55
- expect(resource.names.second.givenName).to eql('Jane')
56
- expect(resource.names.second.familyName).to eql('Snow')
57
- expect(resource.valid?).to be(true)
58
- end
59
57
 
60
- it 'builds an array of nested resources which is invalid if the hash does not follow the schema of the complex type' do
61
- resource = CustomResourse.new(names: [
62
- {
63
- givenName: 'John',
64
- familyName: 123
58
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
59
+ resource = CustomResourse.new(attributes)
60
+
61
+ expect(resource.names.is_a?(Array)).to be(true)
62
+ expect(resource.names.first.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
63
+ expect(resource.names.first.givenName).to eql('John')
64
+ expect(resource.names.first.familyName).to eql('Smith')
65
+ expect(resource.names.second.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
66
+ expect(resource.names.second.givenName).to eql('Jane')
67
+ expect(resource.names.second.familyName).to eql('Snow')
68
+ expect(resource.valid?).to be(true)
69
+ end
70
+
71
+ it 'which builds an array of nested resources which is invalid if the hash does not follow the schema of the complex type' do
72
+ attributes = {
73
+ names: [
74
+ {
75
+ givenName: 'John',
76
+ familyName: 123
77
+ }
78
+ ]
65
79
  }
66
- ])
67
80
 
68
- expect(resource.names.is_a?(Array)).to be(true)
69
- expect(resource.names.first.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
70
- expect(resource.names.first.givenName).to eql('John')
71
- expect(resource.names.first.familyName).to eql(123)
72
- expect(resource.valid?).to be(false)
73
- end
81
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
82
+ resource = CustomResourse.new(attributes)
83
+
84
+ expect(resource.names.is_a?(Array)).to be(true)
85
+ expect(resource.names.first.is_a?(Scimitar::ComplexTypes::Name)).to be(true)
86
+ expect(resource.names.first.givenName).to eql('John')
87
+ expect(resource.names.first.familyName).to eql(123)
88
+ expect(resource.valid?).to be(false)
89
+ end
90
+ end # "shared_examples 'an initializer' do | force_upper_case: |"
91
+
92
+ context 'using schema-matched case' do
93
+ it_behaves_like 'an initializer', force_upper_case: false
94
+ end # "context 'using schema-matched case' do"
95
+
96
+ context 'using upper case' do
97
+ it_behaves_like 'an initializer', force_upper_case: true
98
+ end # "context 'using upper case' do"
74
99
  end # "context '#initialize' do"
75
100
 
76
101
  context '#as_json' do
@@ -88,26 +113,51 @@ RSpec.describe Scimitar::Resources::Base do
88
113
  end # "context '#as_json' do"
89
114
 
90
115
  context '.find_attribute' do
91
- it 'finds in complex type' do
92
- found = CustomResourse.find_attribute('name', 'givenName')
93
- expect(found).to be_present
94
- expect(found.name).to eql('givenName')
95
- expect(found.type).to eql('string')
96
- end
116
+ shared_examples 'a finder' do | force_upper_case: |
117
+ it 'which finds in complex type' do
118
+ args = ['name', 'givenName']
119
+ args.map!(&:upcase) if force_upper_case
97
120
 
98
- it 'finds in multi-value type, without index' do
99
- found = CustomResourse.find_attribute('names', 'givenName')
100
- expect(found).to be_present
101
- expect(found.name).to eql('givenName')
102
- expect(found.type).to eql('string')
103
- end
121
+ found = CustomResourse.find_attribute(*args)
104
122
 
105
- it 'finds in multi-value type, ignoring index' do
106
- found = CustomResourse.find_attribute('names', 42, 'givenName')
107
- expect(found).to be_present
108
- expect(found.name).to eql('givenName')
109
- expect(found.type).to eql('string')
123
+ expect(found).to be_present
124
+ expect(found.name).to eql('givenName')
125
+ expect(found.type).to eql('string')
126
+ end
127
+
128
+ it 'which finds in multi-value type, without index' do
129
+ args = ['names', 'givenName']
130
+ args.map!(&:upcase) if force_upper_case
131
+
132
+ found = CustomResourse.find_attribute(*args)
133
+
134
+ expect(found).to be_present
135
+ expect(found.name).to eql('givenName')
136
+ expect(found.type).to eql('string')
137
+ end
138
+
139
+ it 'which finds in multi-value type, ignoring index' do
140
+ args = if force_upper_case
141
+ ['NAMES', 42, 'GIVENNAME']
142
+ else
143
+ ['names', 42, 'givenName']
144
+ end
145
+
146
+ found = CustomResourse.find_attribute(*args)
147
+
148
+ expect(found).to be_present
149
+ expect(found.name).to eql('givenName')
150
+ expect(found.type).to eql('string')
151
+ end # "shared_examples 'a finder' do | force_upper_case: |"
110
152
  end
153
+
154
+ context 'using schema-matched case' do
155
+ it_behaves_like 'a finder', force_upper_case: false
156
+ end # "context 'using schema-matched case' do"
157
+
158
+ context 'using upper case' do
159
+ it_behaves_like 'a finder', force_upper_case: true
160
+ end # "context 'using upper case' do"
111
161
  end # "context '.find_attribute' do"
112
162
  end # "context 'basic operation' do"
113
163