Comparing changes
v0.3.2
→
v0.4.0
54 commits
11 files changed
Commits
8881c9b
filter and logical_expression rules are not consuming before calling each other. Therefore the non-stopping recursion overflows the stack.
2019-05-15 05:49:31
Changed files (11)
lib
scim
kit
spec
scim
kit
lib/scim/kit/v2/filter.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'parslet'
+
+module Scim
+ module Kit
+ module V2
+ # Parses SCIM filter queries
+ class Filter < Parslet::Parser
+ root :filter
+
+ # FILTER = attrExp / logExp / valuePath / assignVariable / *1"not" "(" FILTER ")"
+ rule(:filter) do
+ logical_expression | filter_atom
+ end
+
+ rule(:filter_atom) do
+ (not_op? >> lparen >> filter >> rparen) | attribute_expression | value_path
+ end
+
+ # valuePath = attrPath "[" valFilter "]" ; FILTER uses sub-attributes of a parent attrPath
+ rule(:value_path) do
+ attribute_path >> lbracket >> value_filter >> rbracket
+ end
+
+ # valFilter = attrExp / logExp / *1"not" "(" valFilter ")"
+ rule(:value_filter) do
+ attribute_expression | logical_expression | not_op? >> lparen >> value_filter >> rparen
+ end
+
+ # attrExp = (attrPath SP "pr") / (attrPath SP compareOp SP compValue)
+ rule(:attribute_expression) do
+ (attribute_path >> space >> presence.as(:operator)) | (attribute_path >> space >> comparison_operator.as(:operator) >> space >> comparison_value.as(:value))
+ end
+
+ # logExp = FILTER SP ("and" / "or") SP FILTER
+ rule(:logical_expression) do
+ filter_atom.as(:left) >> space >> (and_op | or_op).as(:operator) >> space >> filter.as(:right)
+ end
+
+ # compValue = false / null / true / number / string ; rules from JSON (RFC 7159)
+ rule(:comparison_value) do
+ falsey | null | truthy | number | string
+ end
+
+ # compareOp = "eq" / "ne" / "co" / "sw" / "ew" / "gt" / "lt" / "ge" / "le"
+ rule(:comparison_operator) do
+ equal | not_equal | contains | starts_with | ends_with |
+ greater_than | less_than | less_than_equals | greater_than_equals
+ end
+
+ # attrPath = [URI ":"] ATTRNAME *1subAttr ; SCIM attribute name ; URI is SCIM "schema" URI
+ rule(:attribute_path) do
+ ((uri >> colon).repeat(0, 1) >> attribute_name >> sub_attribute.repeat(0, 1)).as(:attribute)
+ end
+
+ # ATTRNAME = ALPHA *(nameChar)
+ rule(:attribute_name) do
+ alpha >> name_character.repeat(0, nil)
+ end
+
+ # nameChar = "-" / "_" / DIGIT / ALPHA
+ rule(:name_character) { hyphen | underscore | digit | alpha }
+
+ # subAttr = "." ATTRNAME ; a sub-attribute of a complex attribute
+ rule(:sub_attribute) { dot >> attribute_name }
+
+ # uri = 1*ALPHA 1*(":" 1*ALPHA)
+ rule(:uri) do
+ # alpha.repeat(1, nil) >> (colon >> (alpha.repeat(1, nil) | version)).repeat(1, nil)
+ str('urn:ietf:params:scim:schemas:') >> (
+ str('core:2.0:User') |
+ str('core:2.0:Group') | (
+ str('extension') >>
+ colon >>
+ alpha.repeat(1) >>
+ colon >>
+ version >>
+ colon >>
+ alpha.repeat(1)
+ )
+ )
+ end
+
+ rule(:presence) { str('pr') }
+ rule(:and_op) { str('and') }
+ rule(:or_op) { str('or') }
+ rule(:not_op?) { (str('not') >> space).repeat(0, 1).as(:not) }
+ rule(:falsey) { str('false') }
+ rule(:truthy) { str('true') }
+ rule(:null) { str('null') }
+ rule(:number) do
+ str('-').maybe >> (
+ str('0') | (match('[1-9]') >> digit.repeat)
+ ) >> (
+ str('.') >> digit.repeat(1)
+ ).maybe >> (
+ match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
+ ).maybe
+ end
+ rule(:equal) { str('eq') }
+ rule(:not_equal) { str('ne') }
+ rule(:contains) { str('co') }
+ rule(:starts_with) { str('sw') }
+ rule(:ends_with) { str('ew') }
+ rule(:greater_than) { str('gt') }
+ rule(:less_than) { str('lt') }
+ rule(:greater_than_equals) { str('ge') }
+ rule(:less_than_equals) { str('le') }
+ rule(:string) do
+ quote >> (str('\\') >> any | str('"').absent? >> any).repeat >> quote
+ end
+ rule(:lparen) { str('(') }
+ rule(:rparen) { str(')') }
+ rule(:lbracket) { str('[') }
+ rule(:rbracket) { str(']') }
+ rule(:digit) { match('\d') }
+ rule(:quote) { str('"') }
+ rule(:single_quote) { str("'") }
+ rule(:space) { match('\s') }
+ rule(:alpha) { match['a-zA-Z'] }
+ rule(:dot) { str('.') }
+ rule(:colon) { str(':') }
+ rule(:hyphen) { str('-') }
+ rule(:underscore) { str('_') }
+ rule(:version) { digit >> dot >> digit }
+ rule(:assign) { str('=') }
+ end
+ end
+ end
+end
lib/scim/kit/v2.rb
@@ -11,6 +11,7 @@ require 'scim/kit/v2/meta'
require 'scim/kit/v2/mutability'
require 'scim/kit/v2/resource'
require 'scim/kit/v2/error'
+require 'scim/kit/v2/filter'
require 'scim/kit/v2/resource_type'
require 'scim/kit/v2/returned'
require 'scim/kit/v2/schema'
lib/scim/kit/version.rb
@@ -2,6 +2,6 @@
module Scim
module Kit
- VERSION = '0.3.2'
+ VERSION = '0.4.0'
end
end
spec/scim/kit/v2/filter_spec.rb
@@ -0,0 +1,174 @@
+# frozen_string_literal: true
+
+RSpec.describe Scim::Kit::V2::Filter do
+ subject { described_class.new }
+
+ [
+ 'userName',
+ 'name.familyName',
+ 'urn:ietf:params:scim:schemas:core:2.0:User:userName',
+ 'meta.lastModified',
+ 'schemas'
+ ].each do |attribute|
+ %w[
+ eq
+ ne
+ co
+ sw
+ ew
+ gt
+ lt
+ ge
+ le
+ ].each do |operator|
+ [
+ 'bjensen',
+ "O'Malley",
+ 'J',
+ '2011-05-13T04:42:34Z',
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'
+ ].each do |value|
+ specify { expect(subject.parse_with_debug(%(#{attribute} #{operator} \"#{value}\"))).to be_truthy }
+ end
+ end
+ end
+
+ specify { expect(subject.parse_with_debug('userName eq "jeramy@ziemann.biz"')).to be_truthy }
+ specify { expect(subject.parse_with_debug(%((title pr) and (userType eq "Employee")))).to be_truthy }
+ specify { expect(subject.attribute_expression.parse_with_debug(%(title pr and userType eq "Employee"))).not_to be_truthy }
+ specify { expect(subject.logical_expression.parse_with_debug(%((title pr) and (userType eq "Employee")))).to be_truthy }
+ specify { expect(subject.value_path.parse_with_debug(%(title pr and userType eq "Employee"))).not_to be_truthy }
+
+ [
+ 'emails[(type eq "work") and (value co "@example.com")]'
+ ].each do |x|
+ specify { expect(subject.value_path.parse_with_debug(x)).to be_truthy }
+ end
+
+ [
+ '(firstName eq "Tsuyoshi") and (lastName eq "Garret")',
+ '(type eq "work") and (value co "@example.com")',
+ 'firstName eq "Tsuyoshi"',
+ 'firstName pr'
+ ].each do |x|
+ specify { expect(subject.value_filter).to parse(x) }
+ end
+
+ [
+ 'firstName eq "Tsuyoshi"',
+ 'firstName pr'
+ ].each do |x|
+ specify { expect(subject.attribute_expression).to parse(x) }
+ end
+
+ [
+ '(firstName eq "Tsuyoshi") and (lastName eq "Garret")',
+ '(firstName eq "Tsuyoshi") or (lastName eq "Garret")',
+ '(title pr) and (userType eq "Employee")',
+ '(title pr) or (userType eq "Employee")'
+ ].each do |x|
+ specify { expect(subject.logical_expression).to parse(x) }
+ end
+
+ ['false', 'null', 'true', '1', '"hello"', '"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"', '"Garrett"'].each do |x|
+ specify { expect(subject.comparison_value).to parse(x) }
+ end
+
+ %w[eq ne co sw ew gt lt ge le].each do |x|
+ specify { expect(subject.comparison_operator).to parse(x) }
+ end
+
+ [
+ 'userName',
+ 'user_name',
+ 'user-name',
+ 'username1',
+ 'meta.lastModified',
+ 'schemas',
+ 'name.familyName',
+ 'urn:ietf:params:scim:schemas:core:2.0:User:userName',
+ 'urn:ietf:params:scim:schemas:core:2.0:User:name.familyName'
+ ].each do |x|
+ specify { expect(subject.attribute_path.parse_with_debug(x)).to be_truthy }
+ end
+
+ [
+ 'userName',
+ 'user_name',
+ 'user-name',
+ 'username1',
+ 'schemas'
+ ].each do |x|
+ specify { expect(subject.attribute_name).to parse(x) }
+ end
+
+ ['-', '_', '0', 'a'].each { |x| specify { expect(subject.name_character).to parse(x) } }
+ specify { expect(subject.sub_attribute).to parse('.name') }
+ specify { expect(subject.presence).to parse('pr') }
+ specify { expect(subject.and_op).to parse('and') }
+ specify { expect(subject.or_op).to parse('or') }
+ specify { expect(subject.not_op?).to parse('not ') }
+ specify { expect(subject.not_op?).to parse('') }
+ specify { expect(subject.not_op?).not_to parse('not') }
+ specify { expect(subject.not_op?).not_to parse('not not') }
+ specify { expect(subject.falsey).to parse('false') }
+ specify { expect(subject.truthy).to parse('true') }
+ specify { expect(subject.null).to parse('null') }
+ 1.upto(100).each { |n| specify { expect(subject.number).to parse(n.to_s) } }
+
+ [
+ 'urn:ietf:params:scim:schemas:core:2.0:User',
+ 'urn:ietf:params:scim:schemas:core:2.0:Group',
+ 'urn:ietf:params:scim:schemas:extension:altean:2.0:User'
+ ].each do |x|
+ specify { expect(subject.uri).to parse(x) }
+ end
+
+ [
+ # '',
+ # %Q(userType eq "Employee" and emails[type eq "work" and value co "@example.com"]),
+ # %Q(emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]),
+ %(meta.lastModified ge "2011-05-13T04:42:34Z"),
+ %(meta.lastModified gt "2011-05-13T04:42:34Z"),
+ %(meta.lastModified le "2011-05-13T04:42:34Z"),
+ %(meta.lastModified lt "2011-05-13T04:42:34Z"),
+ %(name.familyName co "O'Malley"),
+ %(schemas eq "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"),
+ %(title pr and userType eq "Employee"),
+ %(title pr or userType eq "Intern"),
+ %(title pr),
+ %(urn:ietf:params:scim:schemas:core:2.0:User:userName sw "J"),
+ %(userName eq "bjensen"),
+ %(userName sw "J"),
+ %(userType eq "Employee" and (emails co "example.com" or emails.value co "example.org")),
+ %(userType eq "Employee" and (emails.type eq "work")),
+ %(userType ne "Employee" and not (emails co "example.com" or emails.value co "example.org")),
+ '(emails[(type eq "work") and (value co "@example.com")]) or (ims[(type eq "xmpp") and (value co "@foo.com")])',
+ '(title pr) and (userType eq "Employee")',
+ '(title pr) or (userType eq "Intern")',
+ '(userType eq "Employee") and (emails.type eq "work")',
+ '(userType eq "Employee") and (emails[(type eq "work") and (value co "@example.com")])',
+ 'title pr',
+ 'userName pr and not (userName eq "hello@example.com")'
+ ].each do |x|
+ specify { expect(subject.parse_with_debug(x)).to be_truthy }
+ end
+
+ specify { expect(subject.parse_with_debug('userName pr and not (userName eq "hello@example.com")')).to be_truthy }
+
+ [
+ '"Tsuyoshi"',
+ '"hello@example.org"',
+ '"2011-05-13T04:42:34Z"'
+ ].each do |x|
+ specify { expect(subject.string).to parse(x) }
+ end
+
+ specify { expect(subject.hyphen).to parse('-') }
+ specify { expect(subject.underscore).to parse('_') }
+ (0..9).each { |x| specify { expect(subject.digit).to parse(x.to_s) } }
+ [*'a'..'z', *'A'..'Z'].each { |x| specify { expect(subject.alpha).to parse(x) } }
+ specify { expect(subject.colon).to parse(':') }
+ specify { expect(subject.version).to parse('2.0') }
+ specify { expect(subject.version).to parse('1.0') }
+end
spec/spec_helper.rb
@@ -5,6 +5,8 @@ require 'scim/kit'
require 'ffaker'
require 'json'
require 'byebug'
+require 'parslet/convenience'
+require 'parslet/rig/rspec'
require 'webmock/rspec'
Scim::Kit.logger = Logger.new('/dev/null')
.gitignore
@@ -9,5 +9,4 @@
# rspec failure tracking
.rspec_status
-Gemfile.lock
.byebug_history
.rubocop.yml
@@ -9,7 +9,7 @@ AllCops:
- 'vendor/**/*'
TargetRubyVersion: 2.5
-Layout/IndentArray:
+Layout/IndentFirstArrayElement:
EnforcedStyle: consistent
Metrics/BlockLength:
@@ -27,6 +27,9 @@ Naming/FileName:
Exclude:
- 'lib/scim-kit.rb'
+Naming/RescuedExceptionsVariableName:
+ PreferredName: error
+
RSpec/NamedSubject:
Enabled: false
.travis.yml
@@ -3,6 +3,6 @@ sudo: false
language: ruby
cache: bundler
rvm:
- - 2.5.3
- - 2.6.1
+ - 2.5.5
+ - 2.6.3
before_install: gem install bundler -v 1.17.1
CHANGELOG.md
@@ -1,4 +1,5 @@
-Version 0.3.2
+Version 0.4.0
+
# Changelog
All notable changes to this project will be documented in this file.
@@ -6,7 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
-- NA
+- nil
+
+## [0.4.0] - 2019-06-15
+- add implementation of SCIM 2.0 filter parser. [RFC-7644](https://tools.ietf.org/html/rfc7644#section-3.4.2.2)
## [0.3.2] - 2019-02-23
### Changed
@@ -35,7 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- \_assign does not coerce values by default.
- errors are merged together instead of overwritten during attribute validation.
-[Unreleased]: https://github.com/mokhan/scim-kit/compare/v0.3.2...HEAD
+[Unreleased]: https://github.com/mokhan/scim-kit/compare/v0.4.0...HEAD
+[0.4.0]: https://github.com/mokhan/scim-kit/compare/v0.3.2...v0.4.0
[0.3.2]: https://github.com/mokhan/scim-kit/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/mokhan/scim-kit/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/mokhan/scim-kit/compare/v0.2.16...v0.3.0
Gemfile.lock
@@ -0,0 +1,102 @@
+PATH
+ remote: .
+ specs:
+ scim-kit (0.4.0)
+ activemodel (>= 5.2.0)
+ net-hippie (~> 0.2)
+ parslet (~> 1.8)
+ tilt (~> 2.0)
+ tilt-jbuilder (~> 0.7)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activemodel (5.2.3)
+ activesupport (= 5.2.3)
+ activesupport (5.2.3)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 0.7, < 2)
+ minitest (~> 5.1)
+ tzinfo (~> 1.1)
+ addressable (2.6.0)
+ public_suffix (>= 2.0.2, < 4.0)
+ ast (2.4.0)
+ bundler-audit (0.6.1)
+ bundler (>= 1.2.0, < 3)
+ thor (~> 0.18)
+ byebug (11.0.1)
+ concurrent-ruby (1.1.5)
+ crack (0.4.3)
+ safe_yaml (~> 1.0.0)
+ diff-lcs (1.3)
+ ffaker (2.11.0)
+ hashdiff (0.4.0)
+ i18n (1.6.0)
+ concurrent-ruby (~> 1.0)
+ jaro_winkler (1.5.2)
+ jbuilder (2.9.1)
+ activesupport (>= 4.2.0)
+ minitest (5.11.3)
+ net-hippie (0.2.6)
+ parallel (1.17.0)
+ parser (2.6.3.0)
+ ast (~> 2.4.0)
+ parslet (1.8.2)
+ public_suffix (3.1.0)
+ rainbow (3.0.0)
+ rake (10.5.0)
+ rspec (3.8.0)
+ rspec-core (~> 3.8.0)
+ rspec-expectations (~> 3.8.0)
+ rspec-mocks (~> 3.8.0)
+ rspec-core (3.8.1)
+ rspec-support (~> 3.8.0)
+ rspec-expectations (3.8.4)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.8.0)
+ rspec-mocks (3.8.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.8.0)
+ rspec-support (3.8.2)
+ rubocop (0.71.0)
+ jaro_winkler (~> 1.5.1)
+ parallel (~> 1.10)
+ parser (>= 2.6)
+ rainbow (>= 2.2.2, < 4.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 1.7)
+ rubocop-rspec (1.33.0)
+ rubocop (>= 0.60.0)
+ ruby-progressbar (1.10.1)
+ safe_yaml (1.0.5)
+ thor (0.20.3)
+ thread_safe (0.3.6)
+ tilt (2.0.9)
+ tilt-jbuilder (0.7.1)
+ jbuilder
+ tilt (>= 1.3.0, < 3)
+ tzinfo (1.2.5)
+ thread_safe (~> 0.1)
+ unicode-display_width (1.6.0)
+ webmock (3.6.0)
+ addressable (>= 2.3.6)
+ crack (>= 0.3.2)
+ hashdiff (>= 0.4.0, < 2.0.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bundler (~> 1.17)
+ bundler-audit (~> 0.6)
+ byebug
+ ffaker (~> 2.7)
+ rake (~> 10.0)
+ rspec (~> 3.0)
+ rubocop (~> 0.52)
+ rubocop-rspec (~> 1.22)
+ scim-kit!
+ webmock (~> 3.5)
+
+BUNDLED WITH
+ 1.17.3
scim-kit.gemspec
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
spec.authors = ['mo']
spec.email = ['mo@mokhan.ca']
- spec.summary = 'A SCIM library.'
- spec.description = 'A SCIM library.'
+ spec.summary = 'A simple toolkit for working with SCIM 2.0'
+ spec.description = 'A simple toolkit for working with SCIM 2.0'
spec.homepage = 'https://www.github.com/mokhan/scim-kit'
spec.license = 'MIT'
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'activemodel', '>= 5.2.0'
spec.add_dependency 'net-hippie', '~> 0.2'
+ spec.add_dependency 'parslet', '~> 1.8'
spec.add_dependency 'tilt', '~> 2.0'
spec.add_dependency 'tilt-jbuilder', '~> 0.7'
spec.add_development_dependency 'bundler', '~> 1.17'