Comparing changes
v0.2.9
→
v0.2.10
13 commits
10 files changed
Commits
Changed files (10)
lib
spec
scim
lib/scim/kit/v2/templates/attribute.json.jbuilder
@@ -7,6 +7,6 @@ if type.complex? && !type.multi_valued
render attribute, json: json
end
end
-else
+elsif renderable?
json.set! type.name, _value
end
lib/scim/kit/v2/templates/resource.json.jbuilder
@@ -1,23 +1,25 @@
# frozen_string_literal: true
json.key_format! camelize: :lower
-if meta.location
+if mode?(:server)
json.meta do
render meta, json: json
end
end
json.schemas schemas.map(&:id)
-json.id id if id
-json.external_id external_id if external_id
+json.id id if mode?(:server)
+json.external_id external_id if mode?(:client) && external_id
schemas.each do |schema|
if schema.core?
schema.attributes.each do |type|
- render dynamic_attributes[type.name], json: json
+ attribute = dynamic_attributes[type.name]
+ render attribute, json: json
end
else
json.set! schema.id do
schema.attributes.each do |type|
- render dynamic_attributes[type.name], json: json
+ attribute = dynamic_attributes[type.name]
+ render attribute, json: json
end
end
end
lib/scim/kit/v2/attributable.rb
@@ -7,9 +7,9 @@ module Scim
module Attributable
attr_reader :dynamic_attributes
- def define_attributes_for(types)
+ def define_attributes_for(resource, types)
@dynamic_attributes = {}.with_indifferent_access
- types.each { |x| attribute(x) }
+ types.each { |x| attribute(x, resource) }
end
private
@@ -42,8 +42,11 @@ module Scim
end
end
- def attribute(type)
- dynamic_attributes[type.name] = Attribute.new(type: type)
+ def attribute(type, resource)
+ dynamic_attributes[type.name] = Attribute.new(
+ type: type,
+ resource: resource
+ )
extend(create_module_for(type))
end
end
lib/scim/kit/v2/attribute.rb
@@ -9,16 +9,18 @@ module Scim
include Attributable
include Templatable
attr_reader :type
+ attr_reader :_resource
attr_reader :_value
validate :presence_of_value, if: proc { |x| x.type.required }
validate :inclusion_of_value, if: proc { |x| x.type.canonical_values }
validate :validate_type
- def initialize(type:, value: nil)
+ def initialize(resource:, type:, value: nil)
@type = type
- @_value = value
- define_attributes_for(type.attributes)
+ @_value = value || type.multi_valued ? [] : nil
+ @_resource = resource
+ define_attributes_for(resource, type.attributes)
end
def _assign(new_value, coerce: true)
@@ -29,6 +31,14 @@ module Scim
_assign(new_value, coerce: true)
end
+ def renderable?
+ return false if read_only? && _resource.mode?(:client)
+ return false if write_only? &&
+ (_resource.mode?(:server) || _value.blank?)
+
+ true
+ end
+
private
def presence_of_value
@@ -48,6 +58,14 @@ module Scim
errors.add(type.name, I18n.t('errors.messages.invalid'))
end
+
+ def read_only?
+ type.mutability == Mutability::READ_ONLY
+ end
+
+ def write_only?
+ type.mutability == Mutability::WRITE_ONLY
+ end
end
end
end
lib/scim/kit/v2/attribute_type.rb
@@ -63,6 +63,7 @@ module Scim
end
def coerce(value)
+ return value if value.nil?
return value if complex?
if multi_valued
lib/scim/kit/v2/mutability.rb
@@ -10,10 +10,13 @@ module Scim
IMMUTABLE = 'immutable'
WRITE_ONLY = 'writeOnly'
VALID = {
+ immutable: IMMUTABLE,
read_only: READ_ONLY,
read_write: READ_WRITE,
- immutable: IMMUTABLE,
- write_only: WRITE_ONLY
+ readonly: READ_ONLY,
+ readwrite: READ_WRITE,
+ write_only: WRITE_ONLY,
+ writeonly: WRITE_ONLY
}.freeze
def self.find(value)
lib/scim/kit/v2/resource.rb
@@ -21,7 +21,17 @@ module Scim
@meta.disable_timestamps
@schemas = schemas
schemas.each do |schema|
- define_attributes_for(schema.attributes)
+ define_attributes_for(self, schema.attributes)
+ end
+ yield self if block_given?
+ end
+
+ def mode?(type)
+ case type.to_sym
+ when :server
+ meta&.location
+ else
+ meta&.location.nil?
end
end
lib/scim/kit/version.rb
@@ -2,6 +2,6 @@
module Scim
module Kit
- VERSION = '0.2.9'
+ VERSION = '0.2.10'
end
end
spec/scim/kit/v2/attribute_spec.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
RSpec.describe Scim::Kit::V2::Attribute do
- subject { described_class.new(type: type) }
+ subject { described_class.new(type: type, resource: resource) }
+
+ let(:resource) { Scim::Kit::V2::Resource.new(schemas: [schema], location: FFaker::Internet.uri('https')) }
+ let(:schema) { Scim::Kit::V2::Schema.new(id: Scim::Kit::V2::Schemas::USER, name: 'User', location: FFaker::Internet.uri('https')) }
context 'with strings' do
let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) }
@@ -17,12 +20,13 @@ RSpec.describe Scim::Kit::V2::Attribute do
end
context 'when multiple values are allowed' do
- before do
- type.multi_valued = true
+ before { type.multi_valued = true }
+
+ specify { expect(subject._value).to match_array([]) }
+ specify do
subject._value = %w[superman batman]
+ expect(subject._value).to match_array(%w[superman batman])
end
-
- specify { expect(subject._value).to match_array(%w[superman batman]) }
end
context 'when a single value is provided' do
@@ -253,4 +257,56 @@ RSpec.describe Scim::Kit::V2::Attribute do
specify { expect(subject.errors[:emails]).to be_present }
end
end
+
+ context 'when the resource is in server mode' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) }
+ let(:resource) { instance_double(Scim::Kit::V2::Resource) }
+
+ before do
+ allow(resource).to receive(:mode?).with(:server).and_return(true)
+ allow(resource).to receive(:mode?).with(:client).and_return(false)
+ end
+
+ context 'when the type is read only' do
+ before { type.mutability = :read_only }
+
+ specify { expect(subject).to be_renderable }
+ end
+
+ context 'when the type is write only' do
+ before { type.mutability = :write_only }
+
+ specify { expect(subject).not_to be_renderable }
+ end
+ end
+
+ context 'when the resource is in client mode' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) }
+ let(:resource) { instance_double(Scim::Kit::V2::Resource) }
+
+ before do
+ allow(resource).to receive(:mode?).with(:server).and_return(false)
+ allow(resource).to receive(:mode?).with(:client).and_return(true)
+ end
+
+ context 'when the type is read only' do
+ before { type.mutability = :read_only }
+
+ specify { expect(subject).not_to be_renderable }
+ end
+
+ context 'when the type is write only' do
+ before { type.mutability = :write_only }
+
+ specify do
+ subject._value = 'hello'
+ expect(subject).to be_renderable
+ end
+
+ specify do
+ subject._value = nil
+ expect(subject).not_to be_renderable
+ end
+ end
+ end
end
spec/scim/kit/v2/resource_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Scim::Kit::V2::Resource do
describe '#as_json' do
specify { expect(subject.as_json[:schemas]).to match_array([schema.id]) }
specify { expect(subject.as_json[:id]).to eql(id) }
- specify { expect(subject.as_json[:externalId]).to eql(external_id) }
+ specify { expect(subject.as_json[:externalId]).to be_nil } # only render in client mode
specify { expect(subject.as_json[:meta][:resourceType]).to eql('User') }
specify { expect(subject.as_json[:meta][:location]).to eql(resource_location) }
specify { expect(subject.as_json[:meta][:created]).to eql(created_at.iso8601) }
@@ -159,11 +159,160 @@ RSpec.describe Scim::Kit::V2::Resource do
end
end
- context 'when submitting new record' do
+ context 'when building a new resource' do
subject { described_class.new(schemas: schemas) }
+ before do
+ schema.add_attribute(name: 'userName') do |attribute|
+ attribute.required = true
+ attribute.uniqueness = :server
+ end
+ schema.add_attribute(name: 'name') do |attribute|
+ attribute.add_attribute(name: 'formatted') do |x|
+ x.mutability = :read_only
+ end
+ attribute.add_attribute(name: 'familyName')
+ attribute.add_attribute(name: 'givenName')
+ end
+ schema.add_attribute(name: 'displayName') do |attribute|
+ attribute.mutability = :read_only
+ end
+ schema.add_attribute(name: 'locale')
+ schema.add_attribute(name: 'timezone')
+ schema.add_attribute(name: 'active', type: :boolean)
+ schema.add_attribute(name: 'password') do |attribute|
+ attribute.mutability = :write_only
+ attribute.returned = :never
+ end
+ schema.add_attribute(name: 'emails') do |attribute|
+ attribute.multi_valued = true
+ attribute.add_attribute(name: 'value')
+ attribute.add_attribute(name: 'primary', type: :boolean)
+ end
+ schema.add_attribute(name: 'groups') do |attribute|
+ attribute.multi_valued = true
+ attribute.mutability = :read_only
+ attribute.add_attribute(name: 'value') do |x|
+ x.mutability = :read_only
+ end
+ attribute.add_attribute(name: '$ref') do |x|
+ x.reference_types = %w[User Group]
+ x.mutability = :read_only
+ end
+ attribute.add_attribute(name: 'display') do |x|
+ x.mutability = :read_only
+ end
+ end
+ end
+
specify { expect(subject.as_json.key?(:meta)).to be(false) }
specify { expect(subject.as_json.key?(:id)).to be(false) }
specify { expect(subject.as_json.key?(:externalId)).to be(false) }
+
+ context 'when using a simplified API' do
+ let(:user_name) { FFaker::Internet.user_name }
+ let(:resource) do
+ described_class.new(schemas: schemas) do |x|
+ x.user_name = user_name
+ x.name.given_name = 'Barbara'
+ x.name.family_name = 'Jensen'
+ x.emails = [
+ { value: FFaker::Internet.email, primary: true },
+ { value: FFaker::Internet.email, primary: false }
+ ]
+ x.locale = 'en'
+ x.timezone = 'Etc/UTC'
+ end
+ end
+
+ specify { expect(resource.user_name).to eql(user_name) }
+ specify { expect(resource.name.given_name).to eql('Barbara') }
+ specify { expect(resource.name.family_name).to eql('Jensen') }
+ specify { expect(resource.emails[0][:value]).to be_present }
+ specify { expect(resource.emails[0][:primary]).to be(true) }
+ specify { expect(resource.emails[1][:value]).to be_present }
+ specify { expect(resource.emails[1][:primary]).to be(false) }
+ specify { expect(resource.locale).to eql('en') }
+ specify { expect(resource.timezone).to eql('Etc/UTC') }
+
+ specify { expect(resource.to_h[:userName]).to eql(user_name) }
+ specify { expect(resource.to_h[:name][:givenName]).to eql('Barbara') }
+ specify { expect(resource.to_h[:name][:familyName]).to eql('Jensen') }
+ specify { expect(resource.to_h[:emails][0][:value]).to be_present }
+ specify { expect(resource.to_h[:emails][0][:primary]).to be(true) }
+ specify { expect(resource.to_h[:emails][1][:value]).to be_present }
+ specify { expect(resource.to_h[:emails][1][:primary]).to be(false) }
+ specify { expect(resource.to_h[:locale]).to eql('en') }
+ specify { expect(resource.to_h[:timezone]).to eql('Etc/UTC') }
+ specify { expect(resource.to_h.key?(:meta)).to be(false) }
+ specify { expect(resource.to_h.key?(:id)).to be(false) }
+ specify { expect(resource.to_h.key?(:external_id)).to be(false) }
+ end
+
+ context 'when building in client mode' do
+ subject { described_class.new(schemas: schemas) }
+
+ let(:external_id) { SecureRandom.uuid }
+
+ before do
+ subject.password = FFaker::Internet.password
+ subject.external_id = external_id
+ end
+
+ specify { expect(subject.to_h.key?(:id)).to be(false) }
+ specify { expect(subject.to_h.key?(:externalId)).to be(true) }
+ specify { expect(subject.to_h[:externalId]).to eql(external_id) }
+ specify { expect(subject.to_h.key?(:meta)).to be(false) }
+ specify { expect(subject.to_h.key?(:userName)).to be(true) }
+ specify { expect(subject.to_h[:name].key?(:formatted)).to be(false) }
+ specify { expect(subject.to_h[:name].key?(:familyName)).to be(true) }
+ specify { expect(subject.to_h[:name].key?(:givenName)).to be(true) }
+ specify { expect(subject.to_h.key?(:displayName)).to be(false) }
+ specify { expect(subject.to_h.key?(:locale)).to be(true) }
+ specify { expect(subject.to_h.key?(:timezone)).to be(true) }
+ specify { expect(subject.to_h.key?(:active)).to be(true) }
+ specify { expect(subject.to_h.key?(:password)).to be(true) }
+ specify { expect(subject.to_h.key?(:emails)).to be(true) }
+ specify { expect(subject.to_h.key?(:groups)).to be(false) }
+ end
+
+ context 'when building in server mode' do
+ subject { described_class.new(schemas: schemas, location: resource_location) }
+
+ before do
+ subject.external_id = SecureRandom.uuid
+ end
+
+ specify { expect(subject.to_h.key?(:id)).to be(true) }
+ specify { expect(subject.to_h.key?(:externalId)).to be(false) }
+ specify { expect(subject.to_h.key?(:meta)).to be(true) }
+ specify { expect(subject.to_h.key?(:userName)).to be(true) }
+ specify { expect(subject.to_h[:name].key?(:formatted)).to be(true) }
+ specify { expect(subject.to_h[:name].key?(:familyName)).to be(true) }
+ specify { expect(subject.to_h[:name].key?(:givenName)).to be(true) }
+ specify { expect(subject.to_h.key?(:displayName)).to be(true) }
+ specify { expect(subject.to_h.key?(:locale)).to be(true) }
+ specify { expect(subject.to_h.key?(:timezone)).to be(true) }
+ specify { expect(subject.to_h.key?(:active)).to be(true) }
+ specify { expect(subject.to_h.key?(:password)).to be(false) }
+ specify { expect(subject.to_h.key?(:emails)).to be(true) }
+ specify { expect(subject.to_h.key?(:groups)).to be(true) }
+ end
+ end
+
+ describe '#mode?' do
+ context 'when server mode' do
+ subject { described_class.new(schemas: schemas, location: resource_location) }
+
+ specify { expect(subject).to be_mode(:server) }
+ specify { expect(subject).not_to be_mode(:client) }
+ end
+
+ context 'when client mode' do
+ subject { described_class.new(schemas: schemas) }
+
+ specify { expect(subject).not_to be_mode(:server) }
+ specify { expect(subject).to be_mode(:client) }
+ end
end
end