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 +4 -4
- data/app/models/scimitar/complex_types/base.rb +43 -1
- data/app/models/scimitar/resources/base.rb +22 -4
- data/app/models/scimitar/resources/mixin.rb +52 -29
- data/app/models/scimitar/schema/base.rb +3 -1
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +86 -0
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +1 -0
- data/spec/models/scimitar/resources/base_spec.rb +108 -58
- data/spec/models/scimitar/resources/mixin_spec.rb +342 -265
- data/spec/models/scimitar/resources/user_spec.rb +13 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +172 -127
- data/spec/requests/engine_spec.rb +26 -1
- data/spec/spec_helper.rb +27 -0
- data/spec/spec_helper_spec.rb +30 -0
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +61 -0
- metadata +16 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 834a7c3f5dba88856dfea8fdfcc1807f49b36739d1c5886e0dc96e7ca5621642
|
4
|
+
data.tar.gz: 7aaa9fae826b8142c3c3418b825ca86c7de46bb543dcdfb19b042caea15f988e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
450
|
+
altering_hash: ci_scim_hash
|
451
451
|
)
|
452
452
|
|
453
453
|
if extract_root
|
454
|
-
|
454
|
+
ci_scim_hash = ci_scim_hash['root']
|
455
455
|
end
|
456
456
|
end
|
457
457
|
|
458
|
-
self.from_scim!(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", |
|
546
|
-
# "name": { |
|
547
|
-
# "givenName": "Foo", |
|
548
|
-
# "familyName": "Bar" |
|
549
|
-
# }, |
|
550
|
-
# "active": true, |
|
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
|
-
# { |
|
552
|
+
# { | 'emails': [
|
553
553
|
# "type": "work", <------\ | {
|
554
|
-
# "primary": true, \------+---
|
555
|
-
# "value": "foo.bar@test.com" |
|
556
|
-
# } |
|
557
|
-
# ], |
|
558
|
-
# "phoneNumbers": [ |
|
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": [ |
|
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
|
-
|
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 ==
|
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
|
-
|
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
|
-
|
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
|
data/lib/scimitar/version.rb
CHANGED
@@ -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.
|
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-
|
11
|
+
DATE = '2021-09-15'
|
12
12
|
|
13
13
|
end
|
data/lib/scimitar.rb
CHANGED
@@ -24,53 +24,78 @@ RSpec.describe Scimitar::Resources::Base do
|
|
24
24
|
end
|
25
25
|
|
26
26
|
context '#initialize' do
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
|