peruse 0.4.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.
@@ -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