scimitar 1.10.0 → 2.0.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/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
|