nql 0.0.3 → 0.1.2

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.
@@ -1,109 +1,111 @@
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
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 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
+ '!:' => 'not_cont',
82
+ '~' => 'matches'
83
+ }
84
+ comparators[text_value]
85
+ end
86
+ }
87
+ end
88
+
89
+ rule text
90
+ (alphanumeric / utf8 / symbol)+
91
+ (space (alphanumeric / utf8 / symbol)+)*
92
+ end
93
+
94
+ rule alphanumeric
95
+ [a-zA-Z0-9_.]+
96
+ end
97
+
98
+ rule space
99
+ ' '*
100
+ end
101
+
102
+ rule symbol
103
+ [><=+-\/\\@#$%!?:]
104
+ end
105
+
106
+ rule utf8
107
+ [\u00c0\u00c1\u00c2\u00c3\u00c4\u00c7\u00c8\u00c9\u00ca\u00cb\u00cc\u00cd\u00ce\u00cf\u00d1\u00d2\u00d3\u00d4\u00d5\u00d6\u00d9\u00da\u00db\u00dc\u00e0\u00e1\u00e2\u00e3\u00e4\u00e7\u00e8\u00e9\u00ea\u00eb\u00ec\u00ed\u00ee\u00ef\u00f1\u00f2\u00f3\u00f4\u00f5\u00f6\u00f9\u00fa\u00fb\u00fc]
108
+ end
109
+
110
+ end
109
111
  end
@@ -0,0 +1,62 @@
1
+ module NQL
2
+ class Query
3
+
4
+ attr_reader :model
5
+ attr_reader :text
6
+ attr_reader :expression
7
+
8
+ def initialize(model, text)
9
+ raise InvalidModelError.new model unless model.ancestors.include? ::ActiveRecord::Base
10
+ raise DataTypeError.new text if text && !text.is_a?(String)
11
+ @model = model
12
+ @text = text
13
+ evaluate
14
+ end
15
+
16
+ def ransack_search
17
+ if expression
18
+ model.search(expression.to_ransack)
19
+ else
20
+ model.search
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def evaluate
27
+ if text.nil? || text.strip.empty?
28
+ @expression = nil
29
+ else
30
+ parser = SyntaxParser.new
31
+ @expression = parser.parse(text)
32
+ raise SyntaxError.new(parser.failure_reason) unless expression
33
+ validate_attributes!
34
+ end
35
+ end
36
+
37
+ def validate_attributes!
38
+ return unless expression
39
+ extended_attributes = model.column_names | model.reflections.flat_map { |k, v| v.klass.column_names.map { |c| "#{k}_#{c}" } }
40
+ invalid_attributes(expression.to_ransack, extended_attributes).tap do |attributes|
41
+ raise AttributesNotFoundError.new model, attributes if attributes.any?
42
+ end
43
+ end
44
+
45
+ def invalid_attributes(node, valid_attributes)
46
+ return [] unless node
47
+
48
+ node.deep_symbolize_keys.flat_map do |k, v|
49
+ if k == :a
50
+ [v['0'.to_sym][:name]] unless valid_attributes.include?(v['0'.to_sym][:name])
51
+ else
52
+ if v.is_a?(Hash)
53
+ invalid_attributes(v, valid_attributes)
54
+ elsif v.is_a?(Array)
55
+ v.select { |e| e.is_a?(Hash) }.flat_map { |e| invalid_attributes(e, valid_attributes) }
56
+ end
57
+ end
58
+ end.compact
59
+ end
60
+
61
+ end
62
+ end
@@ -1,3 +1,3 @@
1
- module NQL
2
- VERSION = '0.0.3'
3
- end
1
+ module NQL
2
+ VERSION = '0.1.2'
3
+ end
@@ -1,25 +1,27 @@
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
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', '~> 1.4.0'
19
+ s.add_dependency 'activerecord', '>= 3.2.0'
20
+ s.add_dependency 'activesupport', '>= 3.2.0'
21
+ s.add_dependency 'ransack', '~> 0.7'
22
+
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'sqlite3', '~> 1.3.0'
25
+ s.add_development_dependency 'rspec', '~> 3.0'
26
+ s.add_development_dependency 'simplecov'
27
+ end
@@ -1,147 +1,164 @@
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
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+
4
+ describe NQL::SyntaxParser, '-> Comparison' do
5
+
6
+ let(:parser) { NQL::SyntaxParser.new }
7
+
8
+ context 'Structure and comparators' do
9
+
10
+ it 'Equals' do
11
+ tree = parser.parse('var = value')
12
+
13
+ tree.comparison.variable.text_value.should eq 'var'
14
+ tree.comparison.comparator.text_value.should eq '='
15
+ tree.comparison.value.text_value.should eq 'value'
16
+ end
17
+
18
+ it 'Not equals' do
19
+ tree = parser.parse('var != value')
20
+
21
+ tree.comparison.variable.text_value.should eq 'var'
22
+ tree.comparison.comparator.text_value.should eq '!='
23
+ tree.comparison.value.text_value.should eq 'value'
24
+ end
25
+
26
+ it 'Greater than' do
27
+ tree = parser.parse('var > value')
28
+
29
+ tree.comparison.variable.text_value.should eq 'var'
30
+ tree.comparison.comparator.text_value.should eq '>'
31
+ tree.comparison.value.text_value.should eq 'value'
32
+ end
33
+
34
+ it 'Greater or equals than' do
35
+ tree = parser.parse('var >= value')
36
+
37
+ tree.comparison.variable.text_value.should eq 'var'
38
+ tree.comparison.comparator.text_value.should eq '>='
39
+ tree.comparison.value.text_value.should eq 'value'
40
+ end
41
+
42
+ it 'Less than' do
43
+ tree = parser.parse('var < value')
44
+
45
+ tree.comparison.variable.text_value.should eq 'var'
46
+ tree.comparison.comparator.text_value.should eq '<'
47
+ tree.comparison.value.text_value.should eq 'value'
48
+ end
49
+
50
+ it 'Less or equals than' do
51
+ tree = parser.parse('var <= value')
52
+
53
+ tree.comparison.variable.text_value.should eq 'var'
54
+ tree.comparison.comparator.text_value.should eq '<='
55
+ tree.comparison.value.text_value.should eq 'value'
56
+ end
57
+
58
+ it 'Contains' do
59
+ tree = parser.parse('var : value')
60
+
61
+ tree.comparison.variable.text_value.should eq 'var'
62
+ tree.comparison.comparator.text_value.should eq ':'
63
+ tree.comparison.value.text_value.should eq 'value'
64
+ end
65
+
66
+ it 'Not contains' do
67
+ tree = parser.parse('var !: value')
68
+
69
+ tree.comparison.variable.text_value.should eq 'var'
70
+ tree.comparison.comparator.text_value.should eq '!:'
71
+ tree.comparison.value.text_value.should eq 'value'
72
+ end
73
+
74
+ it 'Matches' do
75
+ tree = parser.parse('var ~ value')
76
+
77
+ tree.comparison.variable.text_value.should eq 'var'
78
+ tree.comparison.comparator.text_value.should eq '~'
79
+ tree.comparison.value.text_value.should eq 'value'
80
+ end
81
+
82
+ end
83
+
84
+ context 'Space separators' do
85
+
86
+ it 'Without spaces' do
87
+ tree = parser.parse('var=value')
88
+
89
+ tree.comparison.variable.text_value.should eq 'var'
90
+ tree.comparison.comparator.text_value.should eq '='
91
+ tree.comparison.value.text_value.should eq 'value'
92
+ end
93
+
94
+ it 'With many spaces' do
95
+ tree = parser.parse('var = value')
96
+
97
+ tree.comparison.variable.text_value.should eq 'var'
98
+ tree.comparison.comparator.text_value.should eq '='
99
+ tree.comparison.value.text_value.should eq 'value'
100
+ end
101
+
102
+ end
103
+
104
+ context 'Variable names' do
105
+
106
+ it 'With numbers' do
107
+ tree = parser.parse('var1 = value')
108
+ tree.comparison.variable.text_value.should eq 'var1'
109
+ end
110
+
111
+ it 'With uppercase' do
112
+ tree = parser.parse('varName = value')
113
+ tree.comparison.variable.text_value.should eq 'varName'
114
+ end
115
+
116
+ it 'With underscore' do
117
+ tree = parser.parse('var_name = value')
118
+ tree.comparison.variable.text_value.should eq 'var_name'
119
+ end
120
+
121
+ it 'With dot' do
122
+ tree = parser.parse('var.name = value')
123
+ tree.comparison.variable.text_value.should eq 'var.name'
124
+ end
125
+
126
+ end
127
+
128
+ context 'Values' do
129
+
130
+ it 'With numbers' do
131
+ tree = parser.parse('var = value1')
132
+ tree.comparison.value.text_value.should eq 'value1'
133
+ end
134
+
135
+ it 'With uppercase' do
136
+ tree = parser.parse('var = valueDummy')
137
+ tree.comparison.value.text_value.should eq 'valueDummy'
138
+ end
139
+
140
+ it 'With dot' do
141
+ tree = parser.parse('var = value.dummy')
142
+ tree.comparison.value.text_value.should eq 'value.dummy'
143
+ end
144
+
145
+
146
+ it 'With utf8 chars and symbols' do
147
+ utf8_symbols = "ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜàáâãäçèéêëìíîïñòóôõöùúûü"
148
+ tree = parser.parse("var = .#+-#{utf8_symbols}")
149
+ tree.comparison.value.text_value.should eq ".#+-#{utf8_symbols}"
150
+ end
151
+
152
+ it 'With spaces' do
153
+ tree = parser.parse('var = value 123')
154
+ tree.comparison.value.text_value.should eq 'value 123'
155
+ end
156
+
157
+ it 'With comparators, symbols and spaces' do
158
+ tree = parser.parse('var = value1 > value2 ! value3')
159
+ tree.comparison.value.text_value.should eq 'value1 > value2 ! value3'
160
+ end
161
+
162
+ end
163
+
164
+ end