scim2-filter 0.2.0

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.
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: []