nql 0.0.1

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,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