Comparing changes
v0.2.1
→
v0.2.2
24 commits
22 files changed
Commits
Changed files (22)
lib
spec
fixtures
bin/console
@@ -2,7 +2,7 @@
# frozen_string_literal: true
require 'bundler/setup'
-require 'saml/kit'
+require 'scim/kit'
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
bin/setup
@@ -3,4 +3,4 @@ set -euo pipefail
IFS=$'\n\t'
set -vx
-bundle check || bundle install --jobs $(nproc)
+bundle check || bundle install --jobs "$(sysctl -n hw.ncpu || nproc)"
lib/scim/kit/v2/templates/attribute.json.jbuilder
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+json.key_format! camelize: :lower
+if type.complex? && !type.multi_valued
+ json.set! type.name do
+ dynamic_attributes.values.each do |attribute|
+ render attribute, json: json
+ end
+ end
+else
+ json.set! type.name, _value
+end
lib/scim/kit/v2/templates/meta.json.jbuilder
@@ -5,4 +5,4 @@ json.location location
json.resource_type resource_type
json.created created.iso8601 if created
json.last_modified last_modified.iso8601 if last_modified
-json.version version.to_i if version
+json.version version if version
lib/scim/kit/v2/templates/resource.json.jbuilder
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+json.key_format! camelize: :lower
+json.schemas schemas.map(&:id)
+json.id id
+json.external_id external_id
+json.meta do
+ render meta, json: json
+end
+schemas.each do |schema|
+ if schema.core?
+ schema.attributes.each do |type|
+ render dynamic_attributes[type.name], json: json
+ end
+ else
+ json.set! schema.id do
+ schema.attributes.each do |type|
+ render dynamic_attributes[type.name], json: json
+ end
+ end
+ end
+end
lib/scim/kit/v2/attributable.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module V2
+ # Represents a dynamic attribute that corresponds to a SCIM type
+ module Attributable
+ attr_reader :dynamic_attributes
+
+ def define_attributes_for(types)
+ @dynamic_attributes = {}.with_indifferent_access
+ types.each do |type|
+ dynamic_attributes[type.name] = Attribute.new(type: type)
+ extend(create_module_for(type))
+ end
+ end
+
+ private
+
+ def attribute_for(name)
+ dynamic_attributes[name]
+ end
+
+ def read_attribute(name)
+ attribute = attribute_for(name)
+ return attribute._value if attribute.type.multi_valued
+
+ attribute.type.complex? ? attribute : attribute._value
+ end
+
+ def write_attribute(name, value)
+ attribute_for(name)._value = value
+ end
+
+ def create_module_for(type)
+ name = type.name.to_sym
+ Module.new do
+ define_method(name) do |*_args|
+ read_attribute(name)
+ end
+
+ define_method("#{name}=") do |*args|
+ write_attribute(name, args[0])
+ end
+ end
+ end
+ end
+ end
+ end
+end
lib/scim/kit/v2/attribute.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module V2
+ # Represents a SCIM Attribute
+ class Attribute
+ include Attributable
+ include Templatable
+ attr_reader :type
+ attr_reader :_value
+
+ def initialize(type:, value: nil)
+ @type = type
+ @_value = value
+ define_attributes_for(type.attributes)
+ end
+
+ def _value=(new_value)
+ @_value = type.coerce(new_value)
+
+ if type.canonical_values &&
+ !type.canonical_values.empty? &&
+ !type.canonical_values.include?(new_value)
+ raise ArgumentError, new_value
+ end
+ end
+ end
+ end
+ end
+end
lib/scim/kit/v2/attribute_type.rb
@@ -16,6 +16,13 @@ module Scim
reference: 'reference',
complex: 'complex'
}.freeze
+ COERCION = {
+ string: ->(x) { x.to_s },
+ decimal: ->(x) { x.to_f },
+ integer: ->(x) { x.to_i },
+ datetime: ->(x) { x.is_a?(::String) ? DateTime.parse(x) : x },
+ binary: ->(x) { Base64.strict_encode64(x) }
+ }.freeze
attr_accessor :canonical_values
attr_accessor :case_exact
attr_accessor :description
@@ -28,8 +35,8 @@ module Scim
attr_reader :uniqueness
def initialize(name:, type: :string)
- @name = name
- @type = type
+ @name = name.to_s.underscore
+ @type = type.to_sym
@description = ''
@multi_valued = false
@required = false
@@ -37,7 +44,7 @@ module Scim
@mutability = Mutability::READ_WRITE
@returned = Returned::DEFAULT
@uniqueness = Uniqueness::NONE
- raise ArgumentError, :type unless DATATYPES[type.to_sym]
+ raise ArgumentError, :type unless DATATYPES[@type]
end
def mutability=(value)
@@ -53,9 +60,9 @@ module Scim
end
def add_attribute(name:, type: :string)
- @type = :complex
attribute = AttributeType.new(name: name, type: type)
yield attribute if block_given?
+ @type = :complex
attributes << attribute
end
@@ -64,8 +71,6 @@ module Scim
@reference_types = value
end
- private
-
def attributes
@attributes ||= []
end
@@ -74,6 +79,18 @@ module Scim
type_is?(:complex)
end
+ def coerce(value)
+ if type_is?(:boolean) && ![true, false].include?(value)
+ raise ArgumentError, value
+ end
+ return value if multi_valued
+
+ coercion = COERCION[type]
+ coercion ? coercion.call(value) : value
+ end
+
+ private
+
def string?
type_is?(:string)
end
lib/scim/kit/v2/meta.rb
@@ -14,7 +14,8 @@ module Scim
def initialize(resource_type, location)
@resource_type = resource_type
@location = location
- @version = @created = @last_modified = Time.now
+ @created = @last_modified = Time.now
+ @version = @created.to_i
end
end
end
lib/scim/kit/v2/resource.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Scim
+ module Kit
+ module V2
+ # Represents a SCIM Resource
+ class Resource
+ include Attributable
+ include Templatable
+
+ attr_accessor :id, :external_id
+ attr_reader :meta
+ attr_reader :schemas
+
+ def initialize(schemas:, location:)
+ @meta = Meta.new(schemas[0].name, location)
+ @schemas = schemas
+ schemas.each do |schema|
+ define_attributes_for(schema.attributes)
+ end
+ end
+ end
+ end
+ end
+end
lib/scim/kit/v2/schema.rb
@@ -20,7 +20,11 @@ module Scim
def add_attribute(name:, type: :string)
attribute = AttributeType.new(name: name, type: type)
yield attribute if block_given?
- @attributes << attribute
+ attributes << attribute
+ end
+
+ def core?
+ id.include?(Schemas::CORE)
end
def self.build(*args)
lib/scim/kit/dynamic_attributes.rb
@@ -2,7 +2,7 @@
module Scim
module Kit
- # Allows dynamic assignment of attributes.
+ # Allows dynamic creation of attributes.
module DynamicAttributes
def method_missing(method, *args)
return super unless respond_to_missing?(method)
lib/scim/kit/templatable.rb
@@ -13,7 +13,7 @@ module Scim
end
def to_h
- JSON.parse(to_json, symbolize_names: true)
+ JSON.parse(to_json, symbolize_names: true).with_indifferent_access
end
def render(model, options)
lib/scim/kit/version.rb
@@ -2,6 +2,6 @@
module Scim
module Kit
- VERSION = '0.2.1'
+ VERSION = '0.2.2'
end
end
lib/scim/kit.rb
@@ -2,17 +2,21 @@
require 'tilt'
require 'tilt/jbuilder'
+require 'active_support/core_ext/hash/indifferent_access'
require 'scim/kit/dynamic_attributes'
require 'scim/kit/templatable'
require 'scim/kit/template'
require 'scim/kit/version'
+require 'scim/kit/v2/attributable'
+require 'scim/kit/v2/attribute'
require 'scim/kit/v2/attribute_type'
require 'scim/kit/v2/authentication_scheme'
require 'scim/kit/v2/messages'
require 'scim/kit/v2/meta'
require 'scim/kit/v2/mutability'
+require 'scim/kit/v2/resource'
require 'scim/kit/v2/resource_type'
require 'scim/kit/v2/returned'
require 'scim/kit/v2/schema'
spec/fixtures/avatar.png
Binary file
spec/scim/kit/v2/attribute_spec.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+RSpec.describe Scim::Kit::V2::Attribute do
+ subject { described_class.new(type: type) }
+
+ context 'with strings' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'userName', type: :string) }
+
+ context 'when valid' do
+ let(:user_name) { FFaker::Internet.user_name }
+
+ before { subject._value = user_name }
+
+ specify { expect(subject._value).to eql(user_name) }
+ specify { expect(subject.as_json[:userName]).to eql(user_name) }
+ end
+
+ context 'when multiple values are allowed' do
+ before do
+ type.multi_valued = true
+ subject._value = %w[superman batman]
+ end
+
+ specify { expect(subject._value).to match_array(%w[superman batman]) }
+ end
+
+ context 'when integer' do
+ let(:number) { rand(100) }
+
+ before { subject._value = number }
+
+ specify { expect(subject._value).to eql(number.to_s) }
+ end
+
+ context 'when datetime' do
+ let(:datetime) { DateTime.now }
+
+ before { subject._value = datetime }
+
+ specify { expect(subject._value).to eql(datetime.to_s) }
+ end
+
+ context 'when not matching a canonical value' do
+ before { type.canonical_values = %w[batman robin] }
+
+ specify { expect { subject._value = 'spider man' }.to raise_error(ArgumentError) }
+ end
+
+ context 'when canonical value is given' do
+ before do
+ type.canonical_values = %w[batman robin]
+ subject._value = 'batman'
+ end
+
+ specify { expect(subject._value).to eql('batman') }
+ end
+ end
+
+ context 'with boolean' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'hungry', type: :boolean) }
+
+ context 'when true' do
+ before { subject._value = true }
+
+ specify { expect(subject._value).to be(true) }
+ specify { expect(subject.as_json[:hungry]).to be(true) }
+ end
+
+ context 'when false' do
+ before { subject._value = false }
+
+ specify { expect(subject._value).to be(false) }
+ specify { expect(subject.as_json[:hungry]).to be(false) }
+ end
+
+ context 'when string' do
+ specify { expect { subject._value = 'hello' }.to raise_error(ArgumentError) }
+ end
+ end
+
+ context 'with decimal' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'measurement', type: :decimal) }
+
+ context 'when given float' do
+ before { subject._value = Math::PI }
+
+ specify { expect(subject._value).to eql(Math::PI) }
+ specify { expect(subject.as_json[:measurement]).to be(Math::PI) }
+ end
+
+ context 'when given an integer' do
+ before { subject._value = 42 }
+
+ specify { expect(subject._value).to eql(42.to_f) }
+ specify { expect(subject.as_json[:measurement]).to be(42.to_f) }
+ end
+ end
+
+ context 'with integer' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'age', type: :integer) }
+
+ context 'when given integer' do
+ before { subject._value = 34 }
+
+ specify { expect(subject._value).to be(34) }
+ specify { expect(subject.as_json[:age]).to be(34) }
+ end
+
+ context 'when given float' do
+ before { subject._value = Math::PI }
+
+ specify { expect(subject._value).to eql(Math::PI.to_i) }
+ end
+ end
+
+ context 'with datetime' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'birthdate', type: :datetime) }
+ let(:datetime) { DateTime.new(2019, 0o1, 0o6, 12, 35, 0o0) }
+
+ context 'when given a date time' do
+ before { subject._value = datetime }
+
+ specify { expect(subject._value).to eql(datetime) }
+ specify { expect(subject.as_json[:birthdate]).to eql(datetime.iso8601) }
+ end
+
+ context 'when given a string' do
+ before { subject._value = datetime.to_s }
+
+ specify { expect(subject._value).to eql(datetime) }
+ end
+ end
+
+ context 'with binary' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'photo', type: :binary) }
+ let(:photo) { IO.read('./spec/fixtures/avatar.png', mode: 'rb') }
+
+ context 'when given a .png' do
+ before { subject._value = photo }
+
+ specify { expect(subject._value).to eql(Base64.strict_encode64(photo)) }
+ specify { expect(subject.as_json[:photo]).to eql(Base64.strict_encode64(photo)) }
+ end
+ end
+
+ context 'with reference' do
+ let(:type) { Scim::Kit::V2::AttributeType.new(name: 'group', type: :reference) }
+ let(:uri) { FFaker::Internet.uri('https') }
+
+ before { subject._value = uri }
+
+ specify { expect(subject._value).to eql(uri) }
+ specify { expect(subject.as_json[:group]).to eql(uri) }
+ end
+
+ context 'with complex type' do
+ let(:type) do
+ x = Scim::Kit::V2::AttributeType.new(name: 'name', type: :complex)
+ x.add_attribute(name: 'familyName')
+ x.add_attribute(name: 'givenName')
+ x
+ end
+
+ before do
+ subject.family_name = 'Garrett'
+ subject.given_name = 'Tsuyoshi'
+ end
+
+ specify { expect(subject.family_name).to eql('Garrett') }
+ specify { expect(subject.given_name).to eql('Tsuyoshi') }
+ specify { expect(subject.as_json[:name][:familyName]).to eql('Garrett') }
+ specify { expect(subject.as_json[:name][:givenName]).to eql('Tsuyoshi') }
+ end
+
+ context 'with multi valued complex type' do
+ let(:type) do
+ x = Scim::Kit::V2::AttributeType.new(name: 'emails', type: :complex)
+ x.multi_valued = true
+ x.add_attribute(name: 'value')
+ x.add_attribute(name: 'primary', type: :boolean)
+ x
+ end
+ let(:email) { FFaker::Internet.email }
+ let(:other_email) { FFaker::Internet.email }
+
+ before do
+ subject._value = [
+ { value: email, primary: true },
+ { value: other_email, primary: false }
+ ]
+ end
+
+ specify { expect(subject._value).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) }
+ specify { expect(subject.as_json[:emails]).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) }
+ end
+end
spec/scim/kit/v2/attribute_type_spec.rb
@@ -15,9 +15,9 @@ RSpec.describe Scim::Kit::V2::AttributeType do
describe 'defaults' do
subject { described_class.new(name: 'displayName') }
- specify { expect(subject.name).to eql('displayName') }
+ specify { expect(subject.name).to eql('display_name') }
specify { expect(subject.type).to be(:string) }
- specify { expect(subject.to_h[:name]).to eql('displayName') }
+ specify { expect(subject.to_h[:name]).to eql('display_name') }
specify { expect(subject.to_h[:type]).to eql('string') }
specify { expect(subject.to_h[:multiValued]).to be(false) }
specify { expect(subject.to_h[:description]).to eql('') }
spec/scim/kit/v2/resource_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+RSpec.describe Scim::Kit::V2::Resource do
+ subject { described_class.new(schemas: schemas, location: resource_location) }
+
+ let(:schemas) { [schema] }
+ let(:schema) { Scim::Kit::V2::Schema.new(id: Scim::Kit::V2::Schemas::USER, name: 'User', location: FFaker::Internet.uri('https')) }
+ let(:resource_location) { FFaker::Internet.uri('https') }
+
+ context 'with common attributes' do
+ let(:id) { SecureRandom.uuid }
+ let(:external_id) { SecureRandom.uuid }
+ let(:created_at) { Time.now }
+ let(:updated_at) { Time.now }
+ let(:version) { SecureRandom.uuid }
+
+ before do
+ subject.id = id
+ subject.external_id = external_id
+ subject.meta.created = created_at
+ subject.meta.last_modified = updated_at
+ subject.meta.version = version
+ end
+
+ specify { expect(subject.id).to eql(id) }
+ specify { expect(subject.external_id).to eql(external_id) }
+ specify { expect(subject.meta.resource_type).to eql('User') }
+ specify { expect(subject.meta.location).to eql(resource_location) }
+ specify { expect(subject.meta.created).to eql(created_at) }
+ specify { expect(subject.meta.last_modified).to eql(updated_at) }
+ specify { expect(subject.meta.version).to eql(version) }
+
+ 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[: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) }
+ specify { expect(subject.as_json[:meta][:lastModified]).to eql(updated_at.iso8601) }
+ specify { expect(subject.as_json[:meta][:version]).to eql(version) }
+ end
+ end
+
+ context 'with custom string attribute' do
+ let(:user_name) { FFaker::Internet.user_name }
+
+ before do
+ schema.add_attribute(name: 'userName')
+ subject.user_name = user_name
+ end
+
+ specify { expect(subject.user_name).to eql(user_name) }
+ end
+
+ context 'with attribute named "type"' do
+ before do
+ schema.add_attribute(name: 'type')
+ subject.type = 'User'
+ end
+
+ specify { expect(subject.type).to eql('User') }
+ specify { expect(subject.as_json[:type]).to eql('User') }
+ specify { expect(subject.send(:attribute_for, :type).type).to be_instance_of(Scim::Kit::V2::AttributeType) }
+ end
+
+ context 'with a complex attribute' do
+ before do
+ schema.add_attribute(name: 'name') do |x|
+ x.add_attribute(name: 'familyName')
+ x.add_attribute(name: 'givenName')
+ end
+ subject.name.family_name = 'Garrett'
+ subject.name.given_name = 'Tsuyoshi'
+ end
+
+ specify { expect(subject.name.family_name).to eql('Garrett') }
+ specify { expect(subject.name.given_name).to eql('Tsuyoshi') }
+
+ describe '#as_json' do
+ specify { expect(subject.as_json[:name][:familyName]).to eql('Garrett') }
+ specify { expect(subject.as_json[:name][:givenName]).to eql('Tsuyoshi') }
+ end
+ end
+
+ context 'with a complex multi valued attribute' do
+ let(:email) { FFaker::Internet.email }
+ let(:other_email) { FFaker::Internet.email }
+
+ before do
+ schema.add_attribute(name: 'emails', type: :complex) do |x|
+ x.multi_valued = true
+ x.add_attribute(name: 'value')
+ x.add_attribute(name: 'primary', type: :boolean)
+ end
+ subject.emails = [
+ { value: email, primary: true },
+ { value: other_email, primary: false }
+ ]
+ end
+
+ specify { expect(subject.emails).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) }
+ specify { expect(subject.as_json[:emails]).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) }
+ end
+
+ context 'with multiple schemas' do
+ let(:schemas) { [schema, extension] }
+ let(:extension) { Scim::Kit::V2::Schema.new(id: extension_id, name: 'Extension', location: FFaker::Internet.uri('https')) }
+ let(:extension_id) { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' }
+
+ before do
+ extension.add_attribute(name: :department)
+ subject.department = 'voltron'
+ end
+
+ specify { expect(subject.department).to eql('voltron') }
+ specify { expect(subject.as_json[extension_id][:department]).to eql('voltron') }
+ end
+end
spec/scim/kit/v2/schema_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Scim::Kit::V2::Schema do
subject.add_attribute(name: 'displayName')
end
- specify { expect(result[:attributes][0][:name]).to eql('displayName') }
+ specify { expect(result[:attributes][0][:name]).to eql('display_name') }
specify { expect(result[:attributes][0][:type]).to eql('string') }
specify { expect(result[:attributes][0][:multiValued]).to be(false) }
specify { expect(result[:attributes][0][:description]).to eql('') }
.gitlab-ci.yml
@@ -0,0 +1,11 @@
+image: ruby:2.6
+
+before_script:
+ - apt-get update && apt-get install -y locales
+ - echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
+ - locale-gen
+ - export LC_ALL=en_US.UTF-8
+
+rspec:
+ script:
+ - bin/cibuild
README.md
@@ -83,7 +83,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
## Contributing
-Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/scim-kit.
+Bug reports and pull requests are welcome on GitHub at https://github.com/mokha/scim-kit.
## License