scimitar 1.0.0 → 1.1.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: 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