queryfy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c768903e4796eae77d516db581ddd7e81082cf5c
4
+ data.tar.gz: f39cedbe011e43574d9fe1c576c18da29406fd23
5
+ SHA512:
6
+ metadata.gz: 1f9a43dcb7b136941069f71f850633b964c2dad17f98b52dd5eedae4979a2bd8165abfceb4ce1e2783f03981ab6a8de4535fb39cf750bcc737b983a390fb4cbf
7
+ data.tar.gz: d0ecbe8bc5f0d6602d2b9df2fb6c22acfcfb0d390664cf927ae980122268c9288a58ecf166e0da16b01ee269f4543e470811d5af5381bb5bff843c868367b794
data/.gitignore ADDED
@@ -0,0 +1,33 @@
1
+ # for a library or gem, you might want to ignore these files since the code is
2
+ # intended to run in multiple environments; otherwise, check them in:
3
+ .ruby-gemset
4
+ .ruby-version
5
+ # Gemfile.lock
6
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
7
+ ## Documentation cache and generated files:
8
+ ## Environment normalisation:
9
+ ## Specific to RubyMotion:
10
+ *.gem
11
+ *.rbc
12
+ .dat*
13
+ .repl_history
14
+ .rvmrc
15
+ /.bundle/
16
+ /.config
17
+ /.yardoc
18
+ /.yardoc/
19
+ /Gemfile.lock
20
+ /InstalledFiles
21
+ /_yardoc/
22
+ /coverage/
23
+ /doc/
24
+ /lib/bundler/man/
25
+ /pkg/
26
+ /rdoc/
27
+ /spec/examples.txt
28
+ /spec/reports/
29
+ /test/tmp/
30
+ /test/version_tmp/
31
+ /tmp/
32
+ /vendor/bundle
33
+ build/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'sqlite3'
4
+
5
+ # Specify your gem's dependencies in queryfy.gemspec
6
+ rails = ENV['RAILS'] || '~> 4.2.0'
7
+ gem 'rails', rails
8
+
9
+ gem 'pry'
10
+ gem 'pry-doc'
11
+ gem 'pry-nav'
12
+ gem 'pry-rescue'
13
+ gem 'pry-stack_explorer'
14
+
15
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Bonemind
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Queryfy
2
+
3
+ Queryfy is a gem that allows you to simply and easily paginate and filter activerecord models using queryparams.
4
+ The gem assumes you pass it a hash of queryparams which contain a filterstring in `filter`, and optionally `offset` and `limit` fields.
5
+
6
+ Queryfy uses a filterstring to query models that supports nesting of conditions, e.g:
7
+ ```
8
+ name=="name"&&(desc="desc"||(isbn=2||isbn=4))||author=~"%orwell%"
9
+ ```
10
+
11
+ This gem uses [FilterLexer](https://github.com/MaienM/FilterLexer/) to parse the filterstring
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'queryfy'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install queryfy
28
+
29
+ ## Usage
30
+
31
+ ### General
32
+ ```
33
+ require 'queryfy'
34
+
35
+ class SomeModel
36
+ include Queryfy
37
+ end
38
+
39
+ SomeModel.queryfy(queryparams)
40
+ ```
41
+
42
+ The gem adds makes a `queryfy` method available to ActiveRecord::Base when included.
43
+ The queryfy method takes a hash in the following format:
44
+ ```
45
+ {'filter': 'name=="name", 'offset': 50, 'limit': 10}
46
+ ```
47
+ All three are optional, and any extra values are ignored.
48
+
49
+ Defaults:
50
+ ```
51
+ offset = 0
52
+ limit = 50
53
+ ```
54
+
55
+ If filter is either nil or empty, queryfy will assume everything should be selected, almost like `SomeModel.all`
56
+
57
+ After calling `queryfy` you will get back the following:
58
+ ```
59
+ {data: [your data], count: total results, offset: the offset used, limit: the limit used}
60
+ ```
61
+
62
+ ### Exceptions
63
+
64
+ All exceptions queryfy can raise inherit from `QueryfyError`
65
+
66
+ Currently, the following exceptions exist:
67
+ ```
68
+ FilterParseError: Occurs when filter_lexer fails to parse the filter_query
69
+ NoSuchFieldError: Occurs when trying to filter on a nonexistant column
70
+ ```
71
+
72
+ ### Querystrings
73
+
74
+ Queryfy supports arbitratily deeply nested conditions in filters, and uses
75
+ `filter_lexer` under the hood to parse the filter strings
76
+
77
+ Examples:
78
+ ```
79
+ //SQL: name = 'name'
80
+ name=="name"
81
+
82
+ //SQL: name = 'name' AND description = 'desc'
83
+ name=="name"&&description=="desc"
84
+
85
+ //SQL: name = 'name' AND (description = 'desc1' OR isb = 1234)
86
+ name=="name"&&(description=="desc1"||isbn=1234)
87
+
88
+ //SQL: name = 'name' AND (description = 'desc1' OR isb = 1234) OR name != 'somename'
89
+ name=="name"&&(description=="desc1"||isbn=1234)||name!="somename"
90
+
91
+ //SQL: name = 'name' AND (description = 'desc1' OR (isb = 1234 || isbn = 5678))
92
+ name=="name"&&(description=="desc1"||(isbn=1234||isbn=5678))
93
+ ```
94
+
95
+ ### Operators
96
+
97
+ Since queryfy builds on filter_lexer it supports all operators filter lexer supports:
98
+ ```
99
+ Equal: ==, eq, EQ, is, IS
100
+ Not equal: !=, <>, neq, NEQ, not is, NOT IS, is not, IS NOT
101
+ Less than: <, lt, LT
102
+ Less or equal: <=, le, LE
103
+ greater than: >, gt, GT
104
+ Greated than or equal: >=, ge, GE
105
+ Like: =~, like, LIKE
106
+ Not like: !=~, not like, NOT LIKE
107
+ ```
108
+
109
+ ## Development
110
+
111
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
112
+
113
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
114
+
115
+ ## Contributing
116
+
117
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Bonemind/queryfy.
118
+
119
+
120
+ ## License
121
+
122
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
123
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList['test/*_test.rb']
9
+ t.verbose = true
10
+ end
11
+
12
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "queryfy"
5
+ require "active_record"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/lib/queryfy.rb ADDED
@@ -0,0 +1,117 @@
1
+ require "queryfy/version"
2
+ require 'filter_lexer'
3
+ require 'queryfy/filter_lexer/formatter.rb'
4
+ require 'queryfy/queryfy_errors.rb'
5
+ require 'queryfy/configuration'
6
+ require 'active_record'
7
+
8
+ module Queryfy
9
+ extend Configuration
10
+ define_setting :max_limit, 100
11
+ define_setting :default_limit, 50
12
+
13
+ # Actually builds the query
14
+ def self.build_query(klass, querystring, limit = 50, offset = 0)
15
+ limit = [max_limit, limit.to_i].min
16
+ # Handle empty and nil queries
17
+ if (querystring.nil? || querystring == '')
18
+ return {
19
+ data: klass.limit(limit).offset(offset),
20
+ count: klass.all.count,
21
+ limit: limit.to_i, offset: offset.to_i
22
+ }
23
+ end
24
+
25
+ begin
26
+ tree = FilterLexer::Parser.parse(querystring)
27
+ rescue FilterLexer::ParseException => e
28
+ raise FilterParseError, "Failed to parse querystring, #{ e.message }"
29
+ return
30
+ end
31
+
32
+ # Build the query with pagination
33
+ query = klass.arel_table.project(Arel.star).take(limit).skip(offset)
34
+
35
+ cleaned_tree = self.clean_tree(tree)
36
+ arel_tree = self.cleaned_to_arel(klass.arel_table, cleaned_tree)
37
+ # If we want to actually query, add the conditions to query
38
+ query = query.where(arel_tree) unless arel_tree.nil?
39
+
40
+ total = 0
41
+ if arel_tree.nil?
42
+ total = klass.all.count
43
+ else
44
+ countquery = klass.arel_table.project(klass.arel_table[klass.primary_key.to_sym].count.as('total')).where(arel_tree)
45
+ results = klass.connection.execute(countquery.to_sql)
46
+ if results.count == 0
47
+ raise QueryfyError, 'Failed to select count, this should not happen'
48
+ else
49
+ total = results[0]['total']
50
+ end
51
+ end
52
+
53
+ # Return the results
54
+ return {data: klass.find_by_sql(query.to_sql), count: total.to_i, limit: limit.to_i, offset: offset.to_i}
55
+ end
56
+
57
+ # Cleans the filterlexer tree
58
+ # Output is an array with either filterentries and operators, or an array of filterentries and operators
59
+ # The latter represents a group
60
+ def self.clean_tree(tree, input = [])
61
+ tree.elements.each do |el|
62
+ if el.is_a?(FilterLexer::Expression)
63
+ input += clean_tree(el)
64
+ elsif el.is_a?(FilterLexer::Group)
65
+ input += [clean_tree(el)]
66
+ else
67
+ input.push(el)
68
+ end
69
+ end
70
+ return input
71
+ end
72
+
73
+ # Converts a cleaned tree to something arel can understand
74
+ def self.cleaned_to_arel(arel_table, tree, ast = nil)
75
+ tree.each_with_index do |el, idx|
76
+ next if el.is_a?(FilterLexer::LogicalOperator)
77
+ operator = nil
78
+ operator = tree[idx - 1] if idx > 0
79
+ if el.is_a?(Array)
80
+ ast = join_ast(ast, arel_table.grouping(cleaned_to_arel(arel_table, el)), operator)
81
+ else
82
+ ast = join_ast(ast, el.to_arel(arel_table), operator)
83
+ end
84
+ end
85
+
86
+ return ast
87
+ end
88
+
89
+ # Merges an existing ast with the passed nodes and uses the operator as a merge operator
90
+ def self.join_ast(ast, nodes, operator)
91
+ if ast.nil? && !operator.nil?
92
+ raise InvalidFilterFormat, "Cannot join on nil tree with operator near #{operator.text_value}"
93
+ end
94
+ if operator.nil? || ast.nil?
95
+ ast = nodes
96
+ else
97
+ ast = ast.send(operator.to_arel, nodes)
98
+ end
99
+ return ast
100
+ end
101
+ end
102
+
103
+ class ActiveRecord::Base
104
+ def self.queryfy(queryparams)
105
+ filter = ''
106
+ offset = 0
107
+ limit = Queryfy::default_limit
108
+ if (queryparams.is_a?(Hash))
109
+ filter = queryparams['filter'] unless queryparams['filter'].nil?
110
+ offset = queryparams['offset'] unless queryparams['offset'].nil?
111
+ limit = queryparams['limit'] unless queryparams['limit'].nil?
112
+ elsif(queryparams.is_a?(String))
113
+ filter = queryparams
114
+ end
115
+ return Queryfy.build_query(self, filter, limit, offset)
116
+ end
117
+ end
@@ -0,0 +1,22 @@
1
+ # Provides module configuration
2
+ # Source: https://viget.com/extend/easy-gem-configuration-variables-with-defaults
3
+ module Configuration
4
+ def configuration
5
+ yield self
6
+ end
7
+ def define_setting(name, default = nil)
8
+ class_variable_set("@@#{name}", default)
9
+ define_class_method "#{name}=" do |value|
10
+ class_variable_set("@@#{name}", value)
11
+ end
12
+ define_class_method name do
13
+ class_variable_get("@@#{name}")
14
+ end
15
+ end
16
+ private
17
+ def define_class_method(name, &block)
18
+ (class << self; self; end).instance_eval do
19
+ define_method name, &block
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,96 @@
1
+ require 'queryfy/queryfy_errors'
2
+ module FilterLexer
3
+ class Filter
4
+ # Converts a FilterLexer::Filter to an arel node
5
+ def to_arel(arel_table)
6
+ # Get the elements we want to operate on
7
+ field = elements[0].text_value
8
+ operator_method = elements[1].to_arel
9
+ val = elements[2].text_value
10
+
11
+ # Check if the field we want to filter on exists
12
+ field_index = arel_table.engine.column_names.index(field)
13
+
14
+ # Field does not exist, fail
15
+ if field_index.nil?
16
+ raise NoSuchFieldError, "Unknown field #{ field }"
17
+ else
18
+ # Get the arel field name from our input, just to make sure
19
+ # there is nothing weird is in the input
20
+ field = arel_table.engine.column_names[field_index]
21
+ end
22
+ ast_node = arel_table[field.to_sym]
23
+
24
+ # Build an arel node from the resolved operator, value and field
25
+ return ast_node.send(operator_method, val)
26
+ end
27
+ end
28
+
29
+ # The list below converts Filter::Operators to arel functions
30
+ class AndOperator
31
+ def to_arel
32
+ return 'and'
33
+ end
34
+ end
35
+
36
+ class OrOperator
37
+ def to_arel
38
+ return 'or'
39
+ end
40
+ end
41
+
42
+ class EQOperator
43
+ def to_arel
44
+ return 'eq'
45
+ end
46
+ end
47
+
48
+ class NEQOperator
49
+ def to_arel
50
+ return 'not_eq'
51
+ end
52
+ end
53
+
54
+ class LTOperator
55
+ def to_arel
56
+ return 'lt'
57
+ end
58
+ end
59
+
60
+ class LEOperator
61
+ def to_arel
62
+ return 'lteq'
63
+ end
64
+ end
65
+
66
+ class GTOperator
67
+ def to_arel
68
+ return 'gt'
69
+ end
70
+ end
71
+
72
+ class GEOperator
73
+ def to_arel
74
+ return 'gteq'
75
+ end
76
+ end
77
+
78
+ class NotLikeOperator
79
+ def to_arel
80
+ return 'does_not_match'
81
+ end
82
+ end
83
+
84
+ class LikeOperator
85
+ def to_arel
86
+ return 'matches'
87
+ end
88
+ end
89
+
90
+ class StringLiteral
91
+ def text_value
92
+ val = super
93
+ return val.gsub!(/\A["']|["']\Z/, '')
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,12 @@
1
+ # Base exception class
2
+ class QueryfyError < StandardError
3
+ end
4
+
5
+ class FilterParseError < QueryfyError
6
+ end
7
+
8
+ class NoSuchFieldError < QueryfyError
9
+ end
10
+
11
+ class InvalidFilterFormat < QueryfyError
12
+ end
@@ -0,0 +1,3 @@
1
+ module Queryfy
2
+ VERSION = "0.1.0"
3
+ end
data/queryfy.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'queryfy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "queryfy"
8
+ spec.version = Queryfy::VERSION
9
+ spec.authors = ["Bonemind"]
10
+ spec.email = ["subhime@gmail.com"]
11
+
12
+ spec.summary = %q{Query activerecord models based on query strings}
13
+ spec.description = %q{Query activerecord models based on sql-like syntax with arbitratily deeply nested conditions}
14
+ spec.homepage = "https://github.com/Bonemind/Queryfy"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_runtime_dependency 'filter_lexer', '~>0.2', '>= 0.2.1'
31
+ spec.add_development_dependency "bundler", "~> 1.10"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: queryfy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bonemind
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-11-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: filter_lexer
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.10'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.10'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ description: Query activerecord models based on sql-like syntax with arbitratily deeply
62
+ nested conditions
63
+ email:
64
+ - subhime@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - ".gitignore"
70
+ - ".rspec"
71
+ - ".travis.yml"
72
+ - Gemfile
73
+ - LICENSE.txt
74
+ - README.md
75
+ - Rakefile
76
+ - bin/console
77
+ - bin/setup
78
+ - lib/queryfy.rb
79
+ - lib/queryfy/configuration.rb
80
+ - lib/queryfy/filter_lexer/formatter.rb
81
+ - lib/queryfy/queryfy_errors.rb
82
+ - lib/queryfy/version.rb
83
+ - queryfy.gemspec
84
+ homepage: https://github.com/Bonemind/Queryfy
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ allowed_push_host: https://rubygems.org
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.4.8
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Query activerecord models based on query strings
109
+ test_files: []
110
+ has_rdoc: