scim-query-filter-parser 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gemspec +21 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.yaml +3 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.rdoc +57 -0
- data/Rakefile +73 -0
- data/lib/scim/query/filter/parser.rb +144 -0
- data/test/parse.rb +82 -0
- metadata +74 -0
data/.gemspec
ADDED
@@ -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
|
data/.travis.yml
ADDED
data/CHANGELOG.yaml
ADDED
data/Gemfile
ADDED
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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/test/parse.rb
ADDED
@@ -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: []
|