Class: Scimitar::Lists::QueryParser

Inherits:
Object
  • Object
show all
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
ATTRNAME =

Precompiled expression that matches a valid attribute name according to tools.ietf.org/html/rfc7643#section-2.1.

/[[:alpha:]][[:alnum:]$-_]*/
PAREN =

Tokenizing expressions

/[\(\)]/.freeze
STR =
/"(?:\\"|[^"])*"/.freeze
OP =
/(?:#{OPERATORS.keys.join('|')})\b/i.freeze
WORD =
/[\w\.]+/.freeze
SEP =
/\s?/.freeze
NEXT_TOKEN =
/\A(#{PAREN}|#{STR}|#{OP}|#{WORD})#{SEP}/.freeze
IS_OPERATOR =
/\A(?:#{OP})\Z/.freeze
MONGO_SIMPLE_COMPARISON_OPERATORS =
{
  "eq" => "$eq", # equal
  "ne" => "$ne", # not equal
  "lt" => "$lt", # less than
  "le" => "$lte", # less than or equal
  "gt" => "$gt", # greater than
  "ge" => "$gte" # greater than or equal
}.freeze
MONGO_COMPLEX_COMPARISON_OPERATORS =

Present, starts with, ends with, contains

%w[pr sw ew co].freeze
MONGO_COMBINATION_OPERATORS =
{
  "and" => "$and",
  "or" => "$or"
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

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.



101
102
103
# File 'app/models/scimitar/lists/query_parser.rb', line 101

def initialize(attribute_map)
  @attribute_map = attribute_map.with_indifferent_case_insensitive_access()
end

Instance Attribute Details

#attribute_mapObject (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

#rpnObject (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"'
    ]
  ]
]


154
155
156
157
158
159
160
161
162
163
# File 'app/models/scimitar/lists/query_parser.rb', line 154

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.



194
195
196
197
198
199
# File 'app/models/scimitar/lists/query_parser.rb', line 194

def to_activerecord_query(base_scope)
  return self.to_activerecord_query_backend(
    base_scope:      base_scope,
    expression_tree: self.tree()
  )
end

#to_mongoid_queryObject

Having called #parse, call here to generate a Mongoid query For example, given this input:

userType eq "Employee" and (emails eq "[email protected]" or emails eq "[email protected]")

this method will return a Mongoid query that looks like this:

 {
   "$and" => [
     { :user_type => { "$eq" => 'Employee' } },
     { "$or" => [
       { :emails => { "$eq" => "[email protected]" } },
       { :emails => { "$eq" => "[email protected]" } }
     ]
   ]
}

Use it with the Mongoid::Criteria#where method to filter results, e.g.:

   User.where(parser.to_mongoid_query)

Returns a Mongoid query that is the gem’s best attempt at interpreting the SCIM filter string.



224
225
226
# File 'app/models/scimitar/lists/query_parser.rb', line 224

def to_mongoid_query
  tree_node_to_mongoid_query(tree)
end

#treeObject

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.



170
171
172
173
# File 'app/models/scimitar/lists/query_parser.rb', line 170

def tree
  @stack = @rpn.clone()
  self.get_tree()
end