Comparing changes

v0.4.0 v0.5.0
7 commits 13 files changed

Commits

40aeba5 Update rubies to test with mo khan 2020-01-21 19:19:42
8ac4426 Prop 0.5.0 release mo khan 2020-01-21 19:16:12
4d4170b Add an abstract visitor class mo khan 2020-01-21 18:47:15
0d7f8ac Add base visitor class mo khan 2020-01-21 18:38:56
d9452c2 Fix linter errors mo khan 2020-01-21 17:01:13
b8804d2 Add AST abstraction to traverse tree mo khan 2020-01-21 16:44:14
d772749 Update gems mo khan 2020-01-21 16:32:05
lib/scim/kit/v2/filter/node.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'parslet'
+
+module Scim
+  module Kit
+    module V2
+      class Filter
+        # @private
+        class Node
+          def initialize(hash)
+            @hash = hash
+          end
+
+          def operator
+            self[:operator].to_sym
+          end
+
+          def attribute
+            self[:attribute].to_s
+          end
+
+          def value
+            self[:value].to_s[1..-2]
+          end
+
+          def not?
+            @hash.key?(:not)
+          end
+
+          def accept(visitor)
+            visitor.visit(self)
+          end
+
+          def left
+            self.class.new(self[:left])
+          end
+
+          def right
+            self.class.new(self[:right])
+          end
+
+          def inspect
+            @hash.inspect
+          end
+
+          private
+
+          def [](key)
+            @hash[key]
+          end
+        end
+      end
+    end
+  end
+end
lib/scim/kit/v2/filter/visitor.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'parslet'
+
+module Scim
+  module Kit
+    module V2
+      class Filter
+        # @private
+        class Visitor
+          VISITORS = {
+            and: :visit_and,
+            co: :visit_contains,
+            eq: :visit_equals,
+            ew: :visit_ends_with,
+            ge: :visit_greater_than_equals,
+            gt: :visit_greater_than,
+            le: :visit_less_than_equals,
+            lt: :visit_less_than,
+            ne: :visit_not_equals,
+            or: :visit_or,
+            pr: :visit_presence,
+            sw: :visit_starts_with
+          }.freeze
+
+          def visit(node)
+            visitor_for(node).call(node)
+          end
+
+          protected
+
+          def visitor_for(node)
+            method(VISITORS.fetch(node.operator, :visit_unknown))
+          end
+
+          def visit_and(node)
+            visit(node.left).merge(visit(node.right))
+            raise error_for(:visit_and)
+          end
+
+          def visit_or(_node)
+            raise error_for(:visit_or)
+          end
+
+          def visit_equals(_node)
+            raise error_for(:visit_equals)
+          end
+
+          def visit_not_equals(_node)
+            raise error_for(:visit_not_equals)
+          end
+
+          def visit_contains(_node)
+            raise error_for(:visit_contains)
+          end
+
+          def visit_starts_with(_node)
+            raise error_for(:visit_starts_with)
+          end
+
+          def visit_ends_with(_node)
+            raise error_for(:visit_ends_with)
+          end
+
+          def visit_greater_than(_node)
+            raise error_for(:visit_greater_than)
+          end
+
+          def visit_greater_than_equals(_node)
+            raise error_for(:visit_greater_than_equals)
+          end
+
+          def visit_less_than(_node)
+            raise error_for(:visit_less_than)
+          end
+
+          def visit_less_than_equals(_node)
+            raise error_for(:visit_less_than_equals)
+          end
+
+          def visit_presence(_node)
+            raise error_for(:visit_presence)
+          end
+
+          def visit_unknown(_node)
+            raise error_for(:visit_unknown)
+          end
+
+          private
+
+          def error_for(method)
+            ::Scim::Kit::NotImplementedError.new("#{method} is not implemented")
+          end
+        end
+      end
+    end
+  end
+end
lib/scim/kit/v2/filter.rb
@@ -125,6 +125,12 @@ module Scim
         rule(:underscore) { str('_') }
         rule(:version) { digit >> dot >> digit }
         rule(:assign) { str('=') }
+
+        class << self
+          def parse(filter)
+            Node.new(new.parse(filter))
+          end
+        end
       end
     end
   end
