scim-query-filter-parser 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ GemSpec ||= Gem::Specification.new do |gem|
4
+ gem.name = 'scim-query-filter-parser'
5
+ gem.version = '0.0.1'
6
+ gem.license = 'MIT'
7
+ gem.required_ruby_version = '>= 1.9.1'
8
+
9
+ gem.authors << 'Ingy döt Net'
10
+ gem.email = 'ingy@ingy.net'
11
+ gem.summary = 'SCIM Filter Query Parser'
12
+ gem.description = <<-'.'
13
+ A parser for SCIM filter queries. Specced here:
14
+ http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
15
+ .
16
+ gem.homepage = 'https://github.com/ingydotnet/scim-query-filter-parser'
17
+
18
+ gem.files = `git ls-files`.lines.map{|l|l.chomp}
19
+
20
+ gem.add_development_dependency 'rake'
21
+ end
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ script: bundle exec rake test
@@ -0,0 +1,3 @@
1
+ - version: 0.0.1
2
+ date: Tue Sep 17 16:02:31 PDT 2013
3
+ changes: First release
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright © 2013 Ingy döt Net
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the ‘Software’), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ = Name
2
+
3
+ scim-query-filter-parser - Parser for SCIM Filter Query Strings
4
+
5
+ = Synopsis
6
+
7
+ require 'scim/query/filter/parser'
8
+
9
+ parser = SCIM::Query::Filter::Parser.new
10
+ rpn_array = parser.parse(filter_query_string)
11
+ rpn_stack = parser.rpn
12
+ rpn_tree = parser.tree
13
+
14
+ # or (in a single statement):
15
+ rpn_array = SCIM::Query::Filter::Parser.new.parse(filter_query_string).rpn
16
+
17
+ = Description
18
+
19
+ The SCIM spec describes a simple filter query language here:
20
+ http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
21
+
22
+ This gem can parse one of these filter queries and produce a Reverse Polish
23
+ Notation (RPN) stack representation.
24
+
25
+ For example, parse this filter query string:
26
+
27
+ userType eq "Employee" and (emails co "example.com" or emails co "example.org")
28
+
29
+ Into this RPN stack (array):
30
+
31
+ [userType, "Employee", eq, emails, "example.com", co, emails, "example.org", co, or ,and]
32
+
33
+ Or, optionally into this expression tree:
34
+
35
+ [and, [eq, userType, "Employee"], [or, [co, emails, "example.com"], [co, emails, "example.org"]]]
36
+
37
+ = Methods
38
+
39
+ `parser = SCIM::Query::Filter::Parser.new()`::
40
+ Creae a new parser object.
41
+
42
+ `parser.parse(input)`::
43
+ Parse a SCIM filter query. Return the parser object (self) if successful.
44
+
45
+ `array = parser.rpn`::
46
+ Get the RPN array created by the most recent parse.
47
+
48
+ `tree = parser.tree`::
49
+ Get the parse result converted to a tree form.
50
+
51
+ = Code Status
52
+
53
+ * {<img src="https://travis-ci.org/ingydotnet/scim-query-filter-parser-rb.png" />}[https://travis-ci.org/ingydotnet/scim-query-filter-parser-rb]
54
+
55
+ = Copyright
56
+
57
+ Copyright (c) 2013 Ingy döt Net. See LICENSE for further details.
@@ -0,0 +1,73 @@
1
+ # Load Gem constants from the gemspec
2
+ GemSpecFile = '.gemspec'
3
+ load GemSpecFile
4
+ GemName = GemSpec.name
5
+ GemVersion = GemSpec.version
6
+ GemDir = "#{GemName}-#{GemVersion}"
7
+ GemFile = "#{GemDir}.gem"
8
+ DevNull = '2>/dev/null'
9
+
10
+ # Require the Rake libraries
11
+ require 'rake'
12
+ require 'rake/testtask'
13
+ require 'rake/clean'
14
+ if File.exists? 'test/testml.yaml'
15
+ if File.exists? 'lib/rake/testml.rb'
16
+ $:.unshift "#{Dir.getwd}/lib"
17
+ end
18
+ require 'rake/testml'
19
+ end
20
+
21
+ task :default => 'help'
22
+
23
+ CLEAN.include GemDir, GemFile, 'data.tar.gz', 'metadata.gz'
24
+
25
+ desc 'Run the tests'
26
+ task :test do
27
+ load '.env' if File.exists? '.env'
28
+ Rake::TestTask.new do |t|
29
+ t.verbose = true
30
+ t.test_files = ENV['DEV_TEST_FILES'] &&
31
+ FileList[ENV['DEV_TEST_FILES'].split] ||
32
+ FileList['test/**/*.rb'].sort
33
+ end
34
+ end
35
+
36
+ desc 'Build the gem'
37
+ task :build => [:clean, :test] do
38
+ sh "gem build #{GemSpecFile}"
39
+ end
40
+
41
+ desc 'Install the gem'
42
+ task :install => [:build] do
43
+ sh "gem install #{GemFile}"
44
+ end
45
+
46
+ desc 'Build, unpack and inspect the gem'
47
+ task :distdir => [:build] do
48
+ sh "tar xf #{GemFile} #{DevNull}"
49
+ Dir.mkdir GemDir
50
+ Dir.chdir GemDir
51
+ sh "tar xzf ../data.tar.gz #{DevNull}"
52
+ puts "\n>>> Entering sub-shell for #{GemDir}..."
53
+ system ENV['SHELL']
54
+ end
55
+
56
+ desc 'Build and push the gem'
57
+ task :release => [:build] do
58
+ sh "gem push #{GemFile}"
59
+ end
60
+
61
+ desc 'Print a description of the gem'
62
+ task :desc do
63
+ puts "Gem: '#{GemName}' (version #{GemVersion})"
64
+ puts
65
+ puts GemSpec.description.gsub /^/, ' '
66
+ end
67
+
68
+ desc 'List the Rakefile tasks'
69
+ task :help do
70
+ puts 'The following rake tasks are available:'
71
+ puts
72
+ puts `rake -T`.gsub /^/, ' '
73
+ end
@@ -0,0 +1,144 @@
1
+ class SCIM; class Query; class Filter; class Parser; end; end; end; end
2
+
3
+ class SCIM::Query::Filter::Parser
4
+ attr_accessor :rpn
5
+
6
+ #----------------------------------------------------------------------------
7
+ # Operator Precedence:
8
+ Ops = {
9
+ 'pr' => 4,
10
+ 'eq' => 3,
11
+ 'co' => 3,
12
+ 'sw' => 3,
13
+ 'gt' => 3,
14
+ 'ge' => 3,
15
+ 'lt' => 3,
16
+ 'le' => 3,
17
+ 'and' => 2,
18
+ 'or' => 1,
19
+ }
20
+ Unary = {
21
+ 'pr' => 1,
22
+ }
23
+
24
+ # Tokenizing regexen:
25
+ Paren = /[\(\)]/
26
+ Str = /"(?:\\"|[^"])*"/
27
+ Op = /#{Ops.keys.join'|'}/
28
+ Word = /[\w\.]+/
29
+ Sep = /\s?/
30
+ NextToken = /^(#{Paren}|#{Str}|#{Op}|#{Word})#{Sep}/
31
+ IsOperator = /^(?:#{Op})$/
32
+
33
+ #----------------------------------------------------------------------------
34
+ # Parse SCIM filter query into RPN stack:
35
+ def parse input
36
+ @input = input.clone # Save for error msgs
37
+ @tokens = lex input
38
+ @rpn = parse_expr
39
+ assert_eos
40
+ self
41
+ end
42
+
43
+ def parse_expr
44
+ ast = []
45
+ expect_op = false
46
+ while not eos and peek != ')'
47
+ expect_op && assert_op || assert_not_op
48
+ ast.push(start_group ? parse_group : pop)
49
+ expect_op ^= true unless Unary[ast.last]
50
+ end
51
+ to_rpn ast
52
+ end
53
+
54
+ def parse_group
55
+ pop # pop '(' token
56
+ ast = parse_expr
57
+ assert_close && pop # pop ')' token
58
+ ast
59
+ end
60
+
61
+ # Split input into tokens
62
+ def lex input
63
+ tokens = []
64
+ while ! input.empty? do
65
+ input.sub! NextToken, '' \
66
+ or fail "Can't lex input here '#{input}'"
67
+ tokens.push $1
68
+ end
69
+ tokens
70
+ end
71
+
72
+ # Turn parsed tokens into an RPN stack
73
+ # http://en.wikipedia.org/wiki/Shunting_yard_algorithm
74
+ def to_rpn ast
75
+ out, ops = [], []
76
+ out.push ast.shift if not ast.empty?
77
+ while not ast.empty? do
78
+ op = ast.shift
79
+ p = Ops[op] \
80
+ or fail "Unknown operator '#{op}'"
81
+ while not ops.empty? do
82
+ break if p > Ops[ops.first]
83
+ out.push ops.shift
84
+ end
85
+ ops.unshift op
86
+ out.push ast.shift if not Unary[op]
87
+ end
88
+ (out.concat ops).flatten
89
+ end
90
+
91
+ #----------------------------------------------------------------------------
92
+ # Transform RPN stack into a tree structure
93
+ def tree
94
+ @stack = @rpn.clone
95
+ get_tree
96
+ end
97
+
98
+ def get_tree
99
+ tree = []
100
+ if not @stack.empty?
101
+ op = tree[0] = @stack.pop
102
+ tree[1] = Ops[@stack.last] ? get_tree : @stack.pop
103
+ tree.insert 1, Ops[@stack.last] ? get_tree : @stack.pop \
104
+ if not Unary[op]
105
+ end
106
+ tree
107
+ end
108
+
109
+ #----------------------------------------------------------------------------
110
+ # Token sugar methods
111
+ def peek; @tokens.first end
112
+ def pop; @tokens.shift end
113
+ def eos; @tokens.empty? end
114
+ def start_group; peek == '(' end
115
+ def peek_operator
116
+ not(eos) and peek.match IsOperator
117
+ end
118
+
119
+
120
+ # Error handling methods:
121
+ def parse_error msg
122
+ fail "#{sprintf(msg, *@tokens, 'EOS')}.\nInput: '#{@input}'\n"
123
+ end
124
+
125
+ def assert_op
126
+ return true if peek_operator
127
+ parse_error "Unexpected token '%s'. Expected operator"
128
+ end
129
+
130
+ def assert_not_op
131
+ return true if ! peek_operator
132
+ parse_error "Unexpected operator '%s'"
133
+ end
134
+
135
+ def assert_close
136
+ return true if peek == ')'
137
+ parse_error "Unexpected token '%s'. Expected ')'"
138
+ end
139
+
140
+ def assert_eos
141
+ return true if eos
142
+ parse_error "Unexpected token '%s'. Expected EOS"
143
+ end
144
+ end
@@ -0,0 +1,82 @@
1
+ require 'scim/query/filter/parser'
2
+ require 'test/unit'
3
+ require 'yaml'
4
+ require 'json'
5
+
6
+ class TestParser < Test::Unit::TestCase
7
+ def test_spec
8
+ data = $test_parse_data.lines.to_a
9
+ parser = SCIM::Query::Filter::Parser.new
10
+ while true do
11
+ input = data.shift or break
12
+ input.chomp!
13
+ rpn_yaml = data.shift or break
14
+ next if rpn_yaml == "\n"
15
+ parser.parse(input)
16
+ got_rpn_json = parser.rpn.to_json
17
+ want_rpn_json = YAML.load(rpn_yaml).to_json
18
+ assert_equal want_rpn_json, got_rpn_json,
19
+ "Test parse to RPN: '#{input.chomp}'"
20
+ tree_yaml = data.shift or break
21
+ next if tree_yaml == "\n"
22
+ got_tree_json = parser.tree.to_json
23
+ want_tree_json = YAML.load(tree_yaml).to_json
24
+ assert_equal want_tree_json, got_tree_json,
25
+ "Test parse to tree: '#{input.chomp}'"
26
+ blank_line = data.shift or break
27
+ fail "Got '#{blank_line.chomp}', expected blank line" \
28
+ unless blank_line == "\n"
29
+ end
30
+ end
31
+ end
32
+
33
+ # See http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
34
+ $test_parse_data = <<'...'
35
+
36
+ []
37
+ []
38
+
39
+ userName eq "bjensen"
40
+ [userName,'"bjensen"',eq]
41
+ [eq, userName,'"bjensen"']
42
+
43
+ name.familyName co "O'Malley"
44
+ [name.familyName, '"O''Malley"', co]
45
+ [co, name.familyName, '"O''Malley"']
46
+
47
+ userName sw "J"
48
+ [userName, '"J"', sw]
49
+ [sw, userName, '"J"']
50
+
51
+ title pr
52
+ [title, pr]
53
+ [pr, title]
54
+
55
+ meta.lastModified gt "2011-05-13T04:42:34Z"
56
+ [meta.lastModified, '"2011-05-13T04:42:34Z"', gt]
57
+ [gt, meta.lastModified, '"2011-05-13T04:42:34Z"']
58
+
59
+ meta.lastModified ge "2011-05-13T04:42:34Z"
60
+ [meta.lastModified, '"2011-05-13T04:42:34Z"', ge]
61
+ [ge, meta.lastModified, '"2011-05-13T04:42:34Z"']
62
+
63
+ meta.lastModified lt "2011-05-13T04:42:34Z"
64
+ [meta.lastModified, '"2011-05-13T04:42:34Z"', lt]
65
+ [lt, meta.lastModified, '"2011-05-13T04:42:34Z"']
66
+
67
+ meta.lastModified le "2011-05-13T04:42:34Z"
68
+ [meta.lastModified, '"2011-05-13T04:42:34Z"', le]
69
+ [le, meta.lastModified, '"2011-05-13T04:42:34Z"']
70
+
71
+ title pr and userType eq "Employee"
72
+ [title, pr, userType, '"Employee"', eq, and]
73
+ [and, [pr, title], [eq, userType, '"Employee"']]
74
+
75
+ title pr or userType eq "Intern"
76
+ [title, pr, userType, '"Intern"', eq, or]
77
+ [or, [pr, title], [eq, userType, '"Intern"']]
78
+
79
+ userType eq "Employee" and (emails co "example.com" or emails co "example.org")
80
+ [userType, '"Employee"', eq, emails, '"example.com"', co, emails, '"example.org"', co, or ,and]
81
+ [and, [eq, userType, '"Employee"'], [or, [co, emails, '"example.com"'], [co, emails, '"example.org"']]]
82
+ ...
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scim-query-filter-parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ingy döt Net
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: ! 'A parser for SCIM filter queries. Specced here:
31
+
32
+ http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
33
+
34
+ '
35
+ email: ingy@ingy.net
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - .gemspec
41
+ - .travis.yml
42
+ - CHANGELOG.yaml
43
+ - Gemfile
44
+ - LICENSE
45
+ - README.rdoc
46
+ - Rakefile
47
+ - lib/scim/query/filter/parser.rb
48
+ - test/parse.rb
49
+ homepage: https://github.com/ingydotnet/scim-query-filter-parser
50
+ licenses:
51
+ - MIT
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.9.1
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 1.8.23
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: SCIM Filter Query Parser
74
+ test_files: []