peruse 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.ruby-version +1 -0
- data/AUTHORS +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +20 -0
- data/README.md +118 -0
- data/Rakefile +5 -0
- data/bin/peruse +75 -0
- data/examples/simple.json +25 -0
- data/examples/simple.rb +10 -0
- data/lib/peruse.rb +53 -0
- data/lib/peruse/helper.rb +90 -0
- data/lib/peruse/parser.rb +181 -0
- data/lib/peruse/result_set.rb +16 -0
- data/lib/peruse/transformer.rb +103 -0
- data/lib/peruse/utils.rb +25 -0
- data/lib/peruse/version.rb +3 -0
- data/peruse.gemspec +23 -0
- data/spec/basic_spec.rb +39 -0
- data/spec/binstub_spec.rb +33 -0
- data/spec/boolean_spec.rb +112 -0
- data/spec/chained_search_spec.rb +40 -0
- data/spec/field_value_spec.rb +52 -0
- data/spec/indices_spec.rb +41 -0
- data/spec/last_spec.rb +77 -0
- data/spec/limit_spec.rb +11 -0
- data/spec/nested_search_spec.rb +82 -0
- data/spec/regexp_spec.rb +48 -0
- data/spec/shared/dummy_client.rb +14 -0
- data/spec/shared/peruse_stubs.rb +5 -0
- data/spec/shared/time_stubs.rb +12 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/window_spec.rb +54 -0
- metadata +221 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Peruse
|
4
|
+
class Parser < Parslet::Parser
|
5
|
+
|
6
|
+
# BUILDING BLOCKS
|
7
|
+
|
8
|
+
# Single character rules
|
9
|
+
rule(:lparen) { str('(') >> space? }
|
10
|
+
rule(:rparen) { str(')') >> space? }
|
11
|
+
rule(:digit) { match('[0-9]') }
|
12
|
+
rule(:space) { match('\s').repeat(1) }
|
13
|
+
rule(:space?) { space.maybe }
|
14
|
+
|
15
|
+
# Numbers
|
16
|
+
rule(:positive_integer) { digit.repeat(1) >> space? }
|
17
|
+
rule(:negative_integer) { str('-') >> positive_integer }
|
18
|
+
rule(:integer) { negative_integer | positive_integer }
|
19
|
+
rule(:float) {
|
20
|
+
str('-').maybe >> digit.repeat(1) >> str('.') >> digit.repeat(1) >> space?
|
21
|
+
}
|
22
|
+
rule(:number) { integer | float }
|
23
|
+
|
24
|
+
# Dates
|
25
|
+
rule(:datetime) {
|
26
|
+
# 1979-05-27T07:32:00Z
|
27
|
+
digit.repeat(4) >> str("-") >>
|
28
|
+
digit.repeat(2) >> str("-") >>
|
29
|
+
digit.repeat(2) >> str("T") >>
|
30
|
+
digit.repeat(2) >> str(":") >>
|
31
|
+
digit.repeat(2) >> str(":") >>
|
32
|
+
digit.repeat(2) >> str("Z")
|
33
|
+
}
|
34
|
+
|
35
|
+
# Strings
|
36
|
+
rule(:escaped_special) {
|
37
|
+
str("\\") >> match['0tnr"\\\\']
|
38
|
+
}
|
39
|
+
rule(:string_special) {
|
40
|
+
match['\0\t\n\r"\\\\']
|
41
|
+
}
|
42
|
+
rule(:string) {
|
43
|
+
str('"') >>
|
44
|
+
(escaped_special | string_special.absent? >> any).repeat >>
|
45
|
+
str('"')
|
46
|
+
}
|
47
|
+
|
48
|
+
# Booleans
|
49
|
+
rule(:and_operator) { (str('and') | str('AND') | str('&')) >> space? }
|
50
|
+
rule(:or_operator) { (str('or') | str('OR') | str('|')) >> space? }
|
51
|
+
rule(:not_operator) { (str('not') | str('NOT') | str('~')) >> space? }
|
52
|
+
|
53
|
+
|
54
|
+
# COMMANDS
|
55
|
+
|
56
|
+
# Command parts
|
57
|
+
rule(:enclosed_string) {
|
58
|
+
string | match('[^\s]').repeat
|
59
|
+
}
|
60
|
+
rule(:identifier) { match('[^=\s)(|]').repeat(1) >> match('[^=\s]').repeat }
|
61
|
+
rule(:wildcard) {
|
62
|
+
(lparen >> wildcard >> rparen) |
|
63
|
+
match('[^=\s|)(]').repeat(1)
|
64
|
+
}
|
65
|
+
rule(:query_value) { string | wildcard | datetime | number }
|
66
|
+
rule(:searchop) { match['='] }
|
67
|
+
rule(:rhs) {
|
68
|
+
regexp | query_value
|
69
|
+
}
|
70
|
+
rule(:relative_time) {
|
71
|
+
integer.as(:quantity) >>
|
72
|
+
match('s|m|h|d|w').as(:quantifier)
|
73
|
+
}
|
74
|
+
rule(:absolute_time) {
|
75
|
+
datetime.as(:datetime) | enclosed_string.as(:chronic_time)
|
76
|
+
}
|
77
|
+
|
78
|
+
# Field = Value
|
79
|
+
rule(:field_value) {
|
80
|
+
identifier.as(:field) >> space? >>
|
81
|
+
searchop >> space? >>
|
82
|
+
(rhs.as(:value) | subsearch.as(:subsearch))
|
83
|
+
}
|
84
|
+
|
85
|
+
# Value-only
|
86
|
+
rule(:value_only) {
|
87
|
+
query_value.as(:value)
|
88
|
+
}
|
89
|
+
|
90
|
+
# Window
|
91
|
+
rule(:window) {
|
92
|
+
str('window') >>
|
93
|
+
space >>
|
94
|
+
(relative_time | absolute_time).as(:window_start) >>
|
95
|
+
space >> str('to') >> space >>
|
96
|
+
(relative_time | absolute_time).as(:window_end)
|
97
|
+
}
|
98
|
+
|
99
|
+
# Indices
|
100
|
+
rule(:indices) {
|
101
|
+
str('indices') >>
|
102
|
+
space >>
|
103
|
+
(enclosed_string >>
|
104
|
+
(space? >>
|
105
|
+
str(',') >>
|
106
|
+
space? >>
|
107
|
+
enclosed_string).repeat).as(:indices)
|
108
|
+
}
|
109
|
+
|
110
|
+
# Limit
|
111
|
+
rule(:limit) {
|
112
|
+
str('limit') >> space >> positive_integer.as(:limit)
|
113
|
+
}
|
114
|
+
|
115
|
+
# Regexp
|
116
|
+
rule(:regexp) {
|
117
|
+
str('/') >> (str('\/') | match('[^/]')).repeat.as(:regexp) >> str('/')
|
118
|
+
}
|
119
|
+
|
120
|
+
# Last
|
121
|
+
rule(:last) {
|
122
|
+
(str('last') >> space >> relative_time).as(:last)
|
123
|
+
}
|
124
|
+
|
125
|
+
# Subsearch
|
126
|
+
rule(:subsearch) {
|
127
|
+
str('`') >> space? >> nested_search >> str('`')
|
128
|
+
}
|
129
|
+
rule(:nested_search) {
|
130
|
+
peruse_query.as(:initial_query) >> space? >> str('|') >> space? >>
|
131
|
+
match('[^`]').repeat.as(:extractors)
|
132
|
+
}
|
133
|
+
|
134
|
+
# Reference your custom commands here to make them eligible for parsing
|
135
|
+
# NOTE: order matters!
|
136
|
+
rule(:command) {
|
137
|
+
(
|
138
|
+
field_value |
|
139
|
+
window |
|
140
|
+
last |
|
141
|
+
limit |
|
142
|
+
indices |
|
143
|
+
value_only
|
144
|
+
).as(:command) >> space?
|
145
|
+
}
|
146
|
+
|
147
|
+
|
148
|
+
# QUERY JOINING
|
149
|
+
|
150
|
+
rule(:negated_command) {
|
151
|
+
(not_operator >> command.as(:not)) |
|
152
|
+
command
|
153
|
+
}
|
154
|
+
rule(:primary) { lparen >> or_operation >> rparen | negated_command }
|
155
|
+
|
156
|
+
# borrowed from Parslet's boolean algebra example
|
157
|
+
rule(:negated_and) {
|
158
|
+
(not_operator >> and_operation.as(:not)) |
|
159
|
+
and_operation
|
160
|
+
}
|
161
|
+
rule(:and_operation) {
|
162
|
+
(primary.as(:left) >> and_operator >>
|
163
|
+
negated_and.as(:right)).as(:and) |
|
164
|
+
primary }
|
165
|
+
|
166
|
+
rule(:negated_or) {
|
167
|
+
(not_operator >> or_operation.as(:not)) |
|
168
|
+
or_operation
|
169
|
+
}
|
170
|
+
rule(:or_operation) {
|
171
|
+
(and_operation.as(:left) >> or_operator >>
|
172
|
+
negated_or.as(:right)).as(:or) |
|
173
|
+
and_operation }
|
174
|
+
|
175
|
+
rule(:peruse_query) {
|
176
|
+
space? >> or_operation >> space?
|
177
|
+
}
|
178
|
+
|
179
|
+
root(:peruse_query)
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Peruse
|
2
|
+
class ResultSet
|
3
|
+
attr_accessor :query, :query_string
|
4
|
+
|
5
|
+
def initialize(filter)
|
6
|
+
@query = { query: { filtered: { filter: filter }}}
|
7
|
+
end
|
8
|
+
|
9
|
+
def eval
|
10
|
+
Peruse.elasticsearch_client.search(
|
11
|
+
body: @query,
|
12
|
+
size: Peruse.max_number_of_hits || 10
|
13
|
+
) if @query
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
require 'chronic'
|
3
|
+
|
4
|
+
module Peruse
|
5
|
+
class Transformer < Parslet::Transform
|
6
|
+
# Base
|
7
|
+
rule(command: subtree(:command)) do
|
8
|
+
command
|
9
|
+
end
|
10
|
+
|
11
|
+
# Field = Value
|
12
|
+
rule(
|
13
|
+
field: simple(:field),
|
14
|
+
value: simple(:value)
|
15
|
+
) do
|
16
|
+
Helper.query_builder(
|
17
|
+
String(field) + ":" + String(value)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Limit
|
22
|
+
rule(limit: simple(:limit)) do
|
23
|
+
Helper.limit_builder(Integer(limit))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Regexp
|
27
|
+
rule(
|
28
|
+
field: simple(:field),
|
29
|
+
value: {
|
30
|
+
regexp: simple(:regexp)
|
31
|
+
}
|
32
|
+
) do
|
33
|
+
Helper.regexp_builder(String(field), String(regexp))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Value-only
|
37
|
+
rule(value: simple(:value)) do
|
38
|
+
Helper.query_builder(String(value))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Indices
|
42
|
+
rule(indices: simple(:indices)) do
|
43
|
+
list = String(indices).split(',').collect { |s| s.strip }
|
44
|
+
Helper.indices_builder(list)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Last
|
48
|
+
rule(last: subtree(:last)) do
|
49
|
+
start_time = last
|
50
|
+
end_time = Helper.timestamp_format(Time.now)
|
51
|
+
|
52
|
+
Helper.range_builder(start_time, end_time)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Window
|
56
|
+
rule(
|
57
|
+
window_start: subtree(:window_start),
|
58
|
+
window_end: subtree(:window_end)
|
59
|
+
) do
|
60
|
+
Helper.range_builder(window_start, window_end)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Time parts
|
64
|
+
rule(datetime: simple(:datetime)) do
|
65
|
+
String(datetime)
|
66
|
+
end
|
67
|
+
rule(
|
68
|
+
quantity: simple(:quantity),
|
69
|
+
quantifier: simple(:quantifier)
|
70
|
+
) do
|
71
|
+
timestamp = Helper.time_query_to_timestamp(
|
72
|
+
Integer(quantity).abs, # last -1h same as last 1h
|
73
|
+
String(quantifier)
|
74
|
+
)
|
75
|
+
|
76
|
+
Helper.timestamp_format timestamp
|
77
|
+
end
|
78
|
+
rule(chronic_time: simple(:chronic_time)) do
|
79
|
+
Helper.timestamp_format Chronic.parse(chronic_time)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Negate
|
83
|
+
rule(negate: subtree(:not)) do
|
84
|
+
{ not: negate }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Command joining
|
88
|
+
rule(:or => {
|
89
|
+
left: subtree(:left),
|
90
|
+
right: subtree(:right)
|
91
|
+
}) do
|
92
|
+
Helper.combine_subtrees(left, right, :or)
|
93
|
+
end
|
94
|
+
|
95
|
+
rule(:and => {
|
96
|
+
left: subtree(:left),
|
97
|
+
right: subtree(:right)
|
98
|
+
}) do
|
99
|
+
Helper.combine_subtrees(left, right, :and)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
data/lib/peruse/utils.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Peruse
|
2
|
+
class Utils
|
3
|
+
# nested field matcher
|
4
|
+
def self.extract_values(hash, keys)
|
5
|
+
@vals ||= []
|
6
|
+
|
7
|
+
hash.each_pair do |k, v|
|
8
|
+
if v.is_a? Hash
|
9
|
+
extract_values(v, keys)
|
10
|
+
elsif v.is_a? Array
|
11
|
+
v.flatten!
|
12
|
+
if v.first.is_a? Hash
|
13
|
+
v.each { |el| extract_values(el, keys) }
|
14
|
+
elsif keys.include? k
|
15
|
+
@vals += v
|
16
|
+
end
|
17
|
+
elsif keys.include? k
|
18
|
+
@vals << v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
return @vals
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/peruse.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/peruse/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "peruse"
|
6
|
+
s.version = Peruse::VERSION
|
7
|
+
s.required_ruby_version = ">= 1.9.3"
|
8
|
+
s.add_runtime_dependency "json", "~> 1.8", ">= 1.8.0"
|
9
|
+
s.add_runtime_dependency "parslet", "~> 1.5", ">= 1.5.0"
|
10
|
+
s.add_runtime_dependency "elasticsearch", "~> 1.0", ">= 1.0.0"
|
11
|
+
s.add_runtime_dependency "activesupport", "~> 4.0", ">= 4.0.0"
|
12
|
+
s.add_runtime_dependency "chronic", "~> 0.10", ">= 0.10.0"
|
13
|
+
s.add_development_dependency "rspec", "~> 3.1", ">= 3.1.0"
|
14
|
+
s.add_development_dependency "timecop", "~> 0.7", ">= 0.7.1"
|
15
|
+
s.executables << "peruse"
|
16
|
+
s.summary = "Elasticsearch query language"
|
17
|
+
s.description = "Human-friendly query language for Elasticsearch. Formerly known as plunk."
|
18
|
+
s.authors = ["Ram Mehta", "Jamil Bou Kheir"]
|
19
|
+
s.email = ["ram.mehta@gmail.com", "jamil@elbii.com"]
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.homepage = "https://github.com/elbii/peruse"
|
22
|
+
s.license = "MIT"
|
23
|
+
end
|
data/spec/basic_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'basic searches' do
|
4
|
+
def basic_builder(expected)
|
5
|
+
{
|
6
|
+
query: {
|
7
|
+
filtered: {
|
8
|
+
filter: {
|
9
|
+
query: {
|
10
|
+
query_string: {
|
11
|
+
query: expected
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should parse bar' do
|
21
|
+
result = Peruse.search 'bar'
|
22
|
+
expect(result).to eq(basic_builder('bar'))
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should parse bar ' do
|
26
|
+
result = Peruse.search 'bar '
|
27
|
+
expect(result).to eq(basic_builder('bar'))
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should parse (bar) ' do
|
31
|
+
result = Peruse.search '(bar) '
|
32
|
+
expect(result).to eq(basic_builder('bar'))
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should parse bar ' do
|
36
|
+
result = Peruse.search ' bar '
|
37
|
+
expect(result).to eq(basic_builder('bar'))
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
describe 'binstub' do
|
4
|
+
it 'should execute a valid query from stdin' do
|
5
|
+
result = ""
|
6
|
+
|
7
|
+
Open3.popen3(
|
8
|
+
"bin/peruse",
|
9
|
+
"-h localhost,127.0.0.1",
|
10
|
+
"-s 1",
|
11
|
+
"-r",
|
12
|
+
"-d",
|
13
|
+
"-t timestamp"
|
14
|
+
) do |stdin, stdout, stderr|
|
15
|
+
stdin.puts "last 1w"
|
16
|
+
stdin.close
|
17
|
+
result = stdout.read.chomp
|
18
|
+
end
|
19
|
+
|
20
|
+
expect(result).to be_present
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should not allow invalid options' do
|
24
|
+
exit_status = -1
|
25
|
+
|
26
|
+
Open3.popen3("bin/peruse", "-z") do |stdin, stdout, stderr, thread|
|
27
|
+
exit_status = thread.value.exitstatus
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
expect(exit_status).to eq 1
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'boolean searches' do
|
4
|
+
|
5
|
+
it 'should parse (foo OR bar)' do
|
6
|
+
result = Peruse.search '(foo OR bar)'
|
7
|
+
expected = Peruse::Helper.filter_builder({
|
8
|
+
or: [
|
9
|
+
Peruse::Helper.query_builder('foo'),
|
10
|
+
Peruse::Helper.query_builder('bar')
|
11
|
+
]
|
12
|
+
})
|
13
|
+
expect(result).to eq(expected)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should parse (foo | bar)' do
|
17
|
+
result = Peruse.search '(foo | bar)'
|
18
|
+
expected = Peruse::Helper.filter_builder({
|
19
|
+
or: [
|
20
|
+
Peruse::Helper.query_builder('foo'),
|
21
|
+
Peruse::Helper.query_builder('bar')
|
22
|
+
]
|
23
|
+
})
|
24
|
+
expect(result).to eq(expected)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should parse (foo OR (bar AND baz))' do
|
28
|
+
result = Peruse.search '(foo OR (bar AND baz))'
|
29
|
+
expected = Peruse::Helper.filter_builder({
|
30
|
+
or: [
|
31
|
+
Peruse::Helper.query_builder('foo'),
|
32
|
+
{
|
33
|
+
and: [
|
34
|
+
Peruse::Helper.query_builder('bar'),
|
35
|
+
Peruse::Helper.query_builder('baz')
|
36
|
+
]
|
37
|
+
}
|
38
|
+
]
|
39
|
+
})
|
40
|
+
expect(result).to eq(expected)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should parse foo=bar & baz=fez & fad=bad' do
|
44
|
+
result = Peruse.search 'foo=bar & baz=fez & fad=bad'
|
45
|
+
expected = Peruse::Helper.filter_builder({
|
46
|
+
and: [
|
47
|
+
Peruse::Helper.query_builder('foo:bar'),
|
48
|
+
Peruse::Helper.query_builder('baz:fez'),
|
49
|
+
Peruse::Helper.query_builder('fad:bad')
|
50
|
+
]
|
51
|
+
})
|
52
|
+
expect(result).to eq(expected)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should parse foo=bar | foo=baz | fez=baz' do
|
56
|
+
result = Peruse.search 'foo=bar | foo=baz | fez=baz'
|
57
|
+
expected = Peruse::Helper.filter_builder({
|
58
|
+
or: [
|
59
|
+
Peruse::Helper.query_builder('foo:bar'),
|
60
|
+
Peruse::Helper.query_builder('foo:baz'),
|
61
|
+
Peruse::Helper.query_builder('fez:baz')
|
62
|
+
]
|
63
|
+
})
|
64
|
+
expect(result).to eq(expected)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should parse (foo=bar OR foo=bar)' do
|
68
|
+
result = Peruse.search '(foo=bar OR foo=bar)'
|
69
|
+
expected = Peruse::Helper.filter_builder({
|
70
|
+
or: [
|
71
|
+
Peruse::Helper.query_builder('foo:bar'),
|
72
|
+
Peruse::Helper.query_builder('foo:bar')
|
73
|
+
]
|
74
|
+
})
|
75
|
+
expect(result).to eq(expected)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should parse foo=bar OR baz=fez' do
|
79
|
+
result = Peruse.search 'foo=bar OR baz=fez'
|
80
|
+
expected = Peruse::Helper.filter_builder({
|
81
|
+
or: [
|
82
|
+
Peruse::Helper.query_builder('foo:bar'),
|
83
|
+
Peruse::Helper.query_builder('baz:fez')
|
84
|
+
]
|
85
|
+
})
|
86
|
+
expect(result).to eq(expected)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should parse (foo=bar AND baz=fez) OR ham=cheese' do
|
90
|
+
result = Peruse.search '(foo=bar AND baz=fez) OR ham=cheese'
|
91
|
+
expected = Peruse::Helper.filter_builder({
|
92
|
+
or: [
|
93
|
+
{
|
94
|
+
and: [
|
95
|
+
Peruse::Helper.query_builder('foo:bar'),
|
96
|
+
Peruse::Helper.query_builder('baz:fez')
|
97
|
+
]
|
98
|
+
},
|
99
|
+
Peruse::Helper.query_builder('ham:cheese')
|
100
|
+
]
|
101
|
+
})
|
102
|
+
expect(result).to eq(expected)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should parse NOT foo=bar' do
|
106
|
+
result = Peruse.search 'NOT foo=bar'
|
107
|
+
expected = Peruse::Helper.filter_builder({
|
108
|
+
not: Peruse::Helper.query_builder('foo:bar')
|
109
|
+
})
|
110
|
+
expect(result).to eq(expected)
|
111
|
+
end
|
112
|
+
end
|