peruse 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Peruse
2
+ VERSION = "0.4.0"
3
+ 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
@@ -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