Comparing changes

v0.3.2 v0.4.0
54 commits 11 files changed

Commits

c8aa1c3 update Gemfile.lock mo 2019-06-15 21:00:16
c0ead7f release 0.4.0 mo 2019-06-15 20:59:36
b2f124a update ruby versions mo 2019-06-15 20:54:39
1273451 update gems and description mo 2019-06-15 20:53:36
4c1562e update changelog entry mo 2019-06-15 20:51:11
1168b63 fix linter error mokha 2019-06-15 17:31:06
b3e3045 fix the not? rule mokha 2019-06-15 17:23:05
890f960 ensure space after not operator mokha 2019-06-15 17:11:50
55ab239 remove extra alias for presence mo 2019-05-31 04:35:50
38f5077 change parse tree keys mokha 2019-05-29 15:03:56
42265d2 Adds assign into rules. zamirmf 2019-05-16 20:04:19
0cc64f1 change alias mokha 2019-05-14 22:12:27
4ca97d7 add example for current need mokha 2019-05-14 22:08:33
2bf0d37 add trailing comma mokha 2019-05-14 19:54:23
fef65ad attach names mokha 2019-05-14 00:09:44
3c17f53 add parens mokha 2019-05-13 19:13:22
33b4195 add Gemfile.log mokha 2019-05-13 19:13:01
69ea3c6 remove optional space in brackets mokha 2019-05-12 02:13:24
b4231e6 Parser -> Filter mokha 2019-05-12 01:20:45
c21b5e7 fix linter errors mokha 2019-05-11 20:23:17
e530a9c improve number matcher mokha 2019-05-11 20:10:57
56a86ef fix repetition on sub attribute mokha 2019-05-11 17:20:37
57872a1 attempt to correct the scim uri rule mokha 2019-05-11 17:19:16
b105551 fix the attribute_name rule mokha 2019-05-11 17:09:41
759aa4d max 1 not mokha 2019-05-11 17:01:51
cbed9fa move ABNF doc closer to rules mokha 2019-05-11 02:31:50
3b06f2f fix linter errors mokha 2019-05-10 23:26:58
5a9d5c5 mark failing tests as pending mokha 2019-05-10 23:26:04
203c1f5 match dates as strings mokha 2019-05-10 23:22:36
dbe0e87 collapse specs mokha 2019-05-10 22:52:38
7ae953d expand to multiple lines mokha 2019-05-10 22:17:49
e2cf207 use intention revealing names mokha 2019-05-10 22:06:35
6bc679a try to debug failing specs mokha 2019-05-10 22:01:00
4d8cc8e allow compValue to repeat mokha 2019-05-10 21:55:07
14b24b7 update rules to match ABNF mokha 2019-05-10 21:36:55
ef48a24 add test for AND mokha 2019-05-10 19:11:50
da62382 match uri values mokha 2019-05-10 18:06:27
aa8fd7c parse dates mokha 2019-05-10 18:00:34
1015040 parse uri mokha 2019-05-10 17:58:12
2c92ca6 allow single quotes in values mokha 2019-05-10 17:11:12
6e5dc20 add spec for each operator mokha 2019-05-10 17:04:21
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'