Class: Scimitar::Lists::QueryParser
- Inherits:
-
Object
- Object
- Scimitar::Lists::QueryParser
- Defined in:
- app/models/scimitar/lists/query_parser.rb
Overview
Simple SCIM filter support.
This is currently an extremely limited query parser supporting only a single “name-operator-value” query, no boolean operations or precedence operators and it assumes “(I)LIKE” and “%” as wildcards in SQL for any operators which require partial match (contains / “co”, starts with / “sw”, ends with / “ew”). Generic operations don’t support “pr” either (‘presence’).
Create an instance, then construct a query appropriate for your storage back-end using #attribute to get the attribute name (in terms of “your data”, via your Scimitar::Resources::Mixin-including class implementation of ::scim_queryable_attributes), #operator to get a generic SQL operator such as “=” or “!=” and #parameter to get the value to be found (which you MUST take care to process so as to avoid an SQL injection or similar issues - use escaping suitable for your storage system’s query language).
-
If you don’t want to support (I)LIKE just check for it in #parameter’s return value; it’ll be upper case.
Given the likelihood of using ActiveRecord via Rails, there’s a higher level and easier method - just create the instance, then call QueryParser#to_activerecord_query to get a given base scope narrowed down to match the filter parameters.
Constant Summary collapse
- OPERATORS =
Combined operator precedence.
{ 'pr' => 4, 'eq' => 3, 'ne' => 3, 'gt' => 3, 'ge' => 3, 'lt' => 3, 'le' => 3, 'co' => 3, 'sw' => 3, 'ew' => 3, 'and' => 2, 'or' => 1 }.freeze
- UNARY_OPERATORS =
Unary operators.
Set.new([ 'pr' ]).freeze
- BINARY_OPERATORS =
Binary operators.
Set.new(OPERATORS.keys.reject { |op| UNARY_OPERATORS.include?(op) }).freeze
- PAREN =
Tokenizing expressions
/[\(\)]/.freeze
- STR =
/"(?:\\"|[^"])*"/.freeze
- OP =
/#{OPERATORS.keys.join('|')}/i.freeze
- WORD =
/[\w\.]+/.freeze
- SEP =
/\s?/.freeze
- NEXT_TOKEN =
/\A(#{PAREN}|#{STR}|#{OP}|#{WORD})#{SEP}/.freeze
- IS_OPERATOR =
/\A(?:#{OP})\Z/.freeze
Instance Attribute Summary collapse
-
#attribute_map ⇒ Object
readonly
Returns the value of attribute attribute_map.
-
#rpn ⇒ Object
readonly
Returns the value of attribute rpn.
Instance Method Summary collapse
-
#initialize(attribute_map) ⇒ QueryParser
constructor
Initialise an object.
-
#parse(input) ⇒ Object
Parse SCIM filter query into RPN stack.
-
#to_activerecord_query(base_scope) ⇒ Object
Having called #parse, call here to generate an ActiveRecord query based on a given starting scope.
-
#tree ⇒ Object
Transform the RPN stack into a tree, returning the result.
Constructor Details
#initialize(attribute_map) ⇒ QueryParser
Initialise an object.
attribute_map
-
See Scimitar::Resources::Mixin and documentation on implementing ::scim_queryable_attributes; pass that method’s return value here.
80 81 82 |
# File 'app/models/scimitar/lists/query_parser.rb', line 80 def initialize(attribute_map) @attribute_map = attribute_map.with_indifferent_case_insensitive_access() end |
Instance Attribute Details
#attribute_map ⇒ Object (readonly)
Returns the value of attribute attribute_map.
31 32 33 |
# File 'app/models/scimitar/lists/query_parser.rb', line 31 def attribute_map @attribute_map end |
#rpn ⇒ Object (readonly)
Returns the value of attribute rpn.
31 32 33 |
# File 'app/models/scimitar/lists/query_parser.rb', line 31 def rpn @rpn end |
Instance Method Details
#parse(input) ⇒ Object
Parse SCIM filter query into RPN stack
input
-
Input filter string, e.g. ‘givenName eq “Briony”’.
Returns a “self” for convenience. Call #rpn thereafter to retrieve the parsed RPN stack. For example, given this input:
userType eq "Employee" and (emails co "example.com" or emails co "example.org")
…returns a parser object wherein #rpn will yield:
[
'userType',
'"Employee"',
'eq',
'emails',
'"example.com"',
'co',
'emails',
'"example.org"',
'co',
'or',
'and'
]
Alternatively, call #tree to get an expression tree:
[
'and',
[
'eq',
'userType',
'"Employee"'
],
[
'or',
[
'co',
'emails',
'"example.com"'
],
[
'co',
'emails',
'"example.org"'
]
]
]
133 134 135 136 137 138 139 140 141 142 |
# File 'app/models/scimitar/lists/query_parser.rb', line 133 def parse(input) preprocessed_input = flatten_filter(input) rescue input @input = input.clone() # Saved just for error msgs @tokens = self.lex(preprocessed_input) @rpn = self.parse_expr() self.assert_eos() self end |
#to_activerecord_query(base_scope) ⇒ Object
Having called #parse, call here to generate an ActiveRecord query based on a given starting scope. The scope is used for all ‘and’ queries and as a basis for any nested ‘or’ scopes. For example, given this input:
userType eq "Employee" and (emails eq "[email protected]" or emails eq "[email protected]")
…and if you passed ‘User.active’ as a scope, there would be something along these lines sent to ActiveRecord:
User.active.where(user_type: 'Employee').and(User.active.where(work_email: '[email protected]').or(User.active.where(work_email: '[email protected]')))
See query_parser_spec.rb to get an idea for expected SQL based on various kinds of input, especially section “context ‘with complex cases’ do”.
base_scope
-
The starting scope, e.g. User.active.
Returns an ActiveRecord::Relation giving an SQL query that is the gem’s best attempt at interpreting the SCIM filter string.
173 174 175 176 177 178 |
# File 'app/models/scimitar/lists/query_parser.rb', line 173 def to_activerecord_query(base_scope) return self.to_activerecord_query_backend( base_scope: base_scope, expression_tree: self.tree() ) end |
#tree ⇒ Object
Transform the RPN stack into a tree, returning the result. A new tree is created each time, so you can mutate the result if need be.
See #parse for more information.
149 150 151 152 |
# File 'app/models/scimitar/lists/query_parser.rb', line 149 def tree @stack = @rpn.clone() self.get_tree() end |