diff --git a/app/models/scimitar/resources/mixin.rb b/app/models/scimitar/resources/mixin.rb index 4dd0c5c..160aa3f 100644 --- a/app/models/scimitar/resources/mixin.rb +++ b/app/models/scimitar/resources/mixin.rb @@ -952,7 +952,10 @@ def from_patch_backend_apply!(nature:, path:, value:, altering_hash:) when 'replace' if path_component == 'root' - altering_hash[path_component].merge!(value) + dot_pathed_value = value.inject({}) do |hash, (k, v)| + hash.deep_merge!(::Scimitar::Support::Utilities.dot_path(k.split('.'), v)) + end + altering_hash[path_component].deep_merge!(dot_pathed_value) else altering_hash[path_component] = value end diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..907ac17 --- /dev/null +++ b/bin/console @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +require 'bundler/setup' +require 'rails' + +require_relative '../lib/scimitar' + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/lib/scimitar.rb b/lib/scimitar.rb index 06d6b08..2c60ce5 100644 --- a/lib/scimitar.rb +++ b/lib/scimitar.rb @@ -1,5 +1,6 @@ require 'scimitar/version' require 'scimitar/support/hash_with_indifferent_case_insensitive_access' +require 'scimitar/support/utilities' require 'scimitar/engine' module Scimitar diff --git a/lib/scimitar/support/utilities.rb b/lib/scimitar/support/utilities.rb new file mode 100644 index 0000000..f40575e --- /dev/null +++ b/lib/scimitar/support/utilities.rb @@ -0,0 +1,51 @@ +module Scimitar + + # Namespace containing various chunks of Scimitar support code that don't + # logically fit into other areas. + # + module Support + + # A namespace that contains various stand-alone utility methods which act + # as helpers for other parts of the code base, without risking namespace + # pollution by e.g. being part of a module loaded into a client class. + # + module Utilities + + # Takes an array of components that usually come from a dotted path such + # as foo.bar.baz, along with a value that is found at the end of + # that path, then converts it into a nested Hash with each level of the + # Hash corresponding to a step along the path. + # + # This was written to help with edge case SCIM uses where (most often, at + # least) inbound calls use a dotted notation where nested values are more + # commonly accepted; converting to nesting makes it easier for subsequent + # processing code, which needs only handle nested Hash data. + # + # As an example, passing: + # + # ['foo', 'bar', 'baz'], 'value' + # + # ...yields: + # + # {'foo' => {'bar' => {'baz' => 'value'}}} + # + # Parameters: + # + # +array+:: Array containing path components, usually acquired from a + # string with dot separators and a call to String#split. + # + # +value+:: The value found at the path indicated by +array+. + # + # If +array+ is empty, +value+ is returned directly, with no nesting + # Hash wrapping it. + # + def self.dot_path(array, value) + return value if array.empty? + + {}.tap do | hash | + hash[array.shift()] = self.dot_path(array, value) + end + end + end + end +end diff --git a/spec/models/scimitar/resources/mixin_spec.rb b/spec/models/scimitar/resources/mixin_spec.rb index e92f0ad..c8a1231 100644 --- a/spec/models/scimitar/resources/mixin_spec.rb +++ b/spec/models/scimitar/resources/mixin_spec.rb @@ -2717,6 +2717,28 @@ def self.scim_queryable_attributes expect(@instance.username).to eql('1234') end + it 'which updates nested values using root syntax' do + @instance.update!(first_name: 'Foo', last_name: 'Bar') + + path = 'name.givenName' + path = path.upcase if force_upper_case + + patch = { + 'schemas' => ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], + 'Operations' => [ + { + 'op' => 'replace', + 'value' => { + path => 'Baz' + } + } + ] + } + + @instance.from_scim_patch!(patch_hash: patch) + expect(@instance.first_name).to eql('Baz') + end + it 'which updates nested values' do @instance.update!(first_name: 'Foo', last_name: 'Bar')