lib/scim/kit/v2.rb
@@ -12,6 +12,8 @@ require 'scim/kit/v2/mutability'
 require 'scim/kit/v2/resource'
 require 'scim/kit/v2/error'
 require 'scim/kit/v2/filter'
+require 'scim/kit/v2/filter/node'
+require 'scim/kit/v2/filter/visitor'
 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.4.0'
+    VERSION = '0.5.0'
   end
 end
lib/scim/kit.rb
@@ -20,6 +20,7 @@ module Scim
   module Kit
     class Error < StandardError; end
     class UnknownAttributeError < Error; end
+    class NotImplementedError < Error; end
     TYPE_ERROR = ArgumentError.new(:type)
 
     def self.logger
spec/scim/kit/v2/filter_spec.rb
@@ -92,12 +92,12 @@ RSpec.describe Scim::Kit::V2::Filter do
     specify { expect(subject.attribute_path.parse_with_debug(x)).to be_truthy }
   end
 
-  [
-    'userName',
-    'user_name',
-    'user-name',
-    'username1',
-    'schemas'
+  %w[
+    userName
+    user_name
+    user-name
+    username1
+    schemas
   ].each do |x|
     specify { expect(subject.attribute_name).to parse(x) }
   end
@@ -114,6 +114,7 @@ RSpec.describe Scim::Kit::V2::Filter do
   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) } }
 
   [
@@ -166,9 +167,51 @@ RSpec.describe Scim::Kit::V2::Filter do
 
   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') }
+
+  describe '.parse' do
+    subject { described_class }
+
+    [
+      %(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 |filter|
+      let(:visitor) { Scim::Kit::V2::Filter::Visitor.new }
+
+      specify { expect(subject.parse(filter)).to be_instance_of(Scim::Kit::V2::Filter::Node) }
+
+      specify do
+        node = subject.parse(filter)
+
+        expect do
+          node.accept(visitor)
+        end.to raise_error(Scim::Kit::NotImplementedError)
+      end
+    end
+  end
 end
spec/scim/kit/v2/resource_spec.rb
@@ -566,6 +566,7 @@ RSpec.describe Scim::Kit::V2::Resource do
       specify { expect(subject.name.family_name).to eql('Garrett') }
       specify { expect(subject.emails[0][:value]).to eql(email) }
       specify { expect(subject.emails[0][:primary]).to be(true) }
+
       specify do
         attributes = { schemas: schemas.map(&:id), unknown: 'unknown' }
         expect do
@@ -575,7 +576,7 @@ RSpec.describe Scim::Kit::V2::Resource do
     end
   end
 
-  describe Scim::Kit::V2::Messages::ERROR do
+  describe 'Errors' do
     subject { described_class.new(schemas: schemas) }
 
     let(:schemas) { [Scim::Kit::V2::Error.default_schema] }
.rubocop.yml
@@ -9,7 +9,7 @@ AllCops:
     - 'vendor/**/*'
   TargetRubyVersion: 2.5
 
-Layout/IndentFirstArrayElement:
+Layout/FirstArrayElementIndentation:
   EnforcedStyle: consistent
 
 Metrics/BlockLength:
.travis.yml
@@ -3,6 +3,7 @@ sudo: false
 language: ruby
 cache: bundler
 rvm:
-  - 2.5.5
-  - 2.6.3
+  - 2.5.7
+  - 2.6.5
+  - 2.7.0
 before_install: gem install bundler -v 1.17.1
CHANGELOG.md
@@ -1,4 +1,4 @@
-Version 0.4.0
+Version 0.5.0
 
 # Changelog
 All notable changes to this project will be documented in this file.
@@ -9,7 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 - nil
 
+## [0.5.0] - 2020-01-21
+### Added
+- Add API to traverse a SCIM filter AST
+
 ## [0.4.0] - 2019-06-15
+### Added
 - 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
@@ -39,7 +44,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.4.0...HEAD
+[Unreleased]: https://github.com/mokhan/scim-kit/compare/v0.5.0...HEAD
+[0.4.0]: https://github.com/mokhan/scim-kit/compare/v0.4.0...v0.5.0
 [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
Gemfile.lock
@@ -1,7 +1,7 @@
 PATH
   remote: .
   specs:
-    scim-kit (0.4.0)
+    scim-kit (0.5.0)
       activemodel (>= 5.2.0)
       net-hippie (~> 0.2)
       parslet (~> 1.8)
@@ -11,77 +11,79 @@ PATH
 GEM
   remote: https://rubygems.org/
   specs:
-    activemodel (5.2.3)
-      activesupport (= 5.2.3)
-    activesupport (5.2.3)
+    activemodel (6.0.2.1)
+      activesupport (= 6.0.2.1)
+    activesupport (6.0.2.1)
       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)
+      zeitwerk (~> 2.2)
+    addressable (2.7.0)
+      public_suffix (>= 2.0.2, < 5.0)
     ast (2.4.0)
     bundler-audit (0.6.1)
       bundler (>= 1.2.0, < 3)
       thor (~> 0.18)
-    byebug (11.0.1)
+    byebug (11.1.0)
     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)
+    ffaker (2.13.0)
+    hashdiff (1.0.0)
+    i18n (1.8.2)
       concurrent-ruby (~> 1.0)
-    jaro_winkler (1.5.2)
+    jaro_winkler (1.5.4)
     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)
+    minitest (5.14.0)
+    net-hippie (0.3.1)
+    parallel (1.19.1)
+    parser (2.7.0.2)
       ast (~> 2.4.0)
     parslet (1.8.2)
-    public_suffix (3.1.0)
+    public_suffix (4.0.3)
     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)
