Comparing changes

v0.2.9 v0.2.10
13 commits 10 files changed

Commits

4c057c0 bump version mokha 2019-01-12 17:51:37
e4a620d default multi_valued to array mokha 2019-01-12 17:45:42
0ab2480 fix linter errors mokha 2019-01-12 17:41:21
7bdb077 fix linter errors mokha 2019-01-12 17:23:28
a67f385 attach resource to each attribute mokha 2019-01-12 16:58:06
0268a94 add specs for id and externalId mokha 2019-01-12 16:47:00
89213cb ensure proper hash/json generation mokha 2019-01-12 16:21:57
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