nql 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ script: bundle exec rspec spec
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # NQL
2
2
 
3
+ [![Build Status](https://travis-ci.org/gabynaiman/nql.png)](https://travis-ci.org/gabynaiman/nql)
4
+
3
5
  Natural Query Language built on top of ActiveRecord and Ransack
4
6
 
5
7
  ## Installation
@@ -36,16 +38,29 @@ Or install it yourself as:
36
38
 
37
39
  Converts from natural language to query expression
38
40
 
39
- q = '(name: arg | name: br) & region = south'
40
- Country.search(NQL.to_ransack(q)).result.to_sql
41
+ Country.nql('(name: arg | name: br) & region = south').to_sql
41
42
  => "SELECT coutries.* FROM countries WHERE (countries.name LIKE '%arg%' OR countries.name LIKE '%br%') AND region = 'south'"
42
43
 
43
44
  ### Joins support
44
45
 
45
- q = 'cities.name: buenos'
46
- Country.search(NQL.to_ransack(q)).result.to_sql
46
+ Country.nql('cities.name: buenos').to_sql
47
47
  => "SELECT countries.* FROM countries LEFT OUTER JOIN cities ON countries.id = cities.country_id WHERE cities.name LIKE '%buenos%'"
48
48
 
49
+ ### Invalid expressions handling
50
+
51
+ Safe query
52
+
53
+ Country.nql('xyz').to_sql
54
+ => "SELECT coutries.* FROM countries WHERE (1=2)
55
+
56
+ Raising exceptions
57
+
58
+ Country.nql!('xyz') => raise NQL::SyntaxError
59
+
60
+ Country.nql!('xyz: arg') => raise NQL::AttributesNotFoundError
61
+
62
+ Country.nql!(1234) => raise NQL::DataTypeError
63
+
49
64
  ## Contributing
50
65
 
51
66
  1. Fork it
data/lib/nql.rb CHANGED
@@ -4,22 +4,8 @@ require 'active_support/all'
4
4
  require 'ransack'
5
5
 
6
6
  require 'nql/version'
7
+ require 'nql/extension/hash'
8
+ require 'nql/extension/active_record'
7
9
  require 'nql/grammar'
8
- require 'nql/invalid_expression_error'
9
-
10
- module NQL
11
-
12
- def self.to_ransack(query)
13
- return nil if query.nil? || query.strip.empty?
14
- expression = parser.parse(query)
15
- raise InvalidExpressionError.new(parser.failure_reason) unless expression
16
- expression.to_ransack
17
- end
18
-
19
- private
20
-
21
- def self.parser
22
- @@parser ||= SyntaxParser.new
23
- end
24
-
25
- end
10
+ require 'nql/query'
11
+ require 'nql/error'
@@ -0,0 +1,26 @@
1
+ module NQL
2
+ class Error < StandardError
3
+ end
4
+
5
+ class SyntaxError < Error
6
+ end
7
+
8
+ class DataTypeError < Error
9
+ def initialize(text)
10
+ super "#{text} must be a String"
11
+ end
12
+ end
13
+
14
+ class InvalidModelError < Error
15
+ def initialize(model)
16
+ super "#{model} must be subclass of ActiveRecord::Base"
17
+ end
18
+ end
19
+
20
+ class AttributesNotFoundError < Error
21
+ def initialize(model, attributes)
22
+ super "#{model} does not contains the attributes #{attributes}"
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ def self.nql(query, options={})
5
+ nql! query, options
6
+ rescue NQL::Error
7
+ self.where('1=2')
8
+ end
9
+
10
+ def self.nql!(query, options={})
11
+ nql_search(query).result(options)
12
+ end
13
+
14
+ def self.nql_search(query)
15
+ NQL::Query.new(self, query).ransack_search
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ class Hash
2
+ def deep_symbolize_keys
3
+ inject({}) { |result, (key, value)|
4
+ value = value.deep_symbolize_keys if value.is_a?(Hash)
5
+ result[(key.to_sym rescue key) || key] = value
6
+ result
7
+ }
8
+ end unless Hash.method_defined?(:deep_symbolize_keys)
9
+ 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
1
  module NQL
2
- VERSION = '0.0.5'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -15,10 +15,10 @@ Gem::Specification.new do |s|
15
15
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
16
  s.require_paths = ["lib"]
17
17
 
18
- s.add_dependency 'treetop'
18
+ s.add_dependency 'treetop', '~> 1.4'
19
19
  s.add_dependency 'activerecord', '>= 3.2.0'
20
20
  s.add_dependency 'activesupport', '>= 3.2.0'
21
- s.add_dependency 'ransack'
21
+ s.add_dependency 'ransack', '~> 0.7'
22
22
 
23
23
  s.add_development_dependency 'sqlite3'
24
24
  s.add_development_dependency 'rspec'
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe NQL::Query do
4
+
5
+ before :all do
6
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ":memory:"
7
+ ActiveRecord::Base.connection
8
+ ActiveRecord::Migrator.migrate ActiveRecord::Migrator.migrations_path
9
+ end
10
+
11
+ it 'Valid expression' do
12
+ query = NQL::Query.new Country, 'name: Argentina'
13
+
14
+ query.text.should eq 'name: Argentina'
15
+ query.expression.should be_a Treetop::Runtime::SyntaxNode
16
+ end
17
+
18
+ it 'Valid with empty expression' do
19
+ query = NQL::Query.new Country, ''
20
+
21
+ query.text.should eq ''
22
+ query.expression.should be_nil
23
+ end
24
+
25
+ it 'Valid with nil expression' do
26
+ query = NQL::Query.new Country, nil
27
+
28
+ query.text.should be_nil
29
+ query.expression.should be_nil
30
+ end
31
+
32
+ it 'Invalid model type' do
33
+ expect { NQL::Query.new Object, nil }.to raise_error NQL::InvalidModelError
34
+ end
35
+
36
+ it 'Invalid expression type' do
37
+ expect { NQL::Query.new Country, Object.new }.to raise_error NQL::DataTypeError
38
+ end
39
+
40
+ it 'Invalid expression syntax' do
41
+ expect { NQL::Query.new Country, 'xyz1234' }.to raise_error NQL::SyntaxError
42
+ end
43
+
44
+ it 'Invalid fields' do
45
+ expect { NQL::Query.new Country, 'name: Argentina | xyz: 1234 | abc: 0000'}.to raise_error NQL::AttributesNotFoundError
46
+ end
47
+
48
+ end
@@ -12,42 +12,42 @@ describe 'SQL generation' do
12
12
 
13
13
  it 'Equals' do
14
14
  q = 'name = abcd'
15
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE \"countries\".\"name\" = 'abcd'"
15
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE \"countries\".\"name\" = 'abcd'"
16
16
  end
17
17
 
18
18
  it 'Not equals' do
19
19
  q = 'name != abcd'
20
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" != 'abcd')"
20
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" != 'abcd')"
21
21
  end
22
22
 
23
23
  it 'Greater than' do
24
24
  q = 'name > abcd'
25
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" > 'abcd')"
25
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" > 'abcd')"
26
26
  end
27
27
 
28
28
  it 'Greater or equals than' do
29
29
  q = 'name >= abcd'
30
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" >= 'abcd')"
30
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" >= 'abcd')"
31
31
  end
32
32
 
33
33
  it 'Less than' do
34
34
  q = 'name < abcd'
35
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" < 'abcd')"
35
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" < 'abcd')"
36
36
  end
37
37
 
38
38
  it 'Less or equals than' do
39
39
  q = 'name <= abcd'
40
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" <= 'abcd')"
40
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" <= 'abcd')"
41
41
  end
42
42
 
43
43
  it 'Contains' do
44
44
  q = 'name : abcd'
45
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
45
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
46
46
  end
47
47
 
48
48
  it 'Matches' do
49
49
  q = 'name ~ abcd'
50
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE 'abcd')"
50
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (\"countries\".\"name\" LIKE 'abcd')"
51
51
  end
52
52
 
53
53
  end
@@ -56,22 +56,22 @@ describe 'SQL generation' do
56
56
 
57
57
  it 'And' do
58
58
  q = 'id > 1234 & name = abcd'
59
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd'))"
59
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd'))"
60
60
  end
61
61
 
62
62
  it 'Or' do
63
63
  q = 'id < 1234 | name : abcd'
64
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" < 1234 OR \"countries\".\"name\" LIKE '%abcd%'))"
64
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" < 1234 OR \"countries\".\"name\" LIKE '%abcd%'))"
65
65
  end
66
66
 
67
67
  it 'And then Or' do
68
68
  q = 'id > 1234 & name = abcd | name : efgh'
69
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND (\"countries\".\"name\" = 'abcd' OR \"countries\".\"name\" LIKE '%efgh%')))"
69
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"id\" > 1234 AND (\"countries\".\"name\" = 'abcd' OR \"countries\".\"name\" LIKE '%efgh%')))"
70
70
  end
71
71
 
72
72
  it 'With parentheses' do
73
73
  q = '(id > 1234 & name = abcd) | name : efgh'
74
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"name\" LIKE '%efgh%' OR (\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd')))"
74
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE ((\"countries\".\"name\" LIKE '%efgh%' OR (\"countries\".\"id\" > 1234 AND \"countries\".\"name\" = 'abcd')))"
75
75
  end
76
76
 
77
77
  end
@@ -80,17 +80,17 @@ describe 'SQL generation' do
80
80
 
81
81
  it 'Parent join' do
82
82
  q = 'country.name : abcd'
83
- City.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"cities\".* FROM \"cities\" LEFT OUTER JOIN \"countries\" ON \"countries\".\"id\" = \"cities\".\"country_id\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
83
+ City.nql(q).should produce_sql "SELECT \"cities\".* FROM \"cities\" LEFT OUTER JOIN \"countries\" ON \"countries\".\"id\" = \"cities\".\"country_id\" WHERE (\"countries\".\"name\" LIKE '%abcd%')"
84
84
  end
85
85
 
86
86
  it 'Children join' do
87
87
  q = 'cities.name : abcd'
88
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
88
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
89
89
  end
90
90
 
91
91
  it 'Children join distinct' do
92
92
  q = 'cities.name : abcd'
93
- Country.search(NQL.to_ransack(q)).result(distinct: true).should produce_sql "SELECT DISTINCT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
93
+ Country.nql(q, distinct: true).should produce_sql "SELECT DISTINCT \"countries\".* FROM \"countries\" LEFT OUTER JOIN \"cities\" ON \"cities\".\"country_id\" = \"countries\".\"id\" WHERE (\"cities\".\"name\" LIKE '%abcd%')"
94
94
  end
95
95
 
96
96
  end
@@ -99,22 +99,22 @@ describe 'SQL generation' do
99
99
 
100
100
  it 'Nil' do
101
101
  q = nil
102
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\""
102
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\""
103
103
  end
104
104
 
105
105
  it 'Empty' do
106
106
  q = ''
107
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\""
107
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\""
108
108
  end
109
109
 
110
110
  it 'Empty with spaces' do
111
111
  q = ' '
112
- Country.search(NQL.to_ransack(q)).result.should produce_sql "SELECT \"countries\".* FROM \"countries\""
112
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\""
113
113
  end
114
114
 
115
115
  it 'Partial expression' do
116
116
  q = 'id ='
117
- expect { Country.search(NQL.to_ransack(q)).result }.to raise_exception NQL::InvalidExpressionError
117
+ Country.nql(q).should produce_sql "SELECT \"countries\".* FROM \"countries\" WHERE (1=2)"
118
118
  end
119
119
 
120
120
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,24 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-15 00:00:00.000000000 Z
12
+ date: 2013-02-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: treetop
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ! '>='
19
+ - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '0'
21
+ version: '1.4'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  none: false
26
26
  requirements:
27
- - - ! '>='
27
+ - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: '0'
29
+ version: '1.4'
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: activerecord
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -64,17 +64,17 @@ dependencies:
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
67
- - - ! '>='
67
+ - - ~>
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '0.7'
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
- - - ! '>='
75
+ - - ~>
76
76
  - !ruby/object:Gem::Version
77
- version: '0'
77
+ version: '0.7'
78
78
  - !ruby/object:Gem::Dependency
79
79
  name: sqlite3
80
80
  requirement: !ruby/object:Gem::Requirement
@@ -115,14 +115,18 @@ extensions: []
115
115
  extra_rdoc_files: []
116
116
  files:
117
117
  - .gitignore
118
+ - .travis.yml
118
119
  - Gemfile
119
120
  - LICENSE
120
121
  - README.md
121
122
  - Rakefile
122
123
  - lib/nql.rb
124
+ - lib/nql/error.rb
125
+ - lib/nql/extension/active_record.rb
126
+ - lib/nql/extension/hash.rb
123
127
  - lib/nql/grammar.rb
124
128
  - lib/nql/grammar.treetop
125
- - lib/nql/invalid_expression_error.rb
129
+ - lib/nql/query.rb
126
130
  - lib/nql/version.rb
127
131
  - nql.gemspec
128
132
  - spec/comparison_parser_spec.rb
@@ -131,6 +135,7 @@ files:
131
135
  - spec/migrations/20121108154508_create_cities.rb
132
136
  - spec/models/city.rb
133
137
  - spec/models/country.rb
138
+ - spec/query_spec.rb
134
139
  - spec/ransack_spec.rb
135
140
  - spec/spec_helper.rb
136
141
  - spec/sql_spec.rb
@@ -165,6 +170,8 @@ test_files:
165
170
  - spec/migrations/20121108154508_create_cities.rb
166
171
  - spec/models/city.rb
167
172
  - spec/models/country.rb
173
+ - spec/query_spec.rb
168
174
  - spec/ransack_spec.rb
169
175
  - spec/spec_helper.rb
170
176
  - spec/sql_spec.rb
177
+ has_rdoc:
@@ -1,7 +0,0 @@
1
- module NQL
2
- class InvalidExpressionError < StandardError
3
- def initialize(message)
4
- super
5
- end
6
- end
7
- end