+    rake (13.0.1)
+    rspec (3.9.0)
+      rspec-core (~> 3.9.0)
+      rspec-expectations (~> 3.9.0)
+      rspec-mocks (~> 3.9.0)
+    rspec-core (3.9.1)
+      rspec-support (~> 3.9.1)
+    rspec-expectations (3.9.0)
       diff-lcs (>= 1.2.0, < 2.0)
-      rspec-support (~> 3.8.0)
-    rspec-mocks (3.8.1)
+      rspec-support (~> 3.9.0)
+    rspec-mocks (3.9.1)
       diff-lcs (>= 1.2.0, < 2.0)
-      rspec-support (~> 3.8.0)
-    rspec-support (3.8.2)
-    rubocop (0.71.0)
+      rspec-support (~> 3.9.0)
+    rspec-support (3.9.2)
+    rubocop (0.79.0)
       jaro_winkler (~> 1.5.1)
       parallel (~> 1.10)
-      parser (>= 2.6)
+      parser (>= 2.7.0.1)
       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)
+    rubocop-rspec (1.37.1)
+      rubocop (>= 0.68.1)
     ruby-progressbar (1.10.1)
     safe_yaml (1.0.5)
     thor (0.20.3)
     thread_safe (0.3.6)
-    tilt (2.0.9)
+    tilt (2.0.10)
     tilt-jbuilder (0.7.1)
       jbuilder
       tilt (>= 1.3.0, < 3)
-    tzinfo (1.2.5)
+    tzinfo (1.2.6)
       thread_safe (~> 0.1)
-    unicode-display_width (1.6.0)
-    webmock (3.6.0)
+    unicode-display_width (1.6.1)
+    webmock (3.8.0)
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
       hashdiff (>= 0.4.0, < 2.0.0)
+    zeitwerk (2.2.2)
 
 PLATFORMS
   ruby
@@ -91,7 +93,7 @@ DEPENDENCIES
   bundler-audit (~> 0.6)
   byebug
   ffaker (~> 2.7)
-  rake (~> 10.0)
+  rake (~> 13.0)
   rspec (~> 3.0)
   rubocop (~> 0.52)
   rubocop-rspec (~> 1.22)
scim-kit.gemspec
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
   spec.add_development_dependency 'bundler-audit', '~> 0.6'
   spec.add_development_dependency 'byebug'
   spec.add_development_dependency 'ffaker', '~> 2.7'
-  spec.add_development_dependency 'rake', '~> 10.0'
+  spec.add_development_dependency 'rake', '~> 13.0'
   spec.add_development_dependency 'rspec', '~> 3.0'
   spec.add_development_dependency 'rubocop', '~> 0.52'
   spec.add_development_dependency 'rubocop-rspec', '~> 1.22'