Skip to content

Commit

Permalink
Some reafactoring and better doc.
Browse files Browse the repository at this point in the history
  • Loading branch information
ingydotnet committed Sep 18, 2013
1 parent 20c9485 commit 54125cf
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 25 deletions.
16 changes: 16 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,24 @@ scim-query-filter-parser - Parser for SCIM Filter Query Strings

= Description

The SCIM spec describes a simple filter query language here:
http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources

This gem can parse one of these filter queries and produce a Reverse Polish
Notation (RPN) stack representation.

For example, parse this filter query string:

userType eq "Employee" and (emails co "example.com" or emails co "example.org")

Into this RPN stack (array):

[userType, "Employee", eq, emails, "example.com", co, emails, "example.org", co, or ,and]

Or, optionally into this expression tree:

[and, [eq, userType, "Employee"], [or, [co, emails, "example.com"], [co, emails, "example.org"]]]

= Methods

`parser = SCIM::Query::Filter::Parser.new()`::
Expand Down
50 changes: 25 additions & 25 deletions lib/scim/query/filter/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ class SCIM; class Query; class Filter; class Parser; end; end; end; end
class SCIM::Query::Filter::Parser
attr_accessor :rpn

#----------------------------------------------------------------------------
# Operator Precedence:
Ops = {
'pr' => 4,
'eq' => 3,
'co' => 3,
'sw' => 3,
'pr' => 4,
'gt' => 3,
'ge' => 3,
'lt' => 3,
Expand All @@ -19,13 +21,17 @@ class SCIM::Query::Filter::Parser
'pr' => 1,
}

# Tokenizing regexen:
Paren = /[\(\)]/
Str = /"(?:\\"|[^"])*"/
Op = /#{Ops.keys.join'|'}/
Word = /[\w\.]+/
Sep = /\s?/
Token = /^(#{Paren}|#{Str}|#{Op}|#{Word})#{Sep}/
NextToken = /^(#{Paren}|#{Str}|#{Op}|#{Word})#{Sep}/
IsOperator = /^(?:#{Op})$/

#----------------------------------------------------------------------------
# Parse SCIM filter query into RPN stack:
def parse input
@input = input.clone # Save for error msgs
@tokens = lex input
Expand All @@ -36,11 +42,11 @@ def parse input

def parse_expr
ast = []
want_op = false
expect_op = false
while not eos and peek != ')'
want_op && assert_op || assert_not_op
expect_op && assert_op || assert_not_op
ast.push(start_group ? parse_group : pop)
want_op ^= true unless Unary[ast.last]
expect_op ^= true unless Unary[ast.last]
end
to_rpn ast
end
Expand All @@ -56,14 +62,13 @@ def parse_group
def lex input
tokens = []
while ! input.empty? do
input.sub! Token, '' \
input.sub! NextToken, '' \
or fail "Can't lex input here '#{input}'"
tokens.push $1
end
tokens
end


# Turn parsed tokens into an RPN stack
# http://en.wikipedia.org/wiki/Shunting_yard_algorithm
def to_rpn ast
Expand All @@ -83,6 +88,7 @@ def to_rpn ast
(out.concat ops).flatten
end

#----------------------------------------------------------------------------
# Transform RPN stack into a tree structure
def tree
@stack = @rpn.clone
Expand All @@ -94,21 +100,20 @@ def get_tree
if not @stack.empty?
op = tree[0] = @stack.pop
tree[1] = Ops[@stack.last] ? get_tree : @stack.pop
if not Unary[op]
tree[2] = tree[1]
tree[1] = Ops[@stack.last] ? get_tree : @stack.pop
end
tree.insert 1, Ops[@stack.last] ? get_tree : @stack.pop \
if not Unary[op]
end
tree
end

#----------------------------------------------------------------------------
# Token sugar methods
def peek; @tokens.first end
def pop; @tokens.shift end
def eos; @tokens.empty? end
def start_group; peek == '(' end
def peek_operator
not(eos) and peek.match /^(?:#{Op})$/
not(eos) and peek.match IsOperator
end


Expand All @@ -118,27 +123,22 @@ def parse_error msg
end

def assert_op
parse_error "Unexpected token '%s'. Expected operator" \
if ! peek_operator
true
return true if peek_operator
parse_error "Unexpected token '%s'. Expected operator"
end

def assert_not_op
parse_error "Unexpected operator '%s'" \
if peek_operator
true
return true if ! peek_operator
parse_error "Unexpected operator '%s'"
end

def assert_close
parse_error "Unexpected token '%s'. Expected ')'" \
unless peek == ')'
true
return true if peek == ')'
parse_error "Unexpected token '%s'. Expected ')'"
end

def assert_eos
parse_error "Unexpected token '%s'. Expected EOS" \
if peek
true
return true if eos
parse_error "Unexpected token '%s'. Expected EOS"
end

end
1 change: 1 addition & 0 deletions test/parse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_spec
# See http://www.simplecloud.info/specs/draft-scim-api-01.html#query-resources
$test_parse_data = <<'...'
[]
[]
userName eq "bjensen"
Expand Down

0 comments on commit 54125cf

Please sign in to comment.