csdl 0.1.0 → 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 +4 -4
- data/README.md +48 -39
- data/csdl.gemspec +8 -3
- data/lib/csdl.rb +9 -0
- data/lib/csdl/builder.rb +291 -17
- data/lib/csdl/interaction_filter_processor.rb +147 -10
- data/lib/csdl/operators.rb +28 -2
- data/lib/csdl/processor.rb +204 -30
- data/lib/csdl/query_filter_processor.rb +25 -0
- data/lib/csdl/targets.rb +59 -2
- data/lib/csdl/version.rb +2 -2
- metadata +22 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a90262fd16ee8936dd0fe2de172ca90a443603f6
|
4
|
+
data.tar.gz: 89cb93fa4bdcb617dbd066edc004bde34f33c1af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 485e7164255da714fb1d5bc59e4a9d01b7568e6a58acb5c6b865d85bcef56063cc4c904482482e45c9bdbcb82fab90f097a99fbc2778f2b2b8b46c49c041f494
|
7
|
+
data.tar.gz: 55d987844a46743cc6013402225279458aa33419ea38b38e5226ea1ea665debff6124f8671030d76df24236f4d506e8f16fef13129c63ecc5470159a48b7530b
|
data/README.md
CHANGED
@@ -3,7 +3,10 @@
|
|
3
3
|
CSDL is a gem for producing Abstract Syntax Trees for the [DataSift CSDL Filter Language](http://dev.datasift.com/docs/csdl).
|
4
4
|
Working with an AST instead of raw strings provides a simpler way to test and validate any given CSDL filter.
|
5
5
|
|
6
|
+
[](http://badge.fury.io/rb/csdl)
|
6
7
|
[](https://travis-ci.org/localshred/csdl)
|
8
|
+
[](http://www.rubydoc.info/gems/csdl)
|
9
|
+
[](http://inch-ci.org/github/localshred/csdl)
|
7
10
|
|
8
11
|
## Installation
|
9
12
|
|
@@ -25,41 +28,47 @@ Or install it yourself as:
|
|
25
28
|
|
26
29
|
Use the DSL provided by `CSDL::Builder` to produce an AST representation of your query, and use `CSDL::Processor` to turn your AST into a raw CSDL string.
|
27
30
|
|
28
|
-
|
31
|
+
Be sure to read the [Processor](http://www.rubydoc.info/gems/csdl/CSDL/Processor) and [Processor](http://www.rubydoc.info/gems/csdl/CSDL/Builder) docs if you get stuck.
|
32
|
+
|
33
|
+
Valid builder methods are:
|
34
|
+
|
35
|
+
- `_and` - `AND`s two or more child statements together.
|
36
|
+
- `_not` - Negates a `condition` statement.
|
37
|
+
- `_or` - `OR`s two or more child statements together.
|
38
|
+
- `_return` - Creates a return statement with an implicit `statement_scope`.
|
39
|
+
- `condition` - Builds a `target + operator + argument` group. Ensures `target` and `operator` are valid.
|
40
|
+
- `logical_group` - Create a parenthetical grouping for nested statements. Optionally takes a logical operator as the first argument since we commonly want to wrap OR'd or AND'd statements in a logical group.
|
41
|
+
- `statement_scope` - Create a braced grouping for nested statements used by tag and return blocks.
|
42
|
+
- `tag_tree` - Builds a tag tree classifier (e.g. `tag.movies "Video" { ... }`).
|
43
|
+
- `tag` - Builds a tag classifier (e.g. `tag "Desire" { ... }`).
|
44
|
+
|
45
|
+
Methods prefixed with "\_" are to avoid ruby keyword collisions.
|
29
46
|
|
30
47
|
```ruby
|
31
48
|
builder = ::CSDL::Builder.new._or do
|
32
49
|
[
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
_not("fb.content", :contains_any, "government,politics"),
|
45
|
-
filter("fb.author.country_code", :in, "GB")
|
46
|
-
]
|
47
|
-
}
|
50
|
+
logical_group(:and) {
|
51
|
+
[
|
52
|
+
logical_group(:or) {
|
53
|
+
[
|
54
|
+
condition("fb.content", :contains_any, "ebola"),
|
55
|
+
condition("fb.parent.content", :contains_any, "ebola")
|
56
|
+
]
|
57
|
+
},
|
58
|
+
_not("fb.content", :contains_any, "government,politics"),
|
59
|
+
condition("fb.author.country_code", :in, "GB")
|
60
|
+
]
|
48
61
|
},
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
},
|
60
|
-
_not("fb.content", :contains_any, "vacation,poker awards")
|
61
|
-
]
|
62
|
-
}
|
62
|
+
logical_group(:and) {
|
63
|
+
[
|
64
|
+
logical_group(:or) {
|
65
|
+
[
|
66
|
+
condition("fb.content", :contains_any, "malta,malta island,#malta"),
|
67
|
+
condition("fb.parent.content", :contains_any, "malta,malta island,#malta")
|
68
|
+
]
|
69
|
+
},
|
70
|
+
_not("fb.content", :contains_any, "vacation,poker awards")
|
71
|
+
]
|
63
72
|
}
|
64
73
|
]
|
65
74
|
end
|
@@ -78,16 +87,16 @@ The previous script produces the following output:
|
|
78
87
|
```
|
79
88
|
Builder...
|
80
89
|
(or
|
81
|
-
(
|
90
|
+
(logical_group
|
82
91
|
(and
|
83
|
-
(
|
92
|
+
(logical_group
|
84
93
|
(or
|
85
|
-
(
|
94
|
+
(condition
|
86
95
|
(target "fb.content")
|
87
96
|
(operator :contains_any)
|
88
97
|
(argument
|
89
98
|
(string "ebola")))
|
90
|
-
(
|
99
|
+
(condition
|
91
100
|
(target "fb.parent.content")
|
92
101
|
(operator :contains_any)
|
93
102
|
(argument
|
@@ -97,21 +106,21 @@ Builder...
|
|
97
106
|
(operator :contains_any)
|
98
107
|
(argument
|
99
108
|
(string "government,politics")))
|
100
|
-
(
|
109
|
+
(condition
|
101
110
|
(target "fb.author.country_code")
|
102
111
|
(operator :in)
|
103
112
|
(argument
|
104
113
|
(string "GB")))))
|
105
|
-
(
|
114
|
+
(logical_group
|
106
115
|
(and
|
107
|
-
(
|
116
|
+
(logical_group
|
108
117
|
(or
|
109
|
-
(
|
118
|
+
(condition
|
110
119
|
(target "fb.content")
|
111
120
|
(operator :contains_any)
|
112
121
|
(argument
|
113
122
|
(string "malta,malta island,#malta")))
|
114
|
-
(
|
123
|
+
(condition
|
115
124
|
(target "fb.parent.content")
|
116
125
|
(operator :contains_any)
|
117
126
|
(argument
|
data/csdl.gemspec
CHANGED
@@ -5,13 +5,17 @@ require 'csdl/version'
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "csdl"
|
8
|
-
spec.version =
|
8
|
+
spec.version = CSDL::VERSION
|
9
9
|
spec.authors = ["BJ Neilsen"]
|
10
10
|
spec.email = ["bj.neilsen@gmail.com"]
|
11
11
|
|
12
12
|
spec.summary = %q{AST Processor and Query Builder for DataSift's CSDL language}
|
13
|
-
spec.description =
|
14
|
-
|
13
|
+
spec.description = %q{
|
14
|
+
CSDL is a gem for producing Abstract Syntax Trees for the [DataSift CSDL Filter Language](http://dev.datasift.com/docs/csdl).
|
15
|
+
Working with an AST instead of raw strings provides a simpler way to test and validate any given CSDL filter.
|
16
|
+
}
|
17
|
+
|
18
|
+
spec.homepage = "https://github.com/localshred/csdl"
|
15
19
|
|
16
20
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
21
|
spec.require_paths = ["lib"]
|
@@ -21,4 +25,5 @@ Gem::Specification.new do |spec|
|
|
21
25
|
spec.add_development_dependency "bundler", "~> 1.10"
|
22
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
27
|
spec.add_development_dependency "minitest"
|
28
|
+
spec.add_development_dependency "yard"
|
24
29
|
end
|
data/lib/csdl.rb
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
require "ast"
|
2
2
|
require "csdl/version"
|
3
3
|
|
4
|
+
# CSDL is a library for manipulating Abstract Syntax Trees that represent raw CSDL defintions. Using an AST
|
5
|
+
# makes it much simpler to manipulate, traverse, validate, and test complex CSDL queries.
|
6
|
+
#
|
7
|
+
# Use the DSL {Builder} class to produce a tree of nodes, then process those nodes
|
8
|
+
# with {Processor}, {InteractionFilterProcessor}, or {QueryFilterProcessor}. See
|
9
|
+
# those individuals classes for usage documentation.
|
10
|
+
#
|
11
|
+
# @see http://dev.datasift.com/docs/csdl DataSift CSDL Language Documentation
|
12
|
+
#
|
4
13
|
module CSDL
|
5
14
|
end
|
6
15
|
|
data/lib/csdl/builder.rb
CHANGED
@@ -1,30 +1,143 @@
|
|
1
1
|
module CSDL
|
2
|
+
|
3
|
+
# {Builder} is a class used to produce {http://www.rubydoc.info/gems/ast/AST/Node AST::Node} objects built to be processed
|
4
|
+
# by any one of {Processor}, {InteractionFilterProcessor}, or {QueryFilterProcessor}.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# # Generate your CSDL nodes using the Builder
|
8
|
+
# root_node = CSDL::Builder.new.logical_group(:or) do
|
9
|
+
# #...
|
10
|
+
# condition("fb.content", :contains, "match this string")
|
11
|
+
# #...
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # Process the root node and its children calling the instance method #process
|
15
|
+
# CSDL::Processor.new.process(root_node)
|
16
|
+
#
|
17
|
+
# @see http://dev.datasift.com/docs/csdl DataSift CSDL Language Documentation
|
18
|
+
#
|
2
19
|
class Builder
|
3
20
|
include ::AST::Sexp
|
4
21
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
22
|
+
# Logically AND two or more child nodes together. Does not implicitly create a logical group with parentheses.
|
23
|
+
# If you want to logically group the ANDs, see {#logical_group}.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# nodes = CSDL::Builder.new._and do
|
27
|
+
# [
|
28
|
+
# condition("fb.content", :contains, "this is a string"),
|
29
|
+
# condition("fb.parent.content", :contains, "this is a string"),
|
30
|
+
# ]
|
31
|
+
# end
|
32
|
+
# CSDL::Processor.new.process(nodes) # => 'fb.content contains "this is a string" AND fb.parent.content contains "this is a string"'
|
33
|
+
#
|
34
|
+
# @param block [Proc] Block to return child nodes to apply to this :and node. Block is evaluated against the builder instance.
|
35
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by an :and node.
|
36
|
+
#
|
37
|
+
# @return [AST::Node] An AST :and node with its children being the node(s) returned by the block.
|
38
|
+
#
|
39
|
+
# @see #logical_group
|
40
|
+
#
|
14
41
|
def _and(&block)
|
15
42
|
s(:and, *__one_or_more_child_nodes(&block))
|
16
43
|
end
|
17
44
|
|
45
|
+
# Negate a condition. Analogous to {#condition} with NOT prepended to the condition.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# node = CSDL::Builder.new._not("fb.content", :contains, "do not match this string")
|
49
|
+
# CSDL::Processor.new.process(node) # => 'NOT fb.content contains "do not match this string"'
|
50
|
+
#
|
51
|
+
# @example Multiple negations ANDed together
|
52
|
+
# nodes = CSDL::Builder.new._and do
|
53
|
+
# [
|
54
|
+
# _not("fb.content", :contains, "this is a string"),
|
55
|
+
# _not("fb.parent.content", :contains, "this is a string"),
|
56
|
+
# ]
|
57
|
+
# end
|
58
|
+
# CSDL::Processor.new.process(nodes) # => 'NOT fb.content contains "this is a string" AND NOT fb.parent.content contains "this is a string"'
|
59
|
+
#
|
60
|
+
# @param target [#to_s] A valid Target specifier (see {CSDL::TARGETS}).
|
61
|
+
# @param operator [#to_s] A valid Operator specifier (see {CSDL::OPERATORS}).
|
62
|
+
# @param argument [String, Numeric, nil] The comparator value, if applicable for the given operator.
|
63
|
+
#
|
64
|
+
# @return [AST::Node] An AST :not node with child target, operator, and argument nodes.
|
65
|
+
#
|
66
|
+
# @see #condition
|
67
|
+
#
|
18
68
|
def _not(target, operator, argument = nil)
|
19
|
-
node =
|
69
|
+
node = condition(target, operator, argument)
|
20
70
|
node.updated(:not)
|
21
71
|
end
|
22
72
|
|
73
|
+
# Logically OR two or more child nodes together. Does not implicitly create a logical group with parentheses.
|
74
|
+
# If you want to logically group the ORs, see {#logical_group}.
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# nodes = CSDL::Builder.new._or do
|
78
|
+
# [
|
79
|
+
# condition("fb.content", :contains, "this is a string"),
|
80
|
+
# condition("fb.parent.content", :contains, "this is a string")
|
81
|
+
# ]
|
82
|
+
# end
|
83
|
+
# CSDL::Processor.new.process(nodes) # => 'fb.content contains "this is a string" OR fb.parent.content contains "this is a string"'
|
84
|
+
#
|
85
|
+
# @param block [Proc] Block to return child nodes to apply to this :or node. Block is evaluated against the builder instance.
|
86
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by an :or node.
|
87
|
+
# @return [AST::Node] An AST :or node with its children being the node(s) returned by the block.
|
88
|
+
#
|
89
|
+
# @see #logical_group
|
90
|
+
#
|
91
|
+
def _or(&block)
|
92
|
+
s(:or, *__one_or_more_child_nodes(&block))
|
93
|
+
end
|
94
|
+
|
95
|
+
# Wrap child nodes in a return statement scope.
|
96
|
+
#
|
97
|
+
# @note The base {Processor} will not process return statement nodes, use {InteractionFilterProcessor} instead.
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# nodes = CSDL::Builder.new._return do
|
101
|
+
# condition("fb.content", :contains, "this is a string")
|
102
|
+
# end
|
103
|
+
# CSDL::InteractionFilterProcessor.new.process(nodes) # => 'return {fb.content contains "this is a string"}'
|
104
|
+
#
|
105
|
+
# @param block [Proc] Block to return child nodes to apply to a :statement_scope node. Block is evaluated against the builder instance.
|
106
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by a :statement_scope node.
|
107
|
+
#
|
108
|
+
# @return [AST::Node] An AST :return node with a single child :statement_scope node, its children being the node(s) returned by the block.
|
109
|
+
#
|
110
|
+
# @see #statement_scope
|
111
|
+
#
|
23
112
|
def _return(&block)
|
24
113
|
s(:return, statement_scope(&block))
|
25
114
|
end
|
26
115
|
|
27
|
-
|
116
|
+
# Create a "target + operator[ + argument]" CSDL condition. This method is the workhorse of any CSDL Filter.
|
117
|
+
# See {#_not} if you wish to negate a single condition.
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# node = CSDL::Builder.new.condition("fb.content", :contains, "match this string")
|
121
|
+
# CSDL::Processor.new.process(node) # => 'fb.content contains "match this string"'
|
122
|
+
#
|
123
|
+
# @example Multiple conditions ANDed together
|
124
|
+
# nodes = CSDL::Builder.new._and do
|
125
|
+
# [
|
126
|
+
# condition("fb.content", :contains, "this is a string"),
|
127
|
+
# condition("fb.parent.content", :contains, "this is a string"),
|
128
|
+
# ]
|
129
|
+
# end
|
130
|
+
# CSDL::Processor.new.process(nodes) # => 'fb.content contains "this is a string" AND fb.parent.content contains "this is a string"'
|
131
|
+
#
|
132
|
+
# @param target [#to_s] A valid Target specifier (see {CSDL::TARGETS}).
|
133
|
+
# @param operator [#to_s] A valid Operator specifier (see {CSDL::OPERATORS}).
|
134
|
+
# @param argument [String, Numeric, nil] The comparator value, if applicable for the given operator.
|
135
|
+
#
|
136
|
+
# @return [AST::Node] An AST :condition node with child target, operator, and argument nodes.
|
137
|
+
#
|
138
|
+
# @see #_not
|
139
|
+
#
|
140
|
+
def condition(target, operator, argument = nil)
|
28
141
|
target_node = s(:target, target)
|
29
142
|
operator_node = s(:operator, operator)
|
30
143
|
argument_node = nil
|
@@ -35,9 +148,72 @@ module CSDL
|
|
35
148
|
argument_node = s(:argument, child_argument_node)
|
36
149
|
end
|
37
150
|
|
38
|
-
s(:
|
151
|
+
s(:condition, *[target_node, operator_node, argument_node].compact)
|
39
152
|
end
|
40
153
|
|
154
|
+
# Wrap any child nodes in a logical grouping with parentheses. Additionally specify a logical
|
155
|
+
# operator to wrap all block child nodes. See {#_or} and {#_and}.
|
156
|
+
#
|
157
|
+
# @example
|
158
|
+
# nodes = CSDL::Builder.new.logical_group do
|
159
|
+
# condition("fb.content", :contains, "this is a string")
|
160
|
+
# end
|
161
|
+
# CSDL::Processor.new.process(nodes) # => '(fb.content contains "this is a string")'
|
162
|
+
#
|
163
|
+
# @example Without logical operator argument (default)
|
164
|
+
# nodes = CSDL::Builder.new.logical_group do
|
165
|
+
# _or do
|
166
|
+
# [
|
167
|
+
# condition("fb.content", :contains, "this is a string"),
|
168
|
+
# condition("fb.parent.content", :contains, "this is a string")
|
169
|
+
# ]
|
170
|
+
# end
|
171
|
+
# end
|
172
|
+
# CSDL::Processor.new.process(nodes) # => '(fb.content contains "this is a string" OR fb.parent.content contains "this is a string")'
|
173
|
+
#
|
174
|
+
# @example With logical operator argument, notice removal of _or block from previous example
|
175
|
+
# nodes = CSDL::Builder.new.logical_group(:or) do
|
176
|
+
# [
|
177
|
+
# condition("fb.content", :contains, "this is a string"),
|
178
|
+
# condition("fb.parent.content", :contains, "this is a string")
|
179
|
+
# ]
|
180
|
+
# end
|
181
|
+
# CSDL::Processor.new.process(nodes) # => '(fb.content contains "this is a string" OR fb.parent.content contains "this is a string")'
|
182
|
+
#
|
183
|
+
# @example Complex example
|
184
|
+
# nodes = CSDL::Builder.new._and do
|
185
|
+
# [
|
186
|
+
# logical_group(:or) {
|
187
|
+
# [
|
188
|
+
# condition("fb.content", :contains, "this is a string"),
|
189
|
+
# condition("fb.parent.content", :contains, "this is a string")
|
190
|
+
# ]
|
191
|
+
# },
|
192
|
+
# logical_group(:or) {
|
193
|
+
# [
|
194
|
+
# condition("fb.author.age", :==, "25-34"),
|
195
|
+
# condition("fb.parent.author.age", :==, "25-34")
|
196
|
+
# ]
|
197
|
+
# },
|
198
|
+
# logical_group(:or) {
|
199
|
+
# [
|
200
|
+
# condition("fb.author.gender", :==, "male"),
|
201
|
+
# condition("fb.parent.author.gender", :==, "male")
|
202
|
+
# ]
|
203
|
+
# },
|
204
|
+
# condition("fb.author.region", :==, "texas")
|
205
|
+
# ]
|
206
|
+
# end
|
207
|
+
# CSDL::Processor.new.process(nodes) # => '(fb.content contains "this is a string" OR fb.parent.content contains "this is a string") AND (fb.author.age == "25-34" OR fb.parent.author.age == "25-34") AND (fb.author.gender == "male" OR fb.parent.author.gender == "male") AND fb.author.region == "texas"'
|
208
|
+
#
|
209
|
+
# @param block [Proc] Block to return child nodes to apply to this :logical_group node. Block is evaluated against the builder instance.
|
210
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by a :logical_group node (and possibly also a node for the logical operator).
|
211
|
+
#
|
212
|
+
# @return [AST::Node] An AST :logical_operator node with its children being the node(s) returned by the block.
|
213
|
+
#
|
214
|
+
# @see #_and
|
215
|
+
# @see #_or
|
216
|
+
#
|
41
217
|
def logical_group(logical_operator = nil, &block)
|
42
218
|
if logical_operator.nil?
|
43
219
|
s(:logical_group, *__one_or_more_child_nodes(&block))
|
@@ -46,10 +222,80 @@ module CSDL
|
|
46
222
|
end
|
47
223
|
end
|
48
224
|
|
225
|
+
# Wrap child nodes in a root node. Useful for building CSDL with tagging and a return statement.
|
226
|
+
#
|
227
|
+
# @example
|
228
|
+
# nodes = CSDL::Builder.new.root do
|
229
|
+
# [
|
230
|
+
# tag_tree(["movies"], "Video") { condition("links.url", :any, "youtube.com,vimeo.com") },
|
231
|
+
# tag_tree(["movies"], "Social Networks") { condition("links.url", :any, "twitter.com,facebook.com") },
|
232
|
+
#
|
233
|
+
# return {
|
234
|
+
# _or {
|
235
|
+
# [
|
236
|
+
# condition("fb.topics.category", :in, "Movie,Film,TV"),
|
237
|
+
# condition("fb.parent.topics.category", :in, "Movie,Film,TV")
|
238
|
+
# ]
|
239
|
+
# }
|
240
|
+
# }
|
241
|
+
# ]
|
242
|
+
# end
|
243
|
+
# CSDL::InteractionFilterProcessor.new.process(nodes) # => 'tag.movies "Video" {links.url any "youtube.com,vimeo.com"} tag.movies "Social Networks" {links.url any "twitter.com,facebook.com"} return {fb.topics.category in "Movie,Film,TV" OR fb.parent.topics.cateogry in "Movie,Film,TV"}'
|
244
|
+
#
|
245
|
+
# @param block [Proc] Block to return child nodes to apply to this :statement_scope node. Block is evaluated against the builder instance.
|
246
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by a :statement_scope node.
|
247
|
+
#
|
248
|
+
# @return [AST::Node] An AST :statement_scope node with its children being the node(s) returned by the block.
|
249
|
+
#
|
250
|
+
# @see #_return
|
251
|
+
# @see #tag_tree
|
252
|
+
#
|
253
|
+
def root(&block)
|
254
|
+
s(:root, *__one_or_more_child_nodes(&block))
|
255
|
+
end
|
256
|
+
|
257
|
+
# Wrap child nodes in braces. @note Generally not useful on its own, see {#_return}, {#tag}, or {#tag_tree} usage.
|
258
|
+
#
|
259
|
+
# @note The base {Processor} will not process statement_scope nodes, use {InteractionFilterProcessor} instead.
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# nodes = CSDL::Builder.new.statement_scope do
|
263
|
+
# condition("fb.content", :contains, "this is a string")
|
264
|
+
# end
|
265
|
+
# CSDL::InteractionFilterProcessor.new.process(nodes) # => '{fb.content contains "this is a string"}'
|
266
|
+
#
|
267
|
+
# @param block [Proc] Block to return child nodes to apply to this :statement_scope node. Block is evaluated against the builder instance.
|
268
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by a :statement_scope node.
|
269
|
+
#
|
270
|
+
# @return [AST::Node] An AST :statement_scope node with its children being the node(s) returned by the block.
|
271
|
+
#
|
272
|
+
# @see #_return
|
273
|
+
# @see #tag
|
274
|
+
# @see #tag_tree
|
275
|
+
#
|
49
276
|
def statement_scope(&block)
|
50
277
|
s(:statement_scope, *__one_or_more_child_nodes(&block))
|
51
278
|
end
|
52
279
|
|
280
|
+
# Wrap child nodes in a VEDO tag classification.
|
281
|
+
#
|
282
|
+
# @note The base {Processor} will not process tag nodes, use {InteractionFilterProcessor} instead.
|
283
|
+
#
|
284
|
+
# @example
|
285
|
+
# nodes = CSDL::Builder.new.tag("MyTag") do
|
286
|
+
# condition("fb.content", :contains, "this is a string")
|
287
|
+
# end
|
288
|
+
# CSDL::InteractionFilterProcessor.new.process(nodes) # => 'tag "MyTag" {fb.content contains "this is a string"}'
|
289
|
+
#
|
290
|
+
# @param tag_class [#to_s] The tag classification.
|
291
|
+
# @param block [Proc] Block to return child nodes to apply to a :statement_scope node nested under the :tag node. Block is evaluated against the builder instance.
|
292
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by a :statement_scope node, to be a child of the returned :tag node.
|
293
|
+
#
|
294
|
+
# @return [AST::Node] An AST :tag node with a :tag_class child node and :statement_scope child node.
|
295
|
+
#
|
296
|
+
# @see #statement_scope
|
297
|
+
# @see #tag_tree
|
298
|
+
#
|
53
299
|
def tag(tag_class, &block)
|
54
300
|
s(:tag,
|
55
301
|
s(:tag_class,
|
@@ -57,17 +303,45 @@ module CSDL
|
|
57
303
|
statement_scope(&block))
|
58
304
|
end
|
59
305
|
|
60
|
-
|
61
|
-
|
62
|
-
|
306
|
+
# Wrap child nodes in a VEDO tag classification tree.
|
307
|
+
#
|
308
|
+
# @note The base {Processor} will not process tag_tree nodes, use {InteractionFilterProcessor} instead.
|
309
|
+
#
|
310
|
+
# @example
|
311
|
+
# nodes = CSDL::Builder.new.tag_tree(%w(foo bar), "MyTag") do
|
312
|
+
# condition("fb.content", :contains, "this is a string")
|
313
|
+
# end
|
314
|
+
# CSDL::InteractionFilterProcessor.new.process(nodes) # => 'tag.foo.bar "MyTag" {fb.content contains "this is a string"}'
|
315
|
+
#
|
316
|
+
# @param tag_namespaces [Array<#to_s>] List of classification namespaces.
|
317
|
+
# @param tag_class [#to_s] The tag classification.
|
318
|
+
# @param block [Proc] Block to return child nodes to apply to a :statement_scope node nested under the :tag node. Block is evaluated against the builder instance.
|
319
|
+
# @yieldreturn [AST::Node, Array<AST::Node>] An AST node or array of AST nodes to be wrapped by a :statement_scope node, to be a child of the returned :tag node.
|
320
|
+
#
|
321
|
+
# @return [AST::Node] An AST :tag node with a :tag_namespaces child node, :tag_class child node, and :statement_scope child node.
|
322
|
+
#
|
323
|
+
# @see #statement_scope
|
324
|
+
# @see #tag
|
325
|
+
#
|
326
|
+
def tag_tree(tag_namespaces, tag_class, &block)
|
327
|
+
tag_namespace_nodes = tag_namespaces.map do |tag_namespace|
|
328
|
+
s(:tag_namespace, tag_namespace)
|
63
329
|
end
|
64
330
|
|
65
331
|
s(:tag,
|
66
|
-
s(:
|
67
|
-
*
|
332
|
+
s(:tag_namespaces,
|
333
|
+
*tag_namespace_nodes),
|
68
334
|
s(:tag_class,
|
69
335
|
s(:string, tag_class)),
|
70
336
|
statement_scope(&block))
|
71
337
|
end
|
338
|
+
|
339
|
+
private
|
340
|
+
|
341
|
+
def __one_or_more_child_nodes(&block)
|
342
|
+
children = instance_eval(&block)
|
343
|
+
[ children ].flatten
|
344
|
+
end
|
345
|
+
|
72
346
|
end
|
73
347
|
end
|
@@ -1,6 +1,54 @@
|
|
1
1
|
module CSDL
|
2
|
+
|
3
|
+
# {InteractionFilterProcessor} is a class that inherits from {Processor}, providing additional methods
|
4
|
+
# for building CSDL specifically for Interaction Filters.
|
5
|
+
#
|
6
|
+
# Additional DSL methods provide the return statement, curly brace scopes (statement scopes), and VEDO tagging.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# nodes = CSDL::Builder.new.root do
|
10
|
+
# [
|
11
|
+
# tag_tree(%w(movies), "Video") {
|
12
|
+
# condition("links.url", :any, "youtube.com,vimeo.com")
|
13
|
+
# },
|
14
|
+
# tag_tree(%w(movies), "Social Networks") {
|
15
|
+
# condition("links.url", :any, "twitter.com,facebook.com")
|
16
|
+
# },
|
17
|
+
#
|
18
|
+
# return {
|
19
|
+
# _or {
|
20
|
+
# [
|
21
|
+
# condition("fb.topics.category", :in, "Movie,Film,TV"),
|
22
|
+
# condition("fb.parent.topics.category", :in, "Movie,Film,TV")
|
23
|
+
# ]
|
24
|
+
# }
|
25
|
+
# }
|
26
|
+
# ]
|
27
|
+
# end
|
28
|
+
# CSDL::InteractionFilterProcessor.new.process(nodes) # => 'tag.movies "Video" {links.url any "youtube.com,vimeo.com"} tag.movies "Social Networks" {links.url any "twitter.com,facebook.com"} return {fb.topics.category in "Movie,Film,TV" OR fb.parent.topics.cateogry in "Movie,Film,TV"}'
|
29
|
+
#
|
30
|
+
# @see Processor
|
31
|
+
# @see Builder
|
32
|
+
# @see http://www.rubydoc.info/gems/ast/AST/Processor AST::Processor
|
33
|
+
# @see http://www.rubydoc.info/gems/ast/AST/Node AST::Node
|
34
|
+
# @see http://dev.datasift.com/docs/csdl DataSift CSDL Language Documentation
|
35
|
+
#
|
2
36
|
class InteractionFilterProcessor < ::CSDL::Processor
|
3
37
|
|
38
|
+
# Generate a return statement by processing the child statement_scope node.
|
39
|
+
#
|
40
|
+
# @raise [MissingReturnStatementScopeError] When the :return node is missing a :statement_scope child node.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# node = s(:return,
|
44
|
+
# s(:statement_scope,
|
45
|
+
# s(:string, "foo")))
|
46
|
+
# CSDL::InteractionFilterProcessor.new.process(node) # => 'return {"foo"}'
|
47
|
+
#
|
48
|
+
# @param node [AST::Node] The :return node to be processed.
|
49
|
+
#
|
50
|
+
# @return [String] The processed :statement_scope child node, prepended by the "return" keyword.
|
51
|
+
#
|
4
52
|
def on_return(node)
|
5
53
|
statement_scope = node.children.find { |child| child.type == :statement_scope }
|
6
54
|
|
@@ -11,12 +59,55 @@ module CSDL
|
|
11
59
|
"return #{process(statement_scope)}"
|
12
60
|
end
|
13
61
|
|
62
|
+
# Wrap child nodes in braces. Generally not useful on its own, see {#on_return} or {#on_tag} for integrated usage.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# node = s(:statement_scope,
|
66
|
+
# s(:string, "foo"))
|
67
|
+
# CSDL::InteractionFilterProcessor.new.process(node) # => '{"foo"}'
|
68
|
+
#
|
69
|
+
# @param node [AST::Node] The :statement_scope node to be processed.
|
70
|
+
#
|
71
|
+
# @return [String] The processed child nodes, joined by an empty space and wrapped in braces.
|
72
|
+
#
|
73
|
+
# @see #on_return
|
74
|
+
# @see #on_tag
|
75
|
+
#
|
14
76
|
def on_statement_scope(node)
|
15
77
|
"{" + process_all(node.children).join(" ") + "}"
|
16
78
|
end
|
17
79
|
|
80
|
+
# Process :tag node with it's child nodes :tag_namespaces (optional), :tag_class, and :statement_scope.
|
81
|
+
#
|
82
|
+
# @example Tag Classification
|
83
|
+
# node = s(:tag,
|
84
|
+
# s(:tag_class,
|
85
|
+
# s(:string, "MyTag")),
|
86
|
+
# s(:statement_scope,
|
87
|
+
# s(:string, "foo")))
|
88
|
+
# CSDL::InteractionFilterProcessor.new.process(node) # => 'tag "MyTag" {"foo"}'
|
89
|
+
#
|
90
|
+
# @example Tag Tree Classification
|
91
|
+
# node = s(:tag,
|
92
|
+
# s(:tag_namespaces,
|
93
|
+
# s(:tag_namespace, "foo"),
|
94
|
+
# s(:tag_namespace, "bar"),
|
95
|
+
# s(:tag_namespace, "baz")),
|
96
|
+
# s(:tag_class,
|
97
|
+
# s(:string, "MyTag")),
|
98
|
+
# s(:statement_scope,
|
99
|
+
# s(:string, "value")))
|
100
|
+
# CSDL::InteractionFilterProcessor.new.process(node) # => 'tag.foo.bar.baz "MyTag" {"value"}'
|
101
|
+
#
|
102
|
+
# @param node [AST::Node] The :tag node to be processed.
|
103
|
+
#
|
104
|
+
# @return [String] The tag classifier raw CSDL.
|
105
|
+
#
|
106
|
+
# @raise [MissingTagClassError] When we don't have a first-level :tag_namespaces child.
|
107
|
+
# @raise [MissingTagStatementScopeError] When we don't have a first-level :statement_scope child.
|
108
|
+
#
|
18
109
|
def on_tag(node)
|
19
|
-
|
110
|
+
tag_namespaces = node.children.find { |child| child.type == :tag_namespaces }
|
20
111
|
tag_class = node.children.find { |child| child.type == :tag_class }
|
21
112
|
statement_scope = node.children.find { |child| child.type == :statement_scope }
|
22
113
|
|
@@ -29,32 +120,78 @@ module CSDL
|
|
29
120
|
end
|
30
121
|
|
31
122
|
tag_namespace = "tag"
|
32
|
-
unless
|
33
|
-
tag_namespace += process(
|
123
|
+
unless tag_namespaces.nil?
|
124
|
+
tag_namespace += process(tag_namespaces)
|
34
125
|
end
|
35
126
|
|
36
127
|
children = [tag_namespace] + process_all([ tag_class, statement_scope ])
|
37
128
|
children.join(" ")
|
38
129
|
end
|
39
130
|
|
131
|
+
# Process the first child of the :tag_class node.
|
132
|
+
#
|
133
|
+
# @param node [AST::Node] The :tag_class node to be processed.
|
134
|
+
#
|
135
|
+
# @return [String] The processed value of the first child node.
|
136
|
+
#
|
137
|
+
# @see #on_tag
|
138
|
+
#
|
40
139
|
def on_tag_class(node)
|
41
140
|
process(node.children.first)
|
42
141
|
end
|
43
142
|
|
44
|
-
|
143
|
+
# Process the terminal value of the :tag_namespace node.
|
144
|
+
#
|
145
|
+
# @param node [AST::Node] The :tag_namespace node to be processed.
|
146
|
+
#
|
147
|
+
# @return [String] Terminal value as a string.
|
148
|
+
#
|
149
|
+
# @see #on_tag_namespaces
|
150
|
+
#
|
151
|
+
def on_tag_namespace(node)
|
45
152
|
node.children.first.to_s
|
46
153
|
end
|
47
154
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
155
|
+
# Process the :tag_namespace child nodes of a :tag_namespaces node.
|
156
|
+
#
|
157
|
+
# @example
|
158
|
+
# node = s(:tag_namespaces,
|
159
|
+
# s(:tag_namespace, "foo"),
|
160
|
+
# s(:tag_namespace, "bar"),
|
161
|
+
# s(:tag_namespace, "baz"))
|
162
|
+
# CSDL::InteractionFilterProcessor.new.process(node) # => '.foo.bar.baz'
|
163
|
+
#
|
164
|
+
# @param node [AST::Node] The :tag_namespaces node to be processed.
|
165
|
+
#
|
166
|
+
# @return [String] Dot-delimited tag node namespace.
|
167
|
+
#
|
168
|
+
# @raise [MissingTagNodesError] When there aren't any first-level :tag_namespace child nodes.
|
169
|
+
#
|
170
|
+
def on_tag_namespaces(node)
|
171
|
+
child_tag_namespaces = node.children.select { |child| child.type == :tag_namespace }
|
172
|
+
|
173
|
+
if child_tag_namespaces.empty?
|
174
|
+
fail ::CSDL::MissingTagNodesError, "Invalid CSDL AST: A :tag_namespaces node must have at least one :tag_namespace child"
|
53
175
|
end
|
54
176
|
|
55
|
-
"." + process_all(
|
177
|
+
"." + process_all(child_tag_namespaces).join(".")
|
56
178
|
end
|
57
179
|
|
180
|
+
# Raises an {InvalidInteractionTargetError} if the target isn't a valid CSDL target for interaction filters. Will
|
181
|
+
# be called from the base class when given a :condition node with a :target node.
|
182
|
+
#
|
183
|
+
# @example
|
184
|
+
# CSDL::InteractionFilterProcessorProcessor.new.validate_target!("fake") # => raises InvalidInteractionTargetError
|
185
|
+
#
|
186
|
+
# @param target_key [String] The target to validate.
|
187
|
+
#
|
188
|
+
# @return [void]
|
189
|
+
#
|
190
|
+
# @raise [InvalidInteractionTargetError] When the terminator value is not a valid ineraction filter target. See {CSDL.interaction_target?}.
|
191
|
+
#
|
192
|
+
# @see Processor#on_condition
|
193
|
+
# @see Processor#on_target
|
194
|
+
#
|
58
195
|
def validate_target!(target_key)
|
59
196
|
unless ::CSDL.interaction_target?(target_key)
|
60
197
|
fail ::CSDL::InvalidInteractionTargetError, "Interaction filters cannot use target '#{target_key}'"
|
data/lib/csdl/operators.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
module CSDL
|
2
2
|
|
3
|
+
# A CSDL Operator definition with indication as to valid data types to be used with the operator.
|
4
|
+
#
|
5
|
+
# @attr name [String] The name of the operator.
|
6
|
+
# @attr argument_types [Array<Symbol>] List of valid argument node types that can be associated with an operator.
|
7
|
+
#
|
8
|
+
# @see OPERATORS
|
9
|
+
#
|
3
10
|
Operator = Struct.new(:name, :argument_types)
|
4
11
|
|
5
|
-
|
12
|
+
# A raw array of operators with their valid argument types.
|
13
|
+
#
|
14
|
+
# @return [Array<String, Array<Symbol>>] Array of operators used to produce {OPERATORS} hash.
|
15
|
+
#
|
16
|
+
RAW_OPERATORS = [
|
6
17
|
[ "contains" , [ :string ] ],
|
7
18
|
[ "cs contains" , [ :string ] ],
|
8
19
|
[ "substr" , [ :string ] ],
|
@@ -39,11 +50,26 @@ module CSDL
|
|
39
50
|
[ "geo_polygon" , [ :string ] ]
|
40
51
|
]
|
41
52
|
|
42
|
-
|
53
|
+
# All possible operators.
|
54
|
+
#
|
55
|
+
# @return [Hash<String, Operator>] Hash of {Operator} structs, keyed by the string name of the operator.
|
56
|
+
#
|
57
|
+
OPERATORS = RAW_OPERATORS.reduce({}) do |accumulator, (operator_name, argument_types)|
|
43
58
|
accumulator[operator_name] = Operator.new(operator_name, argument_types)
|
44
59
|
accumulator
|
45
60
|
end.freeze
|
46
61
|
|
62
|
+
# Check if the given target is a valid operator.
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# CSDL.operator?("fake") # => false
|
66
|
+
# CSDL.operator?("contains") # => true
|
67
|
+
# CSDL.operator?(">=") # => true
|
68
|
+
#
|
69
|
+
# @param operator [String] The name of the target.
|
70
|
+
#
|
71
|
+
# @return [Boolean] Whether or not the value is a valid CSDL Operator.
|
72
|
+
#
|
47
73
|
def self.operator?(operator)
|
48
74
|
OPERATORS.key?(operator)
|
49
75
|
end
|
data/lib/csdl/processor.rb
CHANGED
@@ -1,28 +1,142 @@
|
|
1
1
|
module CSDL
|
2
|
+
|
3
|
+
# {Processor} is a class that can take a tree of AST::Node objects built with {Builder}
|
4
|
+
# and produce a valid CSDL string representation according to DataSift's CSDL specification.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# # Generate your CSDL nodes using the Builder
|
8
|
+
# root_node = CSDL::Builder.new.logical_group(:or) do
|
9
|
+
# #...
|
10
|
+
# condition("fb.content", :contains, "match this string")
|
11
|
+
# #...
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # Process the root node and its children calling the instance method #process
|
15
|
+
# CSDL::Processor.new.process(root_node)
|
16
|
+
#
|
17
|
+
# @see Builder
|
18
|
+
# @see http://www.rubydoc.info/gems/ast/AST/Processor AST::Processor
|
19
|
+
# @see http://www.rubydoc.info/gems/ast/AST/Node AST::Node
|
20
|
+
# @see http://dev.datasift.com/docs/csdl DataSift CSDL Language Documentation
|
21
|
+
#
|
2
22
|
class Processor < ::AST::Processor
|
3
23
|
|
24
|
+
# AND two or more child nodes together.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# node = s(:and,
|
28
|
+
# s(:string, "foo"),
|
29
|
+
# s(:string, "bar"),
|
30
|
+
# s(:string, "baz"))
|
31
|
+
# CSDL::Processor.new.process(node) # => '"foo" AND "bar" AND "baz"'
|
32
|
+
#
|
33
|
+
# @param node [AST::Node] The :and node to be processed.
|
34
|
+
#
|
35
|
+
# @raise [MissingChildNodesError] When less than 2 child nodes are present.
|
36
|
+
#
|
37
|
+
# @return [String] Processed child nodes ANDed together into a raw CSDL string representation.
|
38
|
+
#
|
4
39
|
def on_and(node)
|
5
|
-
|
6
|
-
rest = node.children.drop(1)
|
7
|
-
|
8
|
-
rest.reduce(initial) do |csdl, child|
|
9
|
-
csdl += " AND " + process(child)
|
10
|
-
csdl
|
11
|
-
end
|
40
|
+
logically_join_nodes("AND", node.children)
|
12
41
|
end
|
13
42
|
|
43
|
+
# Process the first child node as the "argument" in a condition node tree (target + operator + argument).
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# node = s(:argument,
|
47
|
+
# s(:string, "foo"))
|
48
|
+
# CSDL::Processor.new.process(node) # => '"foo"'
|
49
|
+
#
|
50
|
+
# @param node [AST::Node] The :argument node to be processed.
|
51
|
+
#
|
52
|
+
# @return [String] The first child node, processed by its node type into a raw CSDL string representation.
|
53
|
+
#
|
54
|
+
# @todo Raise if the node doesn't have any children.
|
55
|
+
#
|
14
56
|
def on_argument(node)
|
15
57
|
process(node.children.first)
|
16
58
|
end
|
17
59
|
|
60
|
+
# Process :condition node and it's expected children :target, :operator, and :argument nodes.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# node = s(:condition,
|
64
|
+
# s(:target, "fb.content"),
|
65
|
+
# s(:operator, :contains_any),
|
66
|
+
# s(:argument,
|
67
|
+
# s(:string, "foo")))
|
68
|
+
# CSDL::Processor.new.process(node) # => 'fb.content contains_any "foo"'
|
69
|
+
#
|
70
|
+
# @param node [AST::Node] The :condition node to be processed.
|
71
|
+
#
|
72
|
+
# @return [String] The child nodes :target, :operator, and :argument, processed and joined.
|
73
|
+
#
|
74
|
+
# @todo Raise when we don't have a target node.
|
75
|
+
# @todo Raise when we don't have a operator node.
|
76
|
+
# @todo Raise when we don't have a argument node, assuming the operator is binary.
|
77
|
+
# @todo Raise if the argument node's child is not of a valid node type for the given operator.
|
78
|
+
#
|
79
|
+
def on_condition(node)
|
80
|
+
target = node.children.find { |child| child.type == :target }
|
81
|
+
operator = node.children.find { |child| child.type == :operator }
|
82
|
+
argument = node.children.find { |child| child.type == :argument }
|
83
|
+
process_all([ target, operator, argument ].compact).join(" ")
|
84
|
+
end
|
85
|
+
|
86
|
+
# Wrap all processed child nodes in parentheses.
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# node = s(:logical_group,
|
90
|
+
# s(:or,
|
91
|
+
# s(:string, "foo"),
|
92
|
+
# s(:string, "bar"),
|
93
|
+
# s(:string, "baz")))
|
94
|
+
# CSDL::Processor.new.process(node) # => '("foo" OR "bar" OR "baz")'
|
95
|
+
#
|
96
|
+
# @param node [AST::Node] The :logical_group node to be processed.
|
97
|
+
#
|
98
|
+
# @return [String] All child nodes processed by their node types into a raw CSDL string representation, wrapped in parentheses.
|
99
|
+
#
|
18
100
|
def on_logical_group(node)
|
19
101
|
"(" + process_all(node.children).join(" ") + ")"
|
20
102
|
end
|
21
103
|
|
104
|
+
# Process :not node as a :condition node, prepending the logical operator NOT to the processed :condition node.
|
105
|
+
#
|
106
|
+
# @example
|
107
|
+
# node = s(:not,
|
108
|
+
# s(:target, "fb.content"),
|
109
|
+
# s(:operator, :contains_any),
|
110
|
+
# s(:argument,
|
111
|
+
# s(:string, "foo")))
|
112
|
+
# CSDL::Processor.new.process(node) # => 'NOT fb.content contains_any "foo"'
|
113
|
+
#
|
114
|
+
# @param node [AST::Node] The :not node to be processed.
|
115
|
+
#
|
116
|
+
# @return [String] The child nodes :target, :operator, and :argument, processed and joined.
|
117
|
+
#
|
118
|
+
# @todo Raise when we don't have a target node.
|
119
|
+
# @todo Raise when we don't have a operator node.
|
120
|
+
# @todo Raise when we don't have a argument node, assuming the operator is binary.
|
121
|
+
# @todo Raise if the argument node's child is not of a valid node type for the given operator.
|
122
|
+
# @todo Support negating logical groupings.
|
123
|
+
#
|
22
124
|
def on_not(node)
|
23
|
-
"NOT " + process(node.updated(:
|
125
|
+
"NOT " + process(node.updated(:condition))
|
24
126
|
end
|
25
127
|
|
128
|
+
# Process :operator nodes, ensuring the the given terminator value is a valid operator.
|
129
|
+
#
|
130
|
+
# @example
|
131
|
+
# node = s(:operator, :contains)
|
132
|
+
# CSDL::Processor.new.process(node) # => 'contains'
|
133
|
+
#
|
134
|
+
# @param node [AST::Node] The :operator node to be processed.
|
135
|
+
#
|
136
|
+
# @return [String] The first child, stringified.
|
137
|
+
#
|
138
|
+
# @raise [UnknownOperatorError] When the terminator value is not a valid operator. See {CSDL.operator?}.
|
139
|
+
#
|
26
140
|
def on_operator(node)
|
27
141
|
operator = node.children.first.to_s
|
28
142
|
unless ::CSDL.operator?(operator)
|
@@ -31,44 +145,104 @@ module CSDL
|
|
31
145
|
operator
|
32
146
|
end
|
33
147
|
|
148
|
+
# OR two or more child nodes together.
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# node = s(:or,
|
152
|
+
# s(:string, "foo"),
|
153
|
+
# s(:string, "bar"),
|
154
|
+
# s(:string, "baz"))
|
155
|
+
# CSDL::Processor.new.process(node) # => '"foo" OR "bar" OR "baz"'
|
156
|
+
#
|
157
|
+
# @param node [AST::Node] The :or node to be processed.
|
158
|
+
#
|
159
|
+
# @raise [MissingChildNodesError] When less than 2 child nodes are present.
|
160
|
+
#
|
161
|
+
# @return [String] Processed child nodes OR'd together into a raw CSDL string representation.
|
162
|
+
#
|
34
163
|
def on_or(node)
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
|
39
|
-
initial = process(node.children.first)
|
40
|
-
rest = node.children.drop(1)
|
41
|
-
|
42
|
-
if rest.empty?
|
43
|
-
fail ::CSDL::MissingChildNodesError, "Invalid CSDL AST: 'or' nodes must contain at least two child nodes. Expected >= 2, got #{node.children.size}"
|
44
|
-
end
|
164
|
+
logically_join_nodes("OR", node.children)
|
165
|
+
end
|
45
166
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
167
|
+
# Process all child nodes. Useful for grouping child nodes without any syntax introduction.
|
168
|
+
#
|
169
|
+
# @see InteractionFilterProcessor#_return
|
170
|
+
# @see InteractionFilterProcessor#tag
|
171
|
+
# @see InteractionFilterProcessor#tag_tree
|
172
|
+
#
|
173
|
+
def on_root(node)
|
174
|
+
process_all(node.children).join(" ")
|
50
175
|
end
|
51
176
|
|
177
|
+
# Wrap the stringified terminal value in quotes.
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# node = s(:string, "foo")
|
181
|
+
# CSDL::Processor.new.process(node) # => '"foo"'
|
182
|
+
#
|
183
|
+
# @param node [AST::Node] The :string node to be processed.
|
184
|
+
#
|
185
|
+
# @return [String] The first child node, processed by its node type into a raw CSDL string representation.
|
186
|
+
#
|
187
|
+
# @todo Raise if the node doesn't have any children.
|
188
|
+
#
|
52
189
|
def on_string(node)
|
53
190
|
'"' + node.children.first.to_s.gsub(/"/, '\"') + '"'
|
54
191
|
end
|
55
192
|
|
193
|
+
# Process :target nodes, ensuring the the given terminator value is a valid operator.
|
194
|
+
#
|
195
|
+
# @example
|
196
|
+
# node = s(:target, "fb.content")
|
197
|
+
# CSDL::Processor.new.process(node) # => 'fb.content'
|
198
|
+
#
|
199
|
+
# @param node [AST::Node] The :target node to be processed.
|
200
|
+
#
|
201
|
+
# @return [String] The first child, stringified.
|
202
|
+
#
|
203
|
+
# @raise [UnknownTargetError] When the terminator value is not a valid operator. See {#validate_target!}.
|
204
|
+
#
|
205
|
+
# @see #validate_target!
|
206
|
+
#
|
56
207
|
def on_target(node)
|
57
208
|
target = node.children.first.to_s
|
58
209
|
validate_target!(target)
|
59
210
|
target
|
60
211
|
end
|
61
212
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
213
|
+
# Raises an {UnknownTargetError} if the target isn't a valid CSDL target. Useful for implenting a
|
214
|
+
# child processor that can ensure the target is known and valid for a given use-case
|
215
|
+
# (e.g. {InteractionFilterProcessor Interaction Filters} vs {QueryFilterProcessor Query Filters}).
|
216
|
+
# Generally not useful to be called directly, use {CSDL.target?} instead.
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# CSDL::Processor.new.validate_target!("fake") # => raises UnknownTargetError
|
220
|
+
#
|
221
|
+
# @param target_key [String] The target to validate.
|
222
|
+
#
|
223
|
+
# @return [void]
|
224
|
+
#
|
225
|
+
# @raise [UnknownTargetError] When the terminator value is not a valid operator. See {CSDL.operator?}.
|
226
|
+
#
|
69
227
|
def validate_target!(target_key)
|
70
228
|
unless ::CSDL.target?(target_key)
|
71
|
-
fail ::CSDL::
|
229
|
+
fail ::CSDL::UnknownTargetError, "Target '#{target_key}' is not a known target type."
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
def logically_join_nodes(logical_operator, child_nodes)
|
236
|
+
if child_nodes.size < 2
|
237
|
+
fail ::CSDL::MissingChildNodesError, ":#{logical_operator} nodes must contain at least two child nodes. Expected >= 2, got #{child_nodes.size}"
|
238
|
+
end
|
239
|
+
|
240
|
+
initial = process(child_nodes.first)
|
241
|
+
rest = child_nodes.drop(1)
|
242
|
+
|
243
|
+
rest.reduce(initial) do |csdl, child|
|
244
|
+
csdl += " #{logical_operator.upcase} " + process(child)
|
245
|
+
csdl
|
72
246
|
end
|
73
247
|
end
|
74
248
|
|
@@ -1,6 +1,31 @@
|
|
1
1
|
module CSDL
|
2
|
+
|
3
|
+
# {QueryFilterProcessor} is a class that inherits from {Processor}, providing additional methods
|
4
|
+
# for building CSDL specifically for Query Filters.
|
5
|
+
#
|
6
|
+
# @see Processor
|
7
|
+
# @see Builder
|
8
|
+
# @see http://www.rubydoc.info/gems/ast/AST/Processor AST::Processor
|
9
|
+
# @see http://www.rubydoc.info/gems/ast/AST/Node AST::Node
|
10
|
+
# @see http://dev.datasift.com/docs/csdl DataSift CSDL Language Documentation
|
11
|
+
#
|
2
12
|
class QueryFilterProcessor < ::CSDL::Processor
|
3
13
|
|
14
|
+
# Raises an {InvalidQueryTargetError} if the target isn't a valid CSDL target for query filters. Will
|
15
|
+
# be called from the base class when given a :condition node with a :target node.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# CSDL::QueryFilterProcessorProcessor.new.validate_target!("fake") # => raises InvalidQueryTargetError
|
19
|
+
#
|
20
|
+
# @param target_key [String] The target to validate.
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
#
|
24
|
+
# @raise [InvalidQueryTargetError] When the terminator value is not a valid query filter target. See {CSDL.query_target?}.
|
25
|
+
#
|
26
|
+
# @see Processor#on_condition
|
27
|
+
# @see Processor#on_target
|
28
|
+
#
|
4
29
|
def validate_target!(target_key)
|
5
30
|
unless ::CSDL.query_target?(target_key)
|
6
31
|
fail ::CSDL::InvalidQueryTargetError, "Query filters cannot use target '#{target_key}'"
|
data/lib/csdl/targets.rb
CHANGED
@@ -1,8 +1,21 @@
|
|
1
1
|
module CSDL
|
2
2
|
|
3
|
+
# A CSDL Target definition with indication as to where the target can be used.
|
4
|
+
#
|
5
|
+
# @attr name [String] The name of the target.
|
6
|
+
# @attr interaction? [Boolean] True if the target is availble for use in an Interaction Filter.
|
7
|
+
# @attr analysis? [Boolean] True if the target is availble for use as an Analysis Target.
|
8
|
+
# @attr query? [Boolean] True if the target is availble for use in a Query Filter.
|
9
|
+
#
|
10
|
+
# @see TARGETS
|
11
|
+
#
|
3
12
|
Target = Struct.new(:name, :interaction?, :analysis?, :query?)
|
4
13
|
|
5
|
-
|
14
|
+
# A raw array of targets with their usage flags.
|
15
|
+
#
|
16
|
+
# @return [Array<String, Boolean, Boolean, Boolean>] Array of targets used to produce {TARGETS} hash.
|
17
|
+
#
|
18
|
+
RAW_TARGETS = [
|
6
19
|
|
7
20
|
[ "fb.author.age" , true , true , true ] ,
|
8
21
|
[ "fb.author.country" , true , true , true ] ,
|
@@ -67,7 +80,11 @@ module CSDL
|
|
67
80
|
[ "links.url" , true , true , true ]
|
68
81
|
]
|
69
82
|
|
70
|
-
|
83
|
+
# All possible targets.
|
84
|
+
#
|
85
|
+
# @return [Hash<String, Target>] Hash of {Target} structs, keyed by the string name of the target.
|
86
|
+
#
|
87
|
+
TARGETS = RAW_TARGETS.reduce({}) do |accumulator, (target_name, interaction, analysis, query)|
|
71
88
|
accumulator[target_name] = Target.new(target_name, interaction, analysis, query)
|
72
89
|
accumulator
|
73
90
|
end.freeze
|
@@ -76,18 +93,58 @@ module CSDL
|
|
76
93
|
ANALYSIS_TARGETS = TARGETS.select { |_, target| target.analysis? }
|
77
94
|
QUERY_TARGETS = TARGETS.select { |_, target| target.query? }
|
78
95
|
|
96
|
+
# Check if the given target is a valid target.
|
97
|
+
#
|
98
|
+
# @example
|
99
|
+
# CSDL.target?("fake") # => false
|
100
|
+
# CSDL.target?("fb.content") # => true
|
101
|
+
#
|
102
|
+
# @param target_name [String] The name of the target.
|
103
|
+
#
|
104
|
+
# @return [Boolean] Whether or not the value is a valid CSDL Target.
|
105
|
+
#
|
79
106
|
def self.target?(target_name)
|
80
107
|
TARGETS.key?(target_name)
|
81
108
|
end
|
82
109
|
|
110
|
+
# Check if the given target is a valid interaction target.
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# CSDL.interaction_target?("interaction.tags") # => false
|
114
|
+
# CSDL.interaction_target?("interaction.content") # => true
|
115
|
+
#
|
116
|
+
# @param target_name [String] The name of the target.
|
117
|
+
#
|
118
|
+
# @return [Boolean] Whether or not the value is a valid CSDL Interaction Filter Target.
|
119
|
+
#
|
83
120
|
def self.interaction_target?(target_name)
|
84
121
|
INTERACTION_TARGETS.key?(target_name)
|
85
122
|
end
|
86
123
|
|
124
|
+
# Check if the given target is a valid analysis target.
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# CSDL.analysis_target?("interaction.content") # => false
|
128
|
+
# CSDL.analysis_target?("interaction.tags") # => true
|
129
|
+
#
|
130
|
+
# @param target_name [String] The name of the target.
|
131
|
+
#
|
132
|
+
# @return [Boolean] Whether or not the value is a valid CSDL Analysis Target.
|
133
|
+
#
|
87
134
|
def self.analysis_target?(target_name)
|
88
135
|
ANALYSIS_TARGETS.key?(target_name)
|
89
136
|
end
|
90
137
|
|
138
|
+
# Check if the given target is a valid query target.
|
139
|
+
#
|
140
|
+
# @example
|
141
|
+
# CSDL.query_target?("fb.topics.website") # => false
|
142
|
+
# CSDL.query_target?("fb.topic_ids") # => true
|
143
|
+
#
|
144
|
+
# @param target_name [String] The name of the target.
|
145
|
+
#
|
146
|
+
# @return [Boolean] Whether or not the value is a valid CSDL Query Filter Target.
|
147
|
+
#
|
91
148
|
def self.query_target?(target_name)
|
92
149
|
QUERY_TARGETS.key?(target_name)
|
93
150
|
end
|
data/lib/csdl/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module
|
2
|
-
VERSION = "0.
|
1
|
+
module CSDL
|
2
|
+
VERSION = "0.2.0"
|
3
3
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csdl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- BJ Neilsen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ast
|
@@ -66,7 +66,24 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
-
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: |2
|
84
|
+
|
85
|
+
CSDL is a gem for producing Abstract Syntax Trees for the [DataSift CSDL Filter Language](http://dev.datasift.com/docs/csdl).
|
86
|
+
Working with an AST instead of raw strings provides a simpler way to test and validate any given CSDL filter.
|
70
87
|
email:
|
71
88
|
- bj.neilsen@gmail.com
|
72
89
|
executables: []
|
@@ -89,7 +106,7 @@ files:
|
|
89
106
|
- lib/csdl/query_filter_processor.rb
|
90
107
|
- lib/csdl/targets.rb
|
91
108
|
- lib/csdl/version.rb
|
92
|
-
homepage: https://
|
109
|
+
homepage: https://github.com/localshred/csdl
|
93
110
|
licenses: []
|
94
111
|
metadata: {}
|
95
112
|
post_install_message:
|
@@ -113,3 +130,4 @@ signing_key:
|
|
113
130
|
specification_version: 4
|
114
131
|
summary: AST Processor and Query Builder for DataSift's CSDL language
|
115
132
|
test_files: []
|
133
|
+
has_rdoc:
|