csdl 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/csdl.svg)](http://badge.fury.io/rb/csdl)
|
6
7
|
[![Build Status](https://travis-ci.org/localshred/csdl.svg)](https://travis-ci.org/localshred/csdl)
|
8
|
+
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/csdl)
|
9
|
+
[![Inline docs](http://inch-ci.org/github/localshred/csdl.svg?branch=master)](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:
|