scim2-filter 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e97f1535209f959f102bff6a8d05ab2b0be5284f99482a34bcdfe36030c949b
4
+ data.tar.gz: b69d559219489bfdd64a90af1c6cbc8bbb86576514d83ab985da04ddb0250dee
5
+ SHA512:
6
+ metadata.gz: 4aee232a4ffb1bc1a6138d96702d10c7d53face887c05522671d2079fef7e87846b1db9cbc184e976100094de729b0d38cb7db9d5b473db94811c96f4d51fe9e
7
+ data.tar.gz: 1f1af70c8fbb720e9b8b38ecc95b9d4e33cc5d7565393852598c661ca6770e35dd17a34b0fec24dc4f2ea9474fea74adddf5091e04bf5d8e021f937c5077200f
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # SCIM2 Filter
2
+
3
+ * README: https://github.com/david-mccullars/scim2-filter
4
+ * Documentation: http://www.rubydoc.info/github/david-mccullars/scim2-filter
5
+ * Bug Reports: https://github.com/david-mccullars/scim2-filter/issues
6
+
7
+
8
+ ## Status
9
+
10
+ [![Gem Version](https://badge.fury.io/rb/scim2-filter.svg)](https://badge.fury.io/rb/scim2-filter)
11
+ [![Travis Build Status](https://travis-ci.org/david-mccullars/scim2-filter.svg?branch=master)](https://travis-ci.org/david-mccullars/scim2-filter)
12
+ [![Code Climate](https://codeclimate.com/github/david-mccullars/scim2-filter/badges/gpa.svg)](https://codeclimate.com/github/david-mccullars/scim2-filter)
13
+
14
+
15
+ ## Description
16
+
17
+
18
+ ## Features
19
+
20
+
21
+ ## Installation
22
+
23
+ ```
24
+ gem install scim2-filter
25
+ ```
26
+
27
+ ## Requirements
28
+
29
+
30
+ ## Usage
31
+
32
+
33
+ ## License
34
+
35
+ MIT. See the {file:LICENSE} file.
36
+
37
+
38
+ ## References
@@ -0,0 +1 @@
1
+ require_relative 'scim2'
data/lib/scim2.rb ADDED
@@ -0,0 +1,8 @@
1
+ ##
2
+ # Scim2 module documentation
3
+ #
4
+ module Scim2
5
+
6
+ autoload :Filter, 'scim2/filter'
7
+
8
+ end
@@ -0,0 +1,23 @@
1
+ module Scim2
2
+ ##
3
+ #
4
+ # RFC7644 SCIM (System for Cross-domain Identity Management) 2.0 defines
5
+ # mechanisms to query resources. This includes an optional `filter` parameter
6
+ # which allows for comprehensive filtering of resources based on various
7
+ # criteria. The structure and syntax for the `filter` parameter is defined in
8
+ # Section 3.4.2.2 of RFC7644. This module contains classes to support the
9
+ # validation and parsing of the `filter` parameter as well as its translation
10
+ # into Arel nodes for use in querying a database.
11
+ #
12
+ # @see https://tools.ietf.org/html/rfc7644#section-3.4.2.2
13
+ ##
14
+ module Filter
15
+
16
+ autoload :ArelHandler, 'scim2/filter/arel_handler'
17
+ autoload :Lexer, 'scim2/filter/lexer'
18
+ autoload :NoOpHandler, 'scim2/filter/no_op_handler'
19
+ autoload :Parser, 'scim2/filter/parser'
20
+ autoload :SimpleHandler, 'scim2/filter/simple_handler'
21
+
22
+ end
23
+ end
@@ -0,0 +1,125 @@
1
+ module Scim2
2
+ module Filter
3
+ ##
4
+ # Implementation of parser handler which translates SCIM 2.0 filters into AREL.
5
+ # In order to do this, instances of this class will need to be passed the mapping
6
+ # of attribute names to columns/AREL.
7
+ #
8
+ # @example
9
+ # # userName sw "J"
10
+ #
11
+ # mapping = {
12
+ # userName: User.arel_table[:name],
13
+ # }
14
+ #
15
+ # # "users"."name" LIKE 'J%'
16
+ #
17
+ # @example
18
+ # # urn:ietf:params:scim:schemas:core:2.0:User:userType ne "Employee" and not (emails.value co "example.com" or emails.value co "example.org")
19
+ #
20
+ # mapping = {
21
+ # userType: User.arel_table[:type],
22
+ # emails: {
23
+ # value: User.arel_table[:email],
24
+ # },
25
+ # }
26
+ #
27
+ # # "users"."type" != 'Employee' AND NOT ("users"."email" LIKE '%example.com' OR "users"."email" LIKE '%example.org%')
28
+ #
29
+ # @note Nested filters (e.g. `emails[type eq "work"]` are not supported at this
30
+ # time and will result in an `ArgumentError`
31
+ ##
32
+ class ArelHandler
33
+
34
+ attr_reader :arel_mapping
35
+
36
+ def initialize(arel_mapping)
37
+ @arel_mapping = arel_mapping
38
+ end
39
+
40
+ # Handle NOT filters (e.g. `not (color eq "red")`)
41
+ # @param filter [Hash<Symbol, Object>] the internal filter being NOT'ed
42
+ # @return [Hash<Symbol, Object>]
43
+ def on_not_filter(filter, context:)
44
+ filter.not
45
+ end
46
+
47
+ # Handle basic attribute comparison filters (e.g. `preference.color eq "red"`)
48
+ # @param attribute_path [Array<Symbol>] the attribute name(s) being filtered on, split by `.`
49
+ # @param value [Object] the value being compared against
50
+ # @param op [Object] the comparison operator (e.g. `:eq`)
51
+ # @param schema [String] schema namespace of the attribute
52
+ # @return [Hash<Symbol, Object>]
53
+ def on_attribute_filter(attribute_path, value, context:, op:, schema: nil)
54
+ arel = lookup_arel(attribute_path)
55
+ case op
56
+ when :eq
57
+ arel.eq(value)
58
+ when :ne
59
+ arel.not_eq(value)
60
+ when :co
61
+ arel.matches("%#{value}%")
62
+ when :sw
63
+ arel.matches("#{value}%")
64
+ when :ew
65
+ arel.matches("%#{value}")
66
+ when :gt
67
+ arel.gt(value)
68
+ when :ge
69
+ arel.gteq(value)
70
+ when :lt
71
+ arel.lt(value)
72
+ when :le
73
+ arel.lteq(value)
74
+ when :pr
75
+ arel.not_eq(nil)
76
+ else
77
+ raise Racc::ParseError, "invalid attribute operand #{op.inspect} with argument #{value.inspect}"
78
+ end
79
+ end
80
+
81
+ # Handle logical filters (e.g. `name.givenName sw "D" AND title co "VP"`)
82
+ # @param filter1 [Hash<Symbol, Object>] the left-hand side filter
83
+ # @param filter2 [Hash<Symbol, Object>] the right-hand side filter
84
+ # @param op [Object] the logical operator (e.g. `AND`)
85
+ # @return [Hash<Symbol, Object>]
86
+ def on_logical_filter(filter1, filter2, context:, op:)
87
+ case op
88
+ when :and
89
+ filter1.and(filter2)
90
+ when :or
91
+ filter1.or(filter2)
92
+ else
93
+ raise Racc::ParseError, "invalid logical operand #{op.inspect}"
94
+ end
95
+ end
96
+
97
+ # Handle nested filters (e.g. `emails[type eq "work"]`)
98
+ # @param attribute_path [Array<Symbol>] the attribute name(s) being filtered on, split by `.`
99
+ # @param filter [Hash<Symbol, Object>] the nested filter inside the backets
100
+ # @param schema [String] schema namespace of the attribute
101
+ # @return [Hash<Symbol, Object>]
102
+ def on_nested_filter(attribute_path, filter, context:, schema: nil)
103
+ raise ArgumentError, 'Nested attributes are not currently supported for AREL'
104
+ end
105
+
106
+ protected
107
+
108
+ # Looks up the arel object from the mapping according to the given attribute path
109
+ # @param attribute_path [Array<Symbol>] the attribute name(s) being filtered on, split by `.`
110
+ # @return [Object] the object returned by the mapping
111
+ def lookup_arel(attribute_path)
112
+ arel = arel_mapping.dig(*attribute_path)
113
+ case arel
114
+ when NilClass
115
+ raise ArgumentError, "Attribute #{attribute_path.join(',').inspect} not found in mapping"
116
+ when Arel::Predications
117
+ arel
118
+ else
119
+ raise ArgumentError, "Mapping for attribute #{attribute_path.join(',').inspect} is not a valid arel object"
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'lexer.rex'
2
+
3
+ module Scim2
4
+ module Filter
5
+ ##
6
+ # Implements a SCIM2 compliant lexer for query filters.
7
+ # This class is responsible for producing and emitting
8
+ # tokens for consumption by a parser. It is not
9
+ # intended to use directly.
10
+ ##
11
+ class Lexer < ::Racc::Parser
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,65 @@
1
+ class Scim2::Filter::Lexer
2
+
3
+ macro
4
+ EQ [eE][qQ]
5
+ NE [nN][eE]
6
+ GT [gG][tT]
7
+ GE [gG][eE]
8
+ LT [lL][tT]
9
+ LE [lL][eE]
10
+ CO [cC][oO]
11
+ SW [sS][wW]
12
+ EW [eE][wW]
13
+ PR [pP][rR]
14
+
15
+ AND [aA][nN][dD]
16
+ OR [oO][rR]
17
+ NOT [nN][oO][tT]
18
+
19
+ LPAREN \(
20
+ RPAREN \)
21
+ LBRACKET \[
22
+ RBRACKET \]
23
+ COLON :
24
+ DOT \.
25
+
26
+ DIGIT \d
27
+ ALPHA [a-zA-Z]
28
+ NAMECHAR [a-zA-Z0-9_-]
29
+ SCHEMACHAR [a-zA-Z0-9:\._-]
30
+ ESCAPEDSTR "(?:[^"\\]|\\.)*"
31
+
32
+ rule
33
+ # Attribute Operators
34
+ \s{EQ}\s { [:EQ, :eq] }
35
+ \s{NE}\s { [:NE, :ne] }
36
+ \s{GT}\s { [:GT, :gt] }
37
+ \s{GE}\s { [:GE, :ge] }
38
+ \s{LT}\s { [:LT, :lt] }
39
+ \s{LE}\s { [:LE, :le] }
40
+ \s{CO}\s { [:CO, :co] }
41
+ \s{SW}\s { [:SW, :sw] }
42
+ \s{EW}\s { [:EW, :ew] }
43
+ \s{PR} { [:PR, :pr] }
44
+
45
+ # Logical Operators
46
+ \s{AND}\s { [:AND, :and] }
47
+ \s{OR}\s { [:OR, :or] }
48
+ {NOT}\s* { [:NOT, :not] } # NOTE: RFC7644 (Section 3.4.2) seems to specify no space, but the example contradicts it
49
+
50
+ # Grouping Operators
51
+ {LPAREN} { [:LPAREN, text] }
52
+ {RPAREN} { [:RPAREN, text] }
53
+ {LBRACKET} { [:LBRACKET, text] }
54
+ {RBRACKET} { [:RBRACKET, text] }
55
+
56
+ # Other
57
+ null { [:NULL, nil] }
58
+ (?:true|false) { [:BOOLEAN, text == 'true'] }
59
+ {DIGIT}+ { [:NUMBER, text.to_i] }
60
+ {ESCAPEDSTR} { [:STRING, text.undump] }
61
+ ({ALPHA}+{COLON}{SCHEMACHAR}+){COLON} { [:SCHEMA, @ss.captures.first] }
62
+ {ALPHA}{NAMECHAR}* { [:ATTRNAME, text.to_sym] }
63
+ {DOT} { [:DOT, text] }
64
+
65
+ end
@@ -0,0 +1,144 @@
1
+ #--
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by rex 1.0.7
4
+ # from lexical definition file "lib/scim2/filter/lexer.rex".
5
+ #++
6
+
7
+ require 'racc/parser'
8
+ class Scim2::Filter::Lexer < Racc::Parser
9
+ require 'strscan'
10
+
11
+ class ScanError < StandardError ; end
12
+
13
+ attr_reader :lineno
14
+ attr_reader :filename
15
+ attr_accessor :state
16
+
17
+ def scan_setup(str)
18
+ @ss = StringScanner.new(str)
19
+ @lineno = 1
20
+ @state = nil
21
+ end
22
+
23
+ def action
24
+ yield
25
+ end
26
+
27
+ def scan_str(str)
28
+ scan_setup(str)
29
+ do_parse
30
+ end
31
+ alias :scan :scan_str
32
+
33
+ def load_file( filename )
34
+ @filename = filename
35
+ File.open(filename, "r") do |f|
36
+ scan_setup(f.read)
37
+ end
38
+ end
39
+
40
+ def scan_file( filename )
41
+ load_file(filename)
42
+ do_parse
43
+ end
44
+
45
+
46
+ def next_token
47
+ return if @ss.eos?
48
+
49
+ # skips empty actions
50
+ until token = _next_token or @ss.eos?; end
51
+ token
52
+ end
53
+
54
+ def _next_token
55
+ text = @ss.peek(1)
56
+ @lineno += 1 if text == "\n"
57
+ token = case @state
58
+ when nil
59
+ case
60
+ when (text = @ss.scan(/\s[eE][qQ]\s/))
61
+ action { [:EQ, :eq] }
62
+
63
+ when (text = @ss.scan(/\s[nN][eE]\s/))
64
+ action { [:NE, :ne] }
65
+
66
+ when (text = @ss.scan(/\s[gG][tT]\s/))
67
+ action { [:GT, :gt] }
68
+
69
+ when (text = @ss.scan(/\s[gG][eE]\s/))
70
+ action { [:GE, :ge] }
71
+
72
+ when (text = @ss.scan(/\s[lL][tT]\s/))
73
+ action { [:LT, :lt] }
74
+
75
+ when (text = @ss.scan(/\s[lL][eE]\s/))
76
+ action { [:LE, :le] }
77
+
78
+ when (text = @ss.scan(/\s[cC][oO]\s/))
79
+ action { [:CO, :co] }
80
+
81
+ when (text = @ss.scan(/\s[sS][wW]\s/))
82
+ action { [:SW, :sw] }
83
+
84
+ when (text = @ss.scan(/\s[eE][wW]\s/))
85
+ action { [:EW, :ew] }
86
+
87
+ when (text = @ss.scan(/\s[pP][rR]/))
88
+ action { [:PR, :pr] }
89
+
90
+ when (text = @ss.scan(/\s[aA][nN][dD]\s/))
91
+ action { [:AND, :and] }
92
+
93
+ when (text = @ss.scan(/\s[oO][rR]\s/))
94
+ action { [:OR, :or] }
95
+
96
+ when (text = @ss.scan(/[nN][oO][tT]\s*/))
97
+ action { [:NOT, :not] } # NOTE: RFC7644 (Section 3.4.2) seems to specify no space, but the example contradicts it
98
+
99
+ when (text = @ss.scan(/\(/))
100
+ action { [:LPAREN, text] }
101
+
102
+ when (text = @ss.scan(/\)/))
103
+ action { [:RPAREN, text] }
104
+
105
+ when (text = @ss.scan(/\[/))
106
+ action { [:LBRACKET, text] }
107
+
108
+ when (text = @ss.scan(/\]/))
109
+ action { [:RBRACKET, text] }
110
+
111
+ when (text = @ss.scan(/null/))
112
+ action { [:NULL, nil] }
113
+
114
+ when (text = @ss.scan(/(?:true|false)/))
115
+ action { [:BOOLEAN, text == 'true'] }
116
+
117
+ when (text = @ss.scan(/\d+/))
118
+ action { [:NUMBER, text.to_i] }
119
+
120
+ when (text = @ss.scan(/"(?:[^"\\]|\\.)*"/))
121
+ action { [:STRING, text.undump] }
122
+
123
+ when (text = @ss.scan(/([a-zA-Z]+:[a-zA-Z0-9:\._-]+):/))
124
+ action { [:SCHEMA, @ss.captures.first] }
125
+
126
+ when (text = @ss.scan(/[a-zA-Z][a-zA-Z0-9_-]*/))
127
+ action { [:ATTRNAME, text.to_sym] }
128
+
129
+ when (text = @ss.scan(/\./))
130
+ action { [:DOT, text] }
131
+
132
+
133
+ else
134
+ text = @ss.string[@ss.pos .. -1]
135
+ raise ScanError, "can not match: '" + text + "'"
136
+ end # if
137
+
138
+ else
139
+ raise ScanError, "undefined state: '" + state.to_s + "'"
140
+ end # case state
141
+ token
142
+ end # def _next_token
143
+
144
+ end # class
@@ -0,0 +1,27 @@
1
+ module Scim2
2
+ module Filter
3
+ ##
4
+ # A no-op handler implementation which does nothing with parse events. Its
5
+ # primary is for validation where the parsed data is not needed.
6
+ ##
7
+ class NoOpHandler
8
+
9
+ # Handle NOT filters (e.g. `not (color eq "red")`)
10
+ # @return [NilClass]
11
+ def on_not_filter(filter, context:); end
12
+
13
+ # Handle basic attribute comparison filters (e.g. `preference.color eq "red"`)
14
+ # @return [NilClass]
15
+ def on_attribute_filter(attribute_path, value, context:, op:, schema: nil); end
16
+
17
+ # Handle logical filters (e.g. `name.givenName sw "D" AND title co "VP"`)
18
+ # @return [NilClass]
19
+ def on_logical_filter(filter1, filter2, context:, op:); end
20
+
21
+ # Handle sub filters (e.g. `emails[type eq "work"]`)
22
+ # @return [NilClass]
23
+ def on_nested_filter(attribute_path, filter, context:, schema: nil); end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,108 @@
1
+ class Scim2::Filter::Parser
2
+
3
+ token EQ NE GT GE LT LE CO SW EW PR AND OR NOT LPAREN RPAREN LBRACKET RBRACKET
4
+ token NULL BOOLEAN NUMBER STRING ATTRNAME SCHEMA DOT
5
+
6
+ rule
7
+ #
8
+ # We must separate OR and AND filter rules in order to ensure the order of operations
9
+ # is properly followed. Specifically, the following two filters are the same:
10
+ #
11
+ # w AND x OR y AND z
12
+ # (w AND x) OR (y AND z)
13
+ #
14
+ # Separating the rules the parser will aggressively gobble up AND rules first, leaving
15
+ # OR rules for the very last as it bubbles back up the recursion stack
16
+ #
17
+ filter
18
+ : non_or_filter
19
+ | filter OR non_or_filter
20
+ {
21
+ filter1, op, filter2 = val
22
+ result = handler.on_logical_filter(filter1, filter2, op: op, context: result)
23
+ }
24
+
25
+ non_or_filter
26
+ : non_boolean_filter
27
+ | non_or_filter AND non_boolean_filter
28
+ {
29
+ filter1, op, filter2 = val
30
+ result = handler.on_logical_filter(filter1, filter2, op: op, context: result)
31
+ }
32
+
33
+ non_boolean_filter
34
+ : attribute_filter
35
+ | nested_filter
36
+ | grouped_filter
37
+ | not_filter
38
+
39
+ attribute_filter
40
+ : attr_path PR
41
+ {
42
+ attr, op = val
43
+ result = handler.on_attribute_filter(attr[:path], nil, op: op, schema: attr[:schema], context: result)
44
+ }
45
+ | attr_path comp_op comp_value
46
+ {
47
+ attr, op, v = val
48
+ result = handler.on_attribute_filter(attr[:path], v, op: op, schema: attr[:schema], context: result)
49
+ }
50
+
51
+ nested_filter
52
+ : attr_path LBRACKET filter RBRACKET
53
+ {
54
+ attr, _, filter, _ = val
55
+ result = handler.on_nested_filter(attr[:path], filter, schema: attr[:schema], context: result)
56
+ }
57
+
58
+ grouped_filter
59
+ : LPAREN filter RPAREN
60
+ {
61
+ result = val[1]
62
+ }
63
+
64
+ not_filter
65
+ : NOT grouped_filter
66
+ {
67
+ _, filter = val
68
+ result = handler.on_not_filter(filter, context: result)
69
+ }
70
+
71
+ attr_path
72
+ : SCHEMA attr_path_elements
73
+ {
74
+ schema, path = val
75
+ result = { schema: schema, path: path }
76
+ }
77
+ | attr_path_elements
78
+ {
79
+ result = { path: val.last }
80
+ }
81
+
82
+ attr_path_elements
83
+ : attr_path_elements DOT ATTRNAME
84
+ {
85
+ result = [*val.first, val.last]
86
+ }
87
+ | ATTRNAME
88
+ {
89
+ result = [val.last]
90
+ }
91
+
92
+ comp_op
93
+ : EQ
94
+ | NE
95
+ | GT
96
+ | GE
97
+ | LT
98
+ | LE
99
+ | CO
100
+ | SW
101
+ | EW
102
+
103
+ comp_value
104
+ : BOOLEAN
105
+ | NULL
106
+ | NUMBER
107
+ | STRING
108
+ end
@@ -0,0 +1,43 @@
1
+ require_relative 'parser.tab'
2
+
3
+ module Scim2
4
+ module Filter
5
+ ##
6
+ # Implements a SCIM2 compliant event-based parser for query filters. The parser
7
+ # will emit four different events to a handler implemention as it encounters
8
+ # various components within the filter. For reference, see:
9
+ #
10
+ # * {SimpleHandler#on_not_filter}
11
+ # * {SimpleHandler#on_attribute_filter}
12
+ # * {SimpleHandler#on_logical_filter}
13
+ # * {SimpleHandler#on_nested_filter}
14
+ ##
15
+ class Parser < ::Racc::Parser
16
+
17
+ attr_reader :handler
18
+
19
+ # @param handler <Handler> a handler object that responds to all four events
20
+ def initialize(handler = SimpleHandler.new)
21
+ super()
22
+ @handler = handler
23
+ end
24
+
25
+ # Required by {::Racc::Parser} to emit the next token to the parser. This
26
+ # method should generally not be called directly.
27
+ # @return <String> the next token
28
+ def next_token
29
+ @lexer.next_token
30
+ end
31
+
32
+ # Parses a given string input
33
+ # @param string <String> the filter to parse
34
+ # @return <Object> returns the last object emitted by the handler
35
+ def parse(string)
36
+ @lexer = Lexer.new
37
+ @lexer.scan_setup(string)
38
+ do_parse
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,327 @@
1
+ #
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by Racc 1.5.2
4
+ # from Racc grammar file "".
5
+ #
6
+
7
+ require 'racc/parser.rb'
8
+ module Scim2
9
+ module Filter
10
+ class Parser < Racc::Parser
11
+ ##### State transition tables begin ###
12
+
13
+ racc_action_table = [
14
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 17,
15
+ 14, 10, 9, 16, 9, 19, 10, 9, 10, 9,
16
+ 13, 11, 13, 15, 32, 13, 11, 13, 11, 10,
17
+ 9, 10, 9, 38, 37, 39, 40, 15, 13, 11,
18
+ 13, 11, 44, 15, 33, 32, 42, 43, 16 ]
19
+
20
+ racc_action_check = [
21
+ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
22
+ 1, 0, 0, 2, 10, 8, 9, 9, 15, 15,
23
+ 0, 0, 11, 1, 12, 9, 9, 15, 15, 16,
24
+ 16, 19, 19, 18, 18, 18, 18, 41, 16, 16,
25
+ 19, 19, 41, 29, 14, 31, 29, 32, 34 ]
26
+
27
+ racc_action_pointer = [
28
+ -3, 10, 1, nil, nil, nil, nil, nil, -2, 2,
29
+ -1, -1, -1, nil, 44, 4, 15, nil, 14, 17,
30
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 30,
31
+ nil, 20, 24, nil, 36, nil, nil, nil, nil, nil,
32
+ nil, 24, nil, nil, nil ]
33
+
34
+ racc_action_default = [
35
+ -31, -31, -1, -3, -5, -6, -7, -8, -31, -31,
36
+ -31, -31, -15, -17, -31, -31, -31, -9, -31, -31,
37
+ -18, -19, -20, -21, -22, -23, -24, -25, -26, -31,
38
+ -13, -14, -31, 45, -2, -4, -10, -27, -28, -29,
39
+ -30, -31, -12, -16, -11 ]
40
+
41
+ racc_goto_table = [
42
+ 1, 34, 35, 30, 18, 36, 31, nil, nil, 29,
43
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 41 ]
44
+
45
+ racc_goto_check = [
46
+ 1, 2, 3, 6, 9, 10, 11, nil, nil, 1,
47
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 1 ]
48
+
49
+ racc_goto_pointer = [
50
+ nil, 0, -14, -14, nil, nil, -7, nil, nil, -4,
51
+ -13, -5 ]
52
+
53
+ racc_goto_default = [
54
+ nil, nil, 2, 3, 4, 5, 6, 7, 8, nil,
55
+ nil, 12 ]
56
+
57
+ racc_reduce_table = [
58
+ 0, 0, :racc_error,
59
+ 1, 27, :_reduce_none,
60
+ 3, 27, :_reduce_2,
61
+ 1, 28, :_reduce_none,
62
+ 3, 28, :_reduce_4,
63
+ 1, 29, :_reduce_none,
64
+ 1, 29, :_reduce_none,
65
+ 1, 29, :_reduce_none,
66
+ 1, 29, :_reduce_none,
67
+ 2, 30, :_reduce_9,
68
+ 3, 30, :_reduce_10,
69
+ 4, 31, :_reduce_11,
70
+ 3, 32, :_reduce_12,
71
+ 2, 33, :_reduce_13,
72
+ 2, 34, :_reduce_14,
73
+ 1, 34, :_reduce_15,
74
+ 3, 37, :_reduce_16,
75
+ 1, 37, :_reduce_17,
76
+ 1, 35, :_reduce_none,
77
+ 1, 35, :_reduce_none,
78
+ 1, 35, :_reduce_none,
79
+ 1, 35, :_reduce_none,
80
+ 1, 35, :_reduce_none,
81
+ 1, 35, :_reduce_none,
82
+ 1, 35, :_reduce_none,
83
+ 1, 35, :_reduce_none,
84
+ 1, 35, :_reduce_none,
85
+ 1, 36, :_reduce_none,
86
+ 1, 36, :_reduce_none,
87
+ 1, 36, :_reduce_none,
88
+ 1, 36, :_reduce_none ]
89
+
90
+ racc_reduce_n = 31
91
+
92
+ racc_shift_n = 45
93
+
94
+ racc_token_table = {
95
+ false => 0,
96
+ :error => 1,
97
+ :EQ => 2,
98
+ :NE => 3,
99
+ :GT => 4,
100
+ :GE => 5,
101
+ :LT => 6,
102
+ :LE => 7,
103
+ :CO => 8,
104
+ :SW => 9,
105
+ :EW => 10,
106
+ :PR => 11,
107
+ :AND => 12,
108
+ :OR => 13,
109
+ :NOT => 14,
110
+ :LPAREN => 15,
111
+ :RPAREN => 16,
112
+ :LBRACKET => 17,
113
+ :RBRACKET => 18,
114
+ :NULL => 19,
115
+ :BOOLEAN => 20,
116
+ :NUMBER => 21,
117
+ :STRING => 22,
118
+ :ATTRNAME => 23,
119
+ :SCHEMA => 24,
120
+ :DOT => 25 }
121
+
122
+ racc_nt_base = 26
123
+
124
+ racc_use_result_var = true
125
+
126
+ Racc_arg = [
127
+ racc_action_table,
128
+ racc_action_check,
129
+ racc_action_default,
130
+ racc_action_pointer,
131
+ racc_goto_table,
132
+ racc_goto_check,
133
+ racc_goto_default,
134
+ racc_goto_pointer,
135
+ racc_nt_base,
136
+ racc_reduce_table,
137
+ racc_token_table,
138
+ racc_shift_n,
139
+ racc_reduce_n,
140
+ racc_use_result_var ]
141
+
142
+ Racc_token_to_s_table = [
143
+ "$end",
144
+ "error",
145
+ "EQ",
146
+ "NE",
147
+ "GT",
148
+ "GE",
149
+ "LT",
150
+ "LE",
151
+ "CO",
152
+ "SW",
153
+ "EW",
154
+ "PR",
155
+ "AND",
156
+ "OR",
157
+ "NOT",
158
+ "LPAREN",
159
+ "RPAREN",
160
+ "LBRACKET",
161
+ "RBRACKET",
162
+ "NULL",
163
+ "BOOLEAN",
164
+ "NUMBER",
165
+ "STRING",
166
+ "ATTRNAME",
167
+ "SCHEMA",
168
+ "DOT",
169
+ "$start",
170
+ "filter",
171
+ "non_or_filter",
172
+ "non_boolean_filter",
173
+ "attribute_filter",
174
+ "nested_filter",
175
+ "grouped_filter",
176
+ "not_filter",
177
+ "attr_path",
178
+ "comp_op",
179
+ "comp_value",
180
+ "attr_path_elements" ]
181
+
182
+ Racc_debug_parser = false
183
+
184
+ ##### State transition tables end #####
185
+
186
+ # reduce 0 omitted
187
+
188
+ # reduce 1 omitted
189
+
190
+ module_eval(<<'.,.,', 'parser.racc', 20)
191
+ def _reduce_2(val, _values, result)
192
+ filter1, op, filter2 = val
193
+ result = handler.on_logical_filter(filter1, filter2, op: op, context: result)
194
+
195
+ result
196
+ end
197
+ .,.,
198
+
199
+ # reduce 3 omitted
200
+
201
+ module_eval(<<'.,.,', 'parser.racc', 28)
202
+ def _reduce_4(val, _values, result)
203
+ filter1, op, filter2 = val
204
+ result = handler.on_logical_filter(filter1, filter2, op: op, context: result)
205
+
206
+ result
207
+ end
208
+ .,.,
209
+
210
+ # reduce 5 omitted
211
+
212
+ # reduce 6 omitted
213
+
214
+ # reduce 7 omitted
215
+
216
+ # reduce 8 omitted
217
+
218
+ module_eval(<<'.,.,', 'parser.racc', 41)
219
+ def _reduce_9(val, _values, result)
220
+ attr, op = val
221
+ result = handler.on_attribute_filter(attr[:path], nil, op: op, schema: attr[:schema], context: result)
222
+
223
+ result
224
+ end
225
+ .,.,
226
+
227
+ module_eval(<<'.,.,', 'parser.racc', 46)
228
+ def _reduce_10(val, _values, result)
229
+ attr, op, v = val
230
+ result = handler.on_attribute_filter(attr[:path], v, op: op, schema: attr[:schema], context: result)
231
+
232
+ result
233
+ end
234
+ .,.,
235
+
236
+ module_eval(<<'.,.,', 'parser.racc', 53)
237
+ def _reduce_11(val, _values, result)
238
+ attr, _, filter, _ = val
239
+ result = handler.on_nested_filter(attr[:path], filter, schema: attr[:schema], context: result)
240
+
241
+ result
242
+ end
243
+ .,.,
244
+
245
+ module_eval(<<'.,.,', 'parser.racc', 60)
246
+ def _reduce_12(val, _values, result)
247
+ result = val[1]
248
+
249
+ result
250
+ end
251
+ .,.,
252
+
253
+ module_eval(<<'.,.,', 'parser.racc', 66)
254
+ def _reduce_13(val, _values, result)
255
+ _, filter = val
256
+ result = handler.on_not_filter(filter, context: result)
257
+
258
+ result
259
+ end
260
+ .,.,
261
+
262
+ module_eval(<<'.,.,', 'parser.racc', 73)
263
+ def _reduce_14(val, _values, result)
264
+ schema, path = val
265
+ result = { schema: schema, path: path }
266
+
267
+ result
268
+ end
269
+ .,.,
270
+
271
+ module_eval(<<'.,.,', 'parser.racc', 78)
272
+ def _reduce_15(val, _values, result)
273
+ result = { path: val.last }
274
+
275
+ result
276
+ end
277
+ .,.,
278
+
279
+ module_eval(<<'.,.,', 'parser.racc', 84)
280
+ def _reduce_16(val, _values, result)
281
+ result = [*val.first, val.last]
282
+
283
+ result
284
+ end
285
+ .,.,
286
+
287
+ module_eval(<<'.,.,', 'parser.racc', 88)
288
+ def _reduce_17(val, _values, result)
289
+ result = [val.last]
290
+
291
+ result
292
+ end
293
+ .,.,
294
+
295
+ # reduce 18 omitted
296
+
297
+ # reduce 19 omitted
298
+
299
+ # reduce 20 omitted
300
+
301
+ # reduce 21 omitted
302
+
303
+ # reduce 22 omitted
304
+
305
+ # reduce 23 omitted
306
+
307
+ # reduce 24 omitted
308
+
309
+ # reduce 25 omitted
310
+
311
+ # reduce 26 omitted
312
+
313
+ # reduce 27 omitted
314
+
315
+ # reduce 28 omitted
316
+
317
+ # reduce 29 omitted
318
+
319
+ # reduce 30 omitted
320
+
321
+ def _reduce_none(val, _values, result)
322
+ val[0]
323
+ end
324
+
325
+ end # class Parser
326
+ end # module Filter
327
+ end # module Scim2
@@ -0,0 +1,109 @@
1
+ module Scim2
2
+ module Filter
3
+ ##
4
+ # Reference implementation of parser handler which captures parsed filters into a deeply nested Hash structure.
5
+ #
6
+ # @example
7
+ # # userName sw "J"
8
+ #
9
+ # {
10
+ # sw: {
11
+ # path: [:userName],
12
+ # schema: nil,
13
+ # value: 'J',
14
+ # },
15
+ # }
16
+ #
17
+ # @example
18
+ # # urn:ietf:params:scim:schemas:core:2.0:User:userType ne "Employee" and not (emails.value co "example.com" or emails.value co "example.org")
19
+ #
20
+ # {
21
+ # and: [
22
+ # {
23
+ # ne: {
24
+ # path: [:userType],
25
+ # schema: 'urn:ietf:params:scim:schemas:core:2.0:User',
26
+ # value: 'Employee',
27
+ # },
28
+ # },
29
+ # {
30
+ # not: {
31
+ # or: [
32
+ # {
33
+ # co: {
34
+ # path: %i[emails value],
35
+ # schema: nil,
36
+ # value: 'example.com',
37
+ # },
38
+ # },
39
+ # {
40
+ # co: {
41
+ # path: %i[emails value],
42
+ # schema: nil,
43
+ # value: 'example.org',
44
+ # },
45
+ # },
46
+ # ],
47
+ # },
48
+ # },
49
+ # ],
50
+ # }
51
+ #
52
+ # @example
53
+ # # emails[type eq "work"]
54
+ #
55
+ # {
56
+ # path: [:emails],
57
+ # schema: nil,
58
+ # nested: {
59
+ # eq: {
60
+ # path: [:type],
61
+ # schema: nil,
62
+ # value: 'work',
63
+ # },
64
+ # },
65
+ # }
66
+ #
67
+ # @note This handler is intended only as a reference implementation for custom
68
+ # handlers and is otherwise not designed for production use.
69
+ ##
70
+ class SimpleHandler
71
+
72
+ # Handle NOT filters (e.g. `not (color eq "red")`)
73
+ # @param filter [Hash<Symbol, Object>] the internal filter being NOT'ed
74
+ # @return [Hash<Symbol, Object>]
75
+ def on_not_filter(filter, context:)
76
+ { not: filter }
77
+ end
78
+
79
+ # Handle basic attribute comparison filters (e.g. `preference.color eq "red"`)
80
+ # @param attribute_path [Array<Symbol>] the attribute name(s) being filtered on, split by `.`
81
+ # @param value [Object] the value being compared against
82
+ # @param op [Object] the comparison operator (e.g. `:eq`)
83
+ # @param schema [String] schema namespace of the attribute
84
+ # @return [Hash<Symbol, Object>]
85
+ def on_attribute_filter(attribute_path, value, context:, op:, schema: nil)
86
+ { op => { path: attribute_path, value: value, schema: schema } }
87
+ end
88
+
89
+ # Handle logical filters (e.g. `name.givenName sw "D" AND title co "VP"`)
90
+ # @param filter1 [Hash<Symbol, Object>] the left-hand side filter
91
+ # @param filter2 [Hash<Symbol, Object>] the right-hand side filter
92
+ # @param op [Object] the logical operator (e.g. `AND`)
93
+ # @return [Hash<Symbol, Object>]
94
+ def on_logical_filter(filter1, filter2, context:, op:)
95
+ { op => [filter1, filter2] }
96
+ end
97
+
98
+ # Handle nested filters (e.g. `emails[type eq "work"]`)
99
+ # @param attribute_path [Array<Symbol>] the attribute name(s) being filtered on, split by `.`
100
+ # @param filter [Hash<Symbol, Object>] the nested-filter inside the backets
101
+ # @param schema [String] schema namespace of the attribute
102
+ # @return [Hash<Symbol, Object>]
103
+ def on_nested_filter(attribute_path, filter, context:, schema: nil)
104
+ { path: attribute_path, nested: filter, schema: schema }
105
+ end
106
+
107
+ end
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scim2-filter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - David McCullars
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-05-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: racc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redcarpet
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rexical
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.4'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.4'
125
+ - !ruby/object:Gem::Dependency
126
+ name: yard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: |
140
+ RFC7644 SCIM (System for Cross-domain Identity Management) 2.0 filter parser.
141
+ See https://tools.ietf.org/html/rfc7644#section-3.4.2.2
142
+
143
+ This gem implements a filter syntax parser as well as an optional integration
144
+ for integrating the filter with an Arel table.
145
+ email: david.mccullars@gmail.com
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - README.md
151
+ - lib/scim2-filter.rb
152
+ - lib/scim2.rb
153
+ - lib/scim2/filter.rb
154
+ - lib/scim2/filter/arel_handler.rb
155
+ - lib/scim2/filter/lexer.rb
156
+ - lib/scim2/filter/lexer.rex
157
+ - lib/scim2/filter/lexer.rex.rb
158
+ - lib/scim2/filter/no_op_handler.rb
159
+ - lib/scim2/filter/parser.racc
160
+ - lib/scim2/filter/parser.rb
161
+ - lib/scim2/filter/parser.tab.rb
162
+ - lib/scim2/filter/simple_handler.rb
163
+ homepage: https://github.com/david-mccullars/scim2-filter
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubygems_version: 3.2.3
183
+ signing_key:
184
+ specification_version: 4
185
+ summary: Parser for SCIM query filters
186
+ test_files: []