scimitar 1.10.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +23 -98
- data/app/controllers/scimitar/application_controller.rb +13 -41
- data/app/controllers/scimitar/resource_types_controller.rb +2 -0
- data/app/controllers/scimitar/resources_controller.rb +2 -0
- data/app/controllers/scimitar/schemas_controller.rb +3 -366
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +1 -0
- data/app/models/scimitar/complex_types/address.rb +6 -0
- data/app/models/scimitar/engine_configuration.rb +5 -15
- data/app/models/scimitar/error_response.rb +0 -12
- data/app/models/scimitar/lists/query_parser.rb +13 -113
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resources/base.rb +9 -53
- data/app/models/scimitar/resources/mixin.rb +59 -646
- data/app/models/scimitar/schema/address.rb +0 -1
- data/app/models/scimitar/schema/attribute.rb +5 -14
- data/app/models/scimitar/schema/base.rb +1 -1
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/app/models/scimitar/schema/vdtp.rb +1 -1
- data/app/models/scimitar/service_provider_configuration.rb +3 -14
- data/config/initializers/scimitar.rb +3 -69
- data/lib/scimitar/engine.rb +12 -57
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +2 -7
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
- data/spec/apps/dummy/app/models/mock_group.rb +1 -1
- data/spec/apps/dummy/app/models/mock_user.rb +9 -52
- data/spec/apps/dummy/config/application.rb +1 -0
- data/spec/apps/dummy/config/environments/test.rb +28 -5
- data/spec/apps/dummy/config/initializers/scimitar.rb +10 -90
- data/spec/apps/dummy/config/routes.rb +7 -28
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -11
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
- data/spec/apps/dummy/db/schema.rb +4 -12
- data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
- data/spec/controllers/scimitar/schemas_controller_spec.rb +48 -344
- data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
- data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +9 -146
- data/spec/models/scimitar/resources/base_spec.rb +71 -217
- data/spec/models/scimitar/resources/base_validation_spec.rb +5 -43
- data/spec/models/scimitar/resources/mixin_spec.rb +129 -1508
- data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
- data/spec/models/scimitar/schema/base_spec.rb +1 -1
- data/spec/models/scimitar/schema/user_spec.rb +2 -12
- data/spec/requests/active_record_backed_resources_controller_spec.rb +66 -1016
- data/spec/requests/application_controller_spec.rb +3 -16
- data/spec/requests/engine_spec.rb +0 -75
- data/spec/spec_helper.rb +1 -9
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
- metadata +26 -37
- data/LICENSE.txt +0 -21
- data/README.md +0 -717
- data/lib/scimitar/support/utilities.rb +0 -111
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
- data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +0 -25
@@ -10,7 +10,6 @@ module Scimitar
|
|
10
10
|
def self.scim_attributes
|
11
11
|
@scim_attributes ||= [
|
12
12
|
Attribute.new(name: 'type', type: 'string'),
|
13
|
-
Attribute.new(name: 'primary', type: 'boolean'),
|
14
13
|
Attribute.new(name: 'formatted', type: 'string'),
|
15
14
|
Attribute.new(name: 'streetAddress', type: 'string'),
|
16
15
|
Attribute.new(name: 'locality', type: 'string'),
|
@@ -93,23 +93,14 @@ module Scimitar
|
|
93
93
|
end
|
94
94
|
|
95
95
|
def valid_simple_type?(value)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
|
102
|
-
end
|
96
|
+
valid = (type == 'string' && value.is_a?(String)) ||
|
97
|
+
(type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
|
98
|
+
(type == 'integer' && (value.is_a?(Integer))) ||
|
99
|
+
(type == 'dateTime' && valid_date_time?(value))
|
100
|
+
errors.add(self.name, "has the wrong type. It has to be a(n) #{self.type}.") unless valid
|
103
101
|
valid
|
104
102
|
end
|
105
103
|
|
106
|
-
def simple_type?(value)
|
107
|
-
(type == 'string' && value.is_a?(String)) ||
|
108
|
-
(type == 'boolean' && (value.is_a?(TrueClass) || value.is_a?(FalseClass))) ||
|
109
|
-
(type == 'integer' && (value.is_a?(Integer))) ||
|
110
|
-
(type == 'dateTime' && valid_date_time?(value))
|
111
|
-
end
|
112
|
-
|
113
104
|
def valid_date_time?(value)
|
114
105
|
!!Time.iso8601(value)
|
115
106
|
rescue ArgumentError
|
@@ -13,7 +13,7 @@ module Scimitar
|
|
13
13
|
|
14
14
|
# Converts the schema to its json representation that will be returned by /SCHEMAS end-point of a SCIM service provider.
|
15
15
|
def as_json(options = {})
|
16
|
-
@meta.location
|
16
|
+
@meta.location = Scimitar::Engine.routes.url_helpers.scim_schemas_path(name: id)
|
17
17
|
original = super
|
18
18
|
original.merge('attributes' => original.delete('scim_attributes'))
|
19
19
|
end
|
@@ -6,8 +6,8 @@ module Scimitar
|
|
6
6
|
|
7
7
|
def self.scim_attributes
|
8
8
|
@scim_attributes ||= [
|
9
|
-
Attribute.new(name: 'familyName', type: 'string'),
|
10
|
-
Attribute.new(name: 'givenName', type: 'string'),
|
9
|
+
Attribute.new(name: 'familyName', type: 'string', required: true),
|
10
|
+
Attribute.new(name: 'givenName', type: 'string', required: true),
|
11
11
|
Attribute.new(name: 'middleName', type: 'string'),
|
12
12
|
Attribute.new(name: 'formatted', type: 'string'),
|
13
13
|
Attribute.new(name: 'honorificPrefix', type: 'string'),
|
@@ -20,7 +20,7 @@ module Scimitar
|
|
20
20
|
[
|
21
21
|
Attribute.new(name: 'userName', type: 'string', uniqueness: 'server', required: true),
|
22
22
|
|
23
|
-
Attribute.new(name: 'name',
|
23
|
+
Attribute.new(name: 'name', complexType: Scimitar::ComplexTypes::Name),
|
24
24
|
|
25
25
|
Attribute.new(name: 'displayName', type: 'string'),
|
26
26
|
Attribute.new(name: 'nickName', type: 'string'),
|
@@ -35,15 +35,15 @@ module Scimitar
|
|
35
35
|
|
36
36
|
Attribute.new(name: 'password', type: 'string', mutability: 'writeOnly', returned: 'never'),
|
37
37
|
|
38
|
-
Attribute.new(name: 'emails',
|
39
|
-
Attribute.new(name: 'phoneNumbers',
|
40
|
-
Attribute.new(name: 'ims',
|
41
|
-
Attribute.new(name: 'photos',
|
42
|
-
Attribute.new(name: 'addresses',
|
43
|
-
Attribute.new(name: 'groups',
|
44
|
-
Attribute.new(name: 'entitlements',
|
45
|
-
Attribute.new(name: 'roles',
|
46
|
-
Attribute.new(name: 'x509Certificates',
|
38
|
+
Attribute.new(name: 'emails', multiValued: true, complexType: Scimitar::ComplexTypes::Email),
|
39
|
+
Attribute.new(name: 'phoneNumbers', multiValued: true, complexType: Scimitar::ComplexTypes::PhoneNumber),
|
40
|
+
Attribute.new(name: 'ims', multiValued: true, complexType: Scimitar::ComplexTypes::Ims),
|
41
|
+
Attribute.new(name: 'photos', multiValued: true, complexType: Scimitar::ComplexTypes::Photo),
|
42
|
+
Attribute.new(name: 'addresses', multiValued: true, complexType: Scimitar::ComplexTypes::Address),
|
43
|
+
Attribute.new(name: 'groups', multiValued: true, complexType: Scimitar::ComplexTypes::ReferenceGroup, mutability: 'readOnly'),
|
44
|
+
Attribute.new(name: 'entitlements', multiValued: true, complexType: Scimitar::ComplexTypes::Entitlement),
|
45
|
+
Attribute.new(name: 'roles', multiValued: true, complexType: Scimitar::ComplexTypes::Role),
|
46
|
+
Attribute.new(name: 'x509Certificates', multiValued: true, complexType: Scimitar::ComplexTypes::X509Certificate),
|
47
47
|
]
|
48
48
|
end
|
49
49
|
|
@@ -7,7 +7,7 @@ module Scimitar
|
|
7
7
|
class Vdtp < Base
|
8
8
|
def self.scim_attributes
|
9
9
|
@scim_attributes ||= [
|
10
|
-
Attribute.new(name: 'value', type: 'string', required:
|
10
|
+
Attribute.new(name: 'value', type: 'string', required: true),
|
11
11
|
Attribute.new(name: 'display', type: 'string', mutability: 'readOnly'),
|
12
12
|
Attribute.new(name: 'type', type: 'string'),
|
13
13
|
Attribute.new(name: 'primary', type: 'boolean'),
|
@@ -9,22 +9,11 @@ module Scimitar
|
|
9
9
|
class ServiceProviderConfiguration
|
10
10
|
include ActiveModel::Model
|
11
11
|
|
12
|
-
attr_accessor
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:bulk,
|
16
|
-
:filter,
|
17
|
-
:changePassword,
|
18
|
-
:sort,
|
19
|
-
:etag,
|
20
|
-
:authenticationSchemes,
|
21
|
-
:schemas,
|
22
|
-
:meta,
|
23
|
-
)
|
12
|
+
attr_accessor :patch, :bulk, :filter, :changePassword,
|
13
|
+
:sort, :etag, :authenticationSchemes,
|
14
|
+
:schemas, :meta
|
24
15
|
|
25
16
|
def initialize(attributes = {})
|
26
|
-
@uses_defaults = attributes.empty?
|
27
|
-
|
28
17
|
defaults = {
|
29
18
|
bulk: Supportable.unsupported,
|
30
19
|
changePassword: Supportable.unsupported,
|
@@ -38,10 +38,9 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
38
38
|
Scimitar.engine_configuration = Scimitar::EngineConfiguration.new({
|
39
39
|
|
40
40
|
# If you have filters you want to run for any Scimitar action/route, you
|
41
|
-
# can define them here.
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# customise how Scimitar generates URLs:
|
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:
|
45
44
|
#
|
46
45
|
# application_controller_mixin: Module.new do
|
47
46
|
# def self.included(base)
|
@@ -55,10 +54,6 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
55
54
|
# prepend_before_action :setup_some_kind_of_multi_tenancy_data
|
56
55
|
# end
|
57
56
|
# end
|
58
|
-
#
|
59
|
-
# def scim_schemas_url(options)
|
60
|
-
# super(custom_param: 'value', **options)
|
61
|
-
# end
|
62
57
|
# end, # ...other configuration entries might follow...
|
63
58
|
|
64
59
|
# If you want to support username/password authentication:
|
@@ -86,67 +81,6 @@ Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
|
|
86
81
|
# Note that both basic and token authentication can be declared, with the
|
87
82
|
# parameters in the inbound HTTP request determining which is invoked.
|
88
83
|
|
89
|
-
# Scimitar rescues certain error cases and exceptions, in order to return a
|
90
|
-
# JSON response to the API caller. If you want exceptions to also be
|
91
|
-
# reported to a third party system such as sentry.io or raygun.com, you can
|
92
|
-
# configure a Proc to do so. It is passed a Ruby exception subclass object.
|
93
|
-
# For example, a minimal sentry.io reporter might do this:
|
94
|
-
#
|
95
|
-
# exception_reporter: Proc.new do | exception |
|
96
|
-
# Sentry.capture_exception(exception)
|
97
|
-
# end
|
98
|
-
#
|
99
|
-
# You will still need to configure your reporting system according to its
|
100
|
-
# documentation (e.g. via a Rails "config/initializers/<foo>.rb" file).
|
101
|
-
|
102
|
-
# Scimilar treats "VDTP" (Value, Display, Type, Primary) attribute values,
|
103
|
-
# used for e.g. e-mail addresses or phone numbers, as required by default.
|
104
|
-
# If you encounter a service which calls these with e.g. "null" value data,
|
105
|
-
# you can configure all values to be optional. You'll need to deal with
|
106
|
-
# whatever that means for you receiving system in your model code.
|
107
|
-
#
|
108
|
-
# optional_value_fields_required: false
|
109
|
-
|
110
|
-
# The SCIM standard `/Schemas` endpoint lists, by default, all known schema
|
111
|
-
# definitions with the mutabilty (read-write, read-only, write-only) state
|
112
|
-
# described by those definitions, and includes all defined attributes. For
|
113
|
-
# user-defined schema, this will typically exactly match your underlying
|
114
|
-
# mapped attribute and model capability - it wouldn't make sense to define
|
115
|
-
# your own schema that misrepresented the implementation! For core SCIM RFC
|
116
|
-
# schema, though, you might want to only list actually mapped attributes.
|
117
|
-
# Further, if you happen to have a non-compliant implementation especially
|
118
|
-
# in relation to mutability of some attributes, you may want to report that
|
119
|
-
# accurately in the '/Schemas' list, for auto-discovery purposes. To switch
|
120
|
-
# to a significantly slower but more accurate render method for the list,
|
121
|
-
# driven by your resource subclasses and their attribute maps, set:
|
122
|
-
#
|
123
|
-
# schema_list_from_attribute_mappings: [...array...]
|
124
|
-
#
|
125
|
-
# ...where you provide an Array of *models*, your classes that include the
|
126
|
-
# Scimitar::Resources::Mixin module and, therefore, define an attribute map
|
127
|
-
# translating SCIM schema attributes into actual implemented data. These
|
128
|
-
# must *uniquely* describe, via the Scimitar resources they each declare in
|
129
|
-
# their Scimitar::Resources::Mixin::scim_resource_type implementation, the
|
130
|
-
# set of schemas and extended schemas you want to render. Should resources
|
131
|
-
# share schema, the '/Schemas' endpoint will fail since it cannot determine
|
132
|
-
# which model attribute map it should use and it needs the map in order to
|
133
|
-
# resolve the differences (if any) between what the schema might say, and
|
134
|
-
# what the actual underlying model supports.
|
135
|
-
#
|
136
|
-
# It is further _very_ _strongly_ _recommended_ that, for any
|
137
|
-
# +scim_attributes_map+ containing a collection which has "list:" key (for
|
138
|
-
# an associative array of zero or more entities; the Groups to which a User
|
139
|
-
# might belong is a good example) then you should also specify the "class:"
|
140
|
-
# key, giving the class used for objects in that associated collection. The
|
141
|
-
# class *must* include Scimitar::Resources::Mixin, since its own attribute
|
142
|
-
# map is consulted in order to render the part of the schema describing
|
143
|
-
# those associated properties in the owning resource. If you don't do this,
|
144
|
-
# and if you're using ActiveRecord, then Scimitar attempts association
|
145
|
-
# reflection to determine the collection class - but that's more fragile
|
146
|
-
# than just being told the exact class in the attribute map. No matter how
|
147
|
-
# this class is determined, though, it must be possible to create a simple
|
148
|
-
# instance with +new+ and no parameters, since that's needed in order to
|
149
|
-
# call Scimitar::Resources::Mixin#scim_mutable_attributes.
|
150
84
|
})
|
151
85
|
|
152
86
|
end
|
data/lib/scimitar/engine.rb
CHANGED
@@ -1,38 +1,15 @@
|
|
1
|
-
require 'rails/engine'
|
2
|
-
|
3
1
|
module Scimitar
|
4
2
|
class Engine < ::Rails::Engine
|
5
3
|
isolate_namespace Scimitar
|
6
4
|
|
7
|
-
config.autoload_once_paths = %W(
|
8
|
-
#{root}/app/controllers
|
9
|
-
#{root}/app/models
|
10
|
-
)
|
11
|
-
|
12
5
|
Mime::Type.register 'application/scim+json', :scim
|
13
6
|
|
14
7
|
ActionDispatch::Request.parameter_parsers[Mime::Type.lookup('application/scim+json').symbol] = lambda do |body|
|
15
8
|
JSON.parse(body)
|
16
9
|
end
|
17
10
|
|
18
|
-
# Return an Array of all supported default and custom resource classes.
|
19
|
-
# See also :add_custom_resource and :set_default_resources.
|
20
|
-
#
|
21
11
|
def self.resources
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
# Returns a flat array of instances of all resource schema included in the
|
26
|
-
# resource classes returned by ::resources.
|
27
|
-
#
|
28
|
-
def self.schemas
|
29
|
-
self.resources().map(&:schemas).flatten.uniq.map(&:new)
|
30
|
-
end
|
31
|
-
|
32
|
-
# Returns the list of custom resources, if any.
|
33
|
-
#
|
34
|
-
def self.custom_resources
|
35
|
-
@custom_resources ||= []
|
12
|
+
default_resources + custom_resources
|
36
13
|
end
|
37
14
|
|
38
15
|
# Can be used to add a new resource type which is not provided by the gem.
|
@@ -53,7 +30,7 @@ module Scimitar
|
|
53
30
|
# Scimitar::Engine.add_custom_resource Scim::Resources::ShinyResource
|
54
31
|
#
|
55
32
|
def self.add_custom_resource(resource)
|
56
|
-
|
33
|
+
custom_resources << resource
|
57
34
|
end
|
58
35
|
|
59
36
|
# Resets the resource list to default. This is really only intended for use
|
@@ -63,45 +40,23 @@ module Scimitar
|
|
63
40
|
@custom_resources = []
|
64
41
|
end
|
65
42
|
|
66
|
-
# Returns the
|
67
|
-
#
|
68
|
-
# * Scimitar::Resources::User
|
69
|
-
# * Scimitar::Resources::Group
|
70
|
-
#
|
71
|
-
# ...but if an implementation does not e.g. support Group, it can
|
72
|
-
# be overridden via ::set_default_resources to help with service
|
73
|
-
# auto-discovery.
|
43
|
+
# Returns the list of custom resources, if any.
|
74
44
|
#
|
75
|
-
def self.
|
76
|
-
@
|
77
|
-
@default_resources ||= @standard_default_resources.dup()
|
45
|
+
def self.custom_resources
|
46
|
+
@custom_resources ||= []
|
78
47
|
end
|
79
48
|
|
80
|
-
#
|
49
|
+
# Returns the default resources added in this gem:
|
81
50
|
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
# Scimitar::Resources::Group, and nothing else.
|
51
|
+
# * Scimitar::Resources::User
|
52
|
+
# * Scimitar::Resources::Group
|
85
53
|
#
|
86
|
-
def self.
|
87
|
-
|
88
|
-
unrecognised_resources = resource_array - @standard_default_resources
|
89
|
-
|
90
|
-
if unrecognised_resources.any?
|
91
|
-
raise "Scimitar::Engine::set_default_resources: Only #{@standard_default_resources.map(&:name).join(', ')} are supported"
|
92
|
-
elsif resource_array.empty?
|
93
|
-
raise 'Scimitar::Engine::set_default_resources: At least one resource must be given'
|
94
|
-
end
|
95
|
-
|
96
|
-
@default_resources = resource_array
|
54
|
+
def self.default_resources
|
55
|
+
[ Resources::User, Resources::Group ]
|
97
56
|
end
|
98
57
|
|
99
|
-
|
100
|
-
|
101
|
-
#
|
102
|
-
def self.reset_default_resources
|
103
|
-
self.default_resources()
|
104
|
-
@default_resources = @standard_default_resources
|
58
|
+
def self.schemas
|
59
|
+
resources.map(&:schemas).flatten.uniq.map(&:new)
|
105
60
|
end
|
106
61
|
|
107
62
|
end
|
@@ -23,8 +23,8 @@ class Hash
|
|
23
23
|
#
|
24
24
|
def self.deep_indifferent_case_insensitive_access(object)
|
25
25
|
if object.is_a?(Hash)
|
26
|
-
new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new
|
27
|
-
|
26
|
+
new_hash = Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess.new(object)
|
27
|
+
new_hash.each do | key, value |
|
28
28
|
new_hash[key] = deep_indifferent_case_insensitive_access(value)
|
29
29
|
end
|
30
30
|
new_hash
|
@@ -49,164 +49,34 @@ module Scimitar
|
|
49
49
|
# in a case-insensitive fashion too.
|
50
50
|
#
|
51
51
|
# During enumeration, Hash keys will always be returned in whatever case
|
52
|
-
# they were originally set.
|
53
|
-
# ActiveSupport::HashWithIndifferentAccess, though, the type of the keys is
|
54
|
-
# always returned as a String, even if originally set as a Symbol - only
|
55
|
-
# the upper/lower case nature of the original key is preserved.
|
56
|
-
#
|
57
|
-
# If a key is written more than once with the same effective meaning in a
|
58
|
-
# to-string, to-downcase form, then whatever case was used *first* wins;
|
59
|
-
# e.g. if you did hash['User'] = 23, then hash['USER'] = 42, the result
|
60
|
-
# would be {"User" => 42}.
|
61
|
-
#
|
62
|
-
# It's important to remember that Hash#merge is shallow and replaces values
|
63
|
-
# found at existing keys in the target ("this") hash with values in the
|
64
|
-
# inbound Hash. If that new value that is itself a Hash, this *replaces*
|
65
|
-
# the value. For example:
|
66
|
-
#
|
67
|
-
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
68
|
-
# * Merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
|
69
|
-
#
|
70
|
-
# ...results in "this" target hash's key +Foo+ being addressed in the merge
|
71
|
-
# by inbound key +FOO+, so the case doesn't change. But the value for +Foo+
|
72
|
-
# is _replaced_ by the merging-in Hash completely:
|
73
|
-
#
|
74
|
-
# * Result: <tt>'Foo' => { 'BAR' => 24 }</tt>
|
75
|
-
#
|
76
|
-
# ...and of course we might've replaced with a totally different type, such
|
77
|
-
# as +true+:
|
78
|
-
#
|
79
|
-
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
80
|
-
# * Merge: <tt>'FOO' => true</tt>
|
81
|
-
# * Result: <tt>'Foo' => true</tt>
|
82
|
-
#
|
83
|
-
# If you're intending to merge nested Hashes, then use ActiveSupport's
|
84
|
-
# #deep_merge or an equivalent. This will have the expected outcome, where
|
85
|
-
# the hash with 'BAR' is _merged_ into the existing value and, therefore,
|
86
|
-
# the original 'Bar' key case is preserved:
|
87
|
-
#
|
88
|
-
# * Original: <tt>'Foo' => { 'Bar' => 42 }</tt>
|
89
|
-
# * Deep merge: <tt>'FOO' => { 'BAR' => 24 }</tt>
|
90
|
-
# * Result: <tt>'Foo' => { 'Bar' => 24 }</tt>
|
52
|
+
# they were originally set.
|
91
53
|
#
|
92
54
|
class HashWithIndifferentCaseInsensitiveAccess < ActiveSupport::HashWithIndifferentAccess
|
93
55
|
def with_indifferent_case_insensitive_access
|
94
56
|
self
|
95
57
|
end
|
96
58
|
|
97
|
-
def initialize(constructor = nil)
|
98
|
-
@scimitar_hash_with_indifferent_case_insensitive_access_key_map = {}
|
99
|
-
super
|
100
|
-
end
|
101
|
-
|
102
|
-
# It's vital that the attribute map is carried over when one of these
|
103
|
-
# objects is duplicated. Duplication of this ivar state does *not* happen
|
104
|
-
# when 'dup' is called on our superclass, so we have to do that manually.
|
105
|
-
#
|
106
|
-
def dup
|
107
|
-
duplicate = super
|
108
|
-
duplicate.instance_variable_set(
|
109
|
-
'@scimitar_hash_with_indifferent_case_insensitive_access_key_map',
|
110
|
-
@scimitar_hash_with_indifferent_case_insensitive_access_key_map
|
111
|
-
)
|
112
|
-
|
113
|
-
return duplicate
|
114
|
-
end
|
115
|
-
|
116
|
-
# Override the individual key writer.
|
117
|
-
#
|
118
|
-
def []=(key, value)
|
119
|
-
string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
120
|
-
indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
|
121
|
-
converted_value = convert_value(value, conversion: :assignment)
|
122
|
-
|
123
|
-
# Note '||=', as there might have been a prior use of the "same" key in
|
124
|
-
# a different case. The earliest one is preserved since the actual Hash
|
125
|
-
# underneath all this is already using that variant of the key.
|
126
|
-
#
|
127
|
-
key_for_writing = (
|
128
|
-
@scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] ||= string_key
|
129
|
-
)
|
130
|
-
|
131
|
-
regular_writer(key_for_writing, converted_value)
|
132
|
-
end
|
133
|
-
|
134
|
-
# Override #merge to express it in terms of #merge! (also overridden), so
|
135
|
-
# that merged hashes can have their keys treated indifferently too.
|
136
|
-
#
|
137
|
-
def merge(*other_hashes, &block)
|
138
|
-
dup.merge!(*other_hashes, &block)
|
139
|
-
end
|
140
|
-
|
141
|
-
# Modifies-self version of #merge, overriding Hash#merge!.
|
142
|
-
#
|
143
|
-
def merge!(*hashes_to_merge_to_self, &block)
|
144
|
-
if block_given?
|
145
|
-
hashes_to_merge_to_self.each do |hash_to_merge_to_self|
|
146
|
-
hash_to_merge_to_self.each_pair do |key, value|
|
147
|
-
value = block.call(key, self[key], value) if self.key?(key)
|
148
|
-
self[key] = value
|
149
|
-
end
|
150
|
-
end
|
151
|
-
else
|
152
|
-
hashes_to_merge_to_self.each do |hash_to_merge_to_self|
|
153
|
-
hash_to_merge_to_self.each_pair do |key, value|
|
154
|
-
self[key] = value
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
self
|
160
|
-
end
|
161
|
-
|
162
|
-
# =======================================================================
|
163
|
-
# PRIVATE INSTANCE METHODS
|
164
|
-
# =======================================================================
|
165
|
-
#
|
166
59
|
private
|
167
60
|
|
168
61
|
if Symbol.method_defined?(:name)
|
169
|
-
def
|
170
|
-
key.kind_of?(Symbol) ? key.name : key
|
62
|
+
def convert_key(key)
|
63
|
+
key.kind_of?(Symbol) ? key.name.downcase : key.downcase
|
171
64
|
end
|
172
65
|
else
|
173
|
-
def
|
174
|
-
key.kind_of?(Symbol) ? key.to_s : key
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def scimitar_hash_with_indifferent_case_insensitive_access_downcase(key)
|
179
|
-
key.kind_of?(String) ? key.downcase : key
|
180
|
-
end
|
181
|
-
|
182
|
-
def convert_key(key)
|
183
|
-
string_key = scimitar_hash_with_indifferent_case_insensitive_access_string(key)
|
184
|
-
indifferent_key = scimitar_hash_with_indifferent_case_insensitive_access_downcase(string_key)
|
185
|
-
|
186
|
-
@scimitar_hash_with_indifferent_case_insensitive_access_key_map[indifferent_key] || string_key
|
187
|
-
end
|
188
|
-
|
189
|
-
def convert_value(value, conversion: nil)
|
190
|
-
if value.is_a?(Hash)
|
191
|
-
if conversion == :to_hash
|
192
|
-
value.to_hash
|
193
|
-
else
|
194
|
-
value.with_indifferent_case_insensitive_access
|
195
|
-
end
|
196
|
-
else
|
197
|
-
super
|
66
|
+
def convert_key(key)
|
67
|
+
key.kind_of?(Symbol) ? key.to_s.downcase : key.downcase
|
198
68
|
end
|
199
69
|
end
|
200
70
|
|
201
71
|
def update_with_single_argument(other_hash, block)
|
202
|
-
if other_hash.is_a?
|
72
|
+
if other_hash.is_a? HashWithIndifferentCaseInsensitiveAccess
|
203
73
|
regular_update(other_hash, &block)
|
204
74
|
else
|
205
75
|
other_hash.to_hash.each_pair do |key, value|
|
206
76
|
if block && key?(key)
|
207
|
-
value = block.call(
|
77
|
+
value = block.call(convert_key(key), self[key], value)
|
208
78
|
end
|
209
|
-
|
79
|
+
regular_writer(convert_key(key), convert_value(value))
|
210
80
|
end
|
211
81
|
end
|
212
82
|
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 = '
|
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 = '
|
11
|
+
DATE = '2022-03-04'
|
12
12
|
|
13
13
|
end
|
data/lib/scimitar.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
require 'scimitar/version'
|
2
2
|
require 'scimitar/support/hash_with_indifferent_case_insensitive_access'
|
3
|
-
require 'scimitar/support/utilities'
|
4
3
|
require 'scimitar/engine'
|
5
4
|
|
6
5
|
module Scimitar
|
7
6
|
def self.service_provider_configuration=(custom_configuration)
|
8
|
-
|
9
|
-
@service_provider_configuration = custom_configuration
|
10
|
-
end
|
7
|
+
@service_provider_configuration = custom_configuration
|
11
8
|
end
|
12
9
|
|
13
10
|
def self.service_provider_configuration(location:)
|
@@ -17,9 +14,7 @@ module Scimitar
|
|
17
14
|
end
|
18
15
|
|
19
16
|
def self.engine_configuration=(custom_configuration)
|
20
|
-
|
21
|
-
@engine_configuration = custom_configuration
|
22
|
-
end
|
17
|
+
@engine_configuration = custom_configuration
|
23
18
|
end
|
24
19
|
|
25
20
|
def self.engine_configuration
|