nql 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
1
+ module NQL
2
+ grammar Syntax
3
+
4
+ rule expression
5
+ boolean / primary
6
+ end
7
+
8
+ rule boolean
9
+ left:primary space coordinator:coordinator space right:expression {
10
+ def to_ransack
11
+ group = {'g' => [{'m' => coordinator.to_ransack}]}
12
+
13
+ [left, right].each do |side|
14
+ if side.is_node?(:boolean)
15
+ group['g'][0].merge! side.to_ransack
16
+ else
17
+ group['g'][0]['c'] ||= []
18
+ group['g'][0]['c'] << side.to_ransack
19
+ end
20
+ end
21
+
22
+ group
23
+ end
24
+
25
+ def is_node?(node_type)
26
+ node_type.to_sym == :boolean
27
+ end
28
+ }
29
+ end
30
+
31
+ rule primary
32
+ (space comparison space / '(' space expression space ')') {
33
+ def to_ransack
34
+ detect_node.to_ransack
35
+ end
36
+
37
+ def detect_node
38
+ self.send %w(comparison expression).detect { |m| self.respond_to? m }
39
+ end
40
+
41
+ def is_node?(node_type)
42
+ detect_node.is_node?(node_type)
43
+ end
44
+ }
45
+ end
46
+
47
+ rule coordinator
48
+ ('|' / '&') {
49
+ def to_ransack
50
+ coordinators = {'|' => 'or', '&' => 'and'}
51
+ coordinators[text_value]
52
+ end
53
+ }
54
+ end
55
+
56
+ rule comparison
57
+ variable:alphanumeric space comparator:comparator space value:text {
58
+ def to_ransack
59
+ hash = {'a' => {'0' => {'name' => self.variable.text_value.gsub('.', '_')}}, 'p' => self.comparator.to_ransack, 'v' => {'0' => {'value' => self.value.text_value}}}
60
+ hash = {'c' => [hash]} if !parent || !parent.parent || text_value == parent.parent.text_value
61
+ hash
62
+ end
63
+
64
+ def is_node?(node_type)
65
+ node_type.to_sym == :comparison
66
+ end
67
+ }
68
+ end
69
+
70
+ rule comparator
71
+ ('=' / '!=' / '>' / '>=' / '<' / '<=' / '%')+ {
72
+ def to_ransack
73
+ comparators = {
74
+ '=' => 'eq',
75
+ '!=' => 'not_eq',
76
+ '>' => 'gt',
77
+ '>=' => 'gteq',
78
+ '<' => 'lt',
79
+ '<=' => 'lteq',
80
+ '%' => 'cont'
81
+ }
82
+ comparators[text_value]
83
+ end
84
+ }
85
+ end
86
+
87
+ rule text
88
+ (alphanumeric / utf8 / symbol)+
89
+ (space (alphanumeric / utf8 / symbol)+)*
90
+ end
91
+
92
+ rule alphanumeric
93
+ [a-zA-Z0-9_.]+
94
+ end
95
+
96
+ rule space
97
+ ' '*
98
+ end
99
+
100
+ rule symbol
101
+ [><=+-\/\\@#$%!?:]
102
+ end
103
+
104
+ rule utf8
105
+ [\u00c1\u00c0\u00c9\u00c8\u00cd\u00cc\u00d3\u00d2\u00da\u00d9\u00dc\u00d1\u00c7\u00e1\u00e0\u00e9\u00e8\u00ed\u00ec\u00f3\u00f2\u00fa\u00f9\u00fc\u00f1\u00e7]
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,3 @@
1
+ module NQL
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/nql/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'nql'
6
+ s.version = NQL::VERSION
7
+ s.authors = ['Gabriel Naiman']
8
+ s.email = ['gabynaiman@gmail.com']
9
+ s.description = 'Natural Query Language built on top of ActiveRecord and Ransack'
10
+ s.summary = 'Natural Query Language built on top of ActiveRecord and Ransack'
11
+ s.homepage = 'https://github.com/gabynaiman/nql'
12
+
13
+ s.files = `git ls-files`.split($\)
14
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency 'treetop'
19
+ s.add_dependency 'activerecord', '>= 3.2.0'
20
+ s.add_dependency 'activesupport', '>= 3.2.0'
21
+ s.add_dependency 'ransack'
22
+
23
+ s.add_development_dependency 'sqlite3'
24
+ s.add_development_dependency 'rspec'
25
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+
3
+ describe NQL::SyntaxParser, '-> Comparison' do
4
+
5
+ let(:parser) { NQL::SyntaxParser.new }
6
+
7
+ context 'Structure and comparators' do
8
+
9
+ it 'Equals' do
10
+ tree = parser.parse('var = value')
11
+
12
+ tree.comparison.variable.text_value.should eq 'var'
13
+ tree.comparison.comparator.text_value.should eq '='
14
+ tree.comparison.value.text_value.should eq 'value'
15
+ end
16
+
17
+ it 'Not equals' do
18
+ tree = parser.parse('var != value')
19
+
20
+ tree.comparison.variable.text_value.should eq 'var'
21
+ tree.comparison.comparator.text_value.should eq '!='
22
+ tree.comparison.value.text_value.should eq 'value'
23
+ end
24
+
25
+ it 'Greater than' do
26
+ tree = parser.parse('var > value')
27
+
28
+ tree.comparison.variable.text_value.should eq 'var'
29
+ tree.comparison.comparator.text_value.should eq '>'
30
+ tree.comparison.value.text_value.should eq 'value'
31
+ end
32
+
33
+ it 'Greater or equals than' do
34
+ tree = parser.parse('var >= value')
35
+
36
+ tree.comparison.variable.text_value.should eq 'var'
37
+ tree.comparison.comparator.text_value.should eq '>='
38
+ tree.comparison.value.text_value.should eq 'value'
39
+ end
40
+
41
+ it 'Less than' do
42
+ tree = parser.parse('var < value')
43
+
44
+ tree.comparison.variable.text_value.should eq 'var'
45
+ tree.comparison.comparator.text_value.should eq '<'
46
+ tree.comparison.value.text_value.should eq 'value'
47
+ end
48
+
49
+ it 'Less or equals than' do
50
+ tree = parser.parse('var <= value')
51
+
52
+ tree.comparison.variable.text_value.should eq 'var'
53
+ tree.comparison.comparator.text_value.should eq '<='
54
+ tree.comparison.value.text_value.should eq 'value'
55
+ end
56
+
57
+ it 'Contains' do
58
+ tree = parser.parse('var % value')
59
+
60
+ tree.comparison.variable.text_value.should eq 'var'
61
+ tree.comparison.comparator.text_value.should eq '%'
62
+ tree.comparison.value.text_value.should eq 'value'
63
+ end
64
+
65
+ end
66
+
67
+ context 'Space separators' do
68
+
69
+ it 'Without spaces' do
70
+ tree = parser.parse('var=value')
71
+
72
+ tree.comparison.variable.text_value.should eq 'var'
73
+ tree.comparison.comparator.text_value.should eq '='
74
+ tree.comparison.value.text_value.should eq 'value'
75
+ end
76
+
77
+ it 'With many spaces' do
78
+ tree = parser.parse('var = value')
79
+
80
+ tree.comparison.variable.text_value.should eq 'var'
81
+ tree.comparison.comparator.text_value.should eq '='
82
+ tree.comparison.value.text_value.should eq 'value'
83
+ end
84
+
85
+ end
86
+
87
+ context 'Variable names' do
88
+
89
+ it 'With numbers' do
90
+ tree = parser.parse('var1 = value')
91
+ tree.comparison.variable.text_value.should eq 'var1'
92
+ end
93
+
94
+ it 'With uppercase' do
95
+ tree = parser.parse('varName = value')
96
+ tree.comparison.variable.text_value.should eq 'varName'
97
+ end
98
+
99
+ it 'With underscore' do
100
+ tree = parser.parse('var_name = value')
101
+ tree.comparison.variable.text_value.should eq 'var_name'
102
+ end
103
+
104
+ it 'With dot' do
105
+ tree = parser.parse('var.name = value')
106
+ tree.comparison.variable.text_value.should eq 'var.name'
107
+ end
108
+
109
+ end
110
+
111
+ context 'Values' do
112
+
113
+ it 'With numbers' do
114
+ tree = parser.parse('var = value1')
115
+ tree.comparison.value.text_value.should eq 'value1'
116
+ end
117
+
118
+ it 'With uppercase' do
119
+ tree = parser.parse('var = valueDummy')
120
+ tree.comparison.value.text_value.should eq 'valueDummy'
121
+ end
122
+
123
+ it 'With dot' do
124
+ tree = parser.parse('var = value.dummy')
125
+ tree.comparison.value.text_value.should eq 'value.dummy'
126
+ end
127
+
128
+
129
+ it 'With utf8 chars and symbols' do
130
+ utf8_symbols = "\u00c1\u00c0\u00c9\u00c8\u00cd\u00cc\u00d3\u00d2\u00da\u00d9\u00dc\u00d1\u00c7\u00e1\u00e0\u00e9\u00e8\u00ed\u00ec\u00f3\u00f2\u00fa\u00f9\u00fc\u00f1\u00e7"
131
+ tree = parser.parse("var = .#+-#{utf8_symbols}")
132
+ tree.comparison.value.text_value.should eq ".#+-#{utf8_symbols}"
133
+ end
134
+
135
+ it 'With spaces' do
136
+ tree = parser.parse('var = value 123')
137
+ tree.comparison.value.text_value.should eq 'value 123'
138
+ end
139
+
140
+ it 'With comparators, symbols and spaces' do
141
+ tree = parser.parse('var = value1 > value2 ! value3')
142
+ tree.comparison.value.text_value.should eq 'value1 > value2 ! value3'
143
+ end
144
+
145
+ end
146
+
147
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe NQL::SyntaxParser, '-> Coordination' do
4
+
5
+ let(:parser) { NQL::SyntaxParser.new }
6
+
7
+ it 'And' do
8
+ tree = parser.parse('var1 = value1 & var2 = value2')
9
+
10
+ tree.left.text_value.strip.should eq 'var1 = value1'
11
+ tree.coordinator.text_value.should eq '&'
12
+ tree.right.text_value.strip.should eq 'var2 = value2'
13
+ end
14
+
15
+ it 'Or' do
16
+ tree = parser.parse('var1 = value1 | var2 = value2')
17
+
18
+ tree.left.text_value.strip.should eq 'var1 = value1'
19
+ tree.coordinator.text_value.should eq '|'
20
+ tree.right.text_value.strip.should eq 'var2 = value2'
21
+ end
22
+
23
+ it 'And then Or' do
24
+ tree = parser.parse('var1 = value1 & var2 = value2 | var3 = value3')
25
+
26
+ tree.left.text_value.strip.should eq 'var1 = value1'
27
+ tree.coordinator.text_value.should eq '&'
28
+ tree.right.left.text_value.strip.should eq 'var2 = value2'
29
+ tree.right.coordinator.text_value.strip.should eq '|'
30
+ tree.right.right.text_value.strip.should eq 'var3 = value3'
31
+ end
32
+
33
+ it 'With parentheses' do
34
+ tree = parser.parse('(var1 = value1 & var2 = value2) | var3 = value3')
35
+
36
+ tree.left.expression.left.text_value.strip.should eq 'var1 = value1'
37
+ tree.left.expression.coordinator.text_value.should eq '&'
38
+ tree.left.expression.right.text_value.strip.should eq 'var2 = value2'
39
+ tree.coordinator.text_value.strip.should eq '|'
40
+ tree.right.text_value.strip.should eq 'var3 = value3'
41
+ end
42
+
43
+ end
@@ -0,0 +1,9 @@
1
+ class CreateCountries < ActiveRecord::Migration
2
+ def change
3
+ create_table :countries do |t|
4
+ t.string :name, null: false
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class CreateCities < ActiveRecord::Migration
2
+ def change
3
+ create_table :cities do |t|
4
+ t.string :name, null: false
5
+ t.references :country
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ class City < ActiveRecord::Base
2
+ belongs_to :country
3
+ end
@@ -0,0 +1,3 @@
1
+ class Country < ActiveRecord::Base
2
+ has_many :cities, dependent: :destroy
3
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Ransack Query' do
4
+
5
+ let(:parser) { NQL::SyntaxParser.new }
6
+
7
+ context 'Single comparisons' do
8
+
9
+ it 'Equals' do
10
+ q = parser.parse('id = 1234').to_ransack
11
+
12
+ q['c'][0].should have_attribute 'id'
13
+ q['c'][0].should have_predicate 'eq'
14
+ q['c'][0].should have_value '1234'
15
+ end
16
+
17
+ it 'Not equals' do
18
+ q = parser.parse('id != 1234').to_ransack
19
+
20
+ q['c'][0].should have_attribute 'id'
21
+ q['c'][0].should have_predicate 'not_eq'
22
+ q['c'][0].should have_value '1234'
23
+ end
24
+
25
+ it 'Greater than' do
26
+ q = parser.parse('id > 1234').to_ransack
27
+
28
+ q['c'][0].should have_attribute 'id'
29
+ q['c'][0].should have_predicate 'gt'
30
+ q['c'][0].should have_value '1234'
31
+ end
32
+
33
+ it 'Greater or equals than' do
34
+ q = parser.parse('id >= 1234').to_ransack
35
+
36
+ q['c'][0].should have_attribute 'id'
37
+ q['c'][0].should have_predicate 'gteq'
38
+ q['c'][0].should have_value '1234'
39
+ end
40
+
41
+ it 'Less than' do
42
+ q = parser.parse('id < 1234').to_ransack
43
+
44
+ q['c'][0].should have_attribute 'id'
45
+ q['c'][0].should have_predicate 'lt'
46
+ q['c'][0].should have_value '1234'
47
+ end
48
+
49
+ it 'Less or equals than' do
50
+ q = parser.parse('id <= 1234').to_ransack
51
+
52
+ q['c'][0].should have_attribute 'id'
53
+ q['c'][0].should have_predicate 'lteq'
54
+ q['c'][0].should have_value '1234'
55
+ end
56
+
57
+ it 'Contains' do
58
+ q = parser.parse('id % 1234').to_ransack
59
+
60
+ q['c'][0].should have_attribute 'id'
61
+ q['c'][0].should have_predicate 'cont'
62
+ q['c'][0].should have_value '1234'
63
+ end
64
+
65
+ it 'Model references' do
66
+ q = parser.parse('models.id = 1234').to_ransack
67
+
68
+ q['c'][0].should have_attribute 'models_id'
69
+ q['c'][0].should have_predicate 'eq'
70
+ q['c'][0].should have_value '1234'
71
+ end
72
+
73
+ end
74
+
75
+ context 'Coordinated comparisons' do
76
+
77
+ it 'And' do
78
+ q = parser.parse('id > 1234 & name = abcd').to_ransack
79
+
80
+ q['g'][0]['m'].should eq 'and'
81
+ q['g'][0]['c'][0].should have_attribute 'id'
82
+ q['g'][0]['c'][0].should have_predicate 'gt'
83
+ q['g'][0]['c'][0].should have_value '1234'
84
+ q['g'][0]['c'][1].should have_attribute 'name'
85
+ q['g'][0]['c'][1].should have_predicate 'eq'
86
+ q['g'][0]['c'][1].should have_value 'abcd'
87
+ end
88
+
89
+ it 'Or' do
90
+ q = parser.parse('id < 1234 | name % abcd').to_ransack
91
+
92
+ q['g'][0]['m'].should eq 'or'
93
+ q['g'][0]['c'][0].should have_attribute 'id'
94
+ q['g'][0]['c'][0].should have_predicate 'lt'
95
+ q['g'][0]['c'][0].should have_value '1234'
96
+ q['g'][0]['c'][1].should have_attribute 'name'
97
+ q['g'][0]['c'][1].should have_predicate 'cont'
98
+ q['g'][0]['c'][1].should have_value 'abcd'
99
+ end
100
+
101
+ it 'And then Or' do
102
+ q = parser.parse('id > 1234 & name = abcd | name % efgh').to_ransack
103
+
104
+ q['g'][0]['m'].should eq 'and'
105
+ q['g'][0]['c'][0].should have_attribute 'id'
106
+ q['g'][0]['c'][0].should have_predicate 'gt'
107
+ q['g'][0]['c'][0].should have_value '1234'
108
+ q['g'][0]['g'][0]['m'].should eq 'or'
109
+ q['g'][0]['g'][0]['c'][0].should have_attribute 'name'
110
+ q['g'][0]['g'][0]['c'][0].should have_predicate 'eq'
111
+ q['g'][0]['g'][0]['c'][0].should have_value 'abcd'
112
+ q['g'][0]['g'][0]['c'][1].should have_attribute 'name'
113
+ q['g'][0]['g'][0]['c'][1].should have_predicate 'cont'
114
+ q['g'][0]['g'][0]['c'][1].should have_value 'efgh'
115
+ end
116
+
117
+ it 'With parentheses' do
118
+ q = parser.parse('(id > 1234 & name = abcd) | name % efgh').to_ransack
119
+
120
+ q['g'][0]['g'][0]['m'].should eq 'and'
121
+ q['g'][0]['g'][0]['c'][0].should have_attribute 'id'
122
+ q['g'][0]['g'][0]['c'][0].should have_predicate 'gt'
123
+ q['g'][0]['g'][0]['c'][0].should have_value '1234'
124
+ q['g'][0]['g'][0]['c'][1].should have_attribute 'name'
125
+ q['g'][0]['g'][0]['c'][1].should have_predicate 'eq'
126
+ q['g'][0]['g'][0]['c'][1].should have_value 'abcd'
127
+ q['g'][0]['m'].should eq 'or'
128
+ q['g'][0]['c'][0].should have_attribute 'name'
129
+ q['g'][0]['c'][0].should have_predicate 'cont'
130
+ q['g'][0]['c'][0].should have_value 'efgh'
131
+ end
132
+
133
+ end
134
+
135
+ end