rao-query 0.0.3.pre

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19dcce90ee295149c4a55e9412bbcf3ecf98407d
4
+ data.tar.gz: 28f11c998767d9f7304e52793af3739638701ea5
5
+ SHA512:
6
+ metadata.gz: 064ab43e5fd6708d42008df4eb0f46b5f2fbb341104b94c16646818f4d9ba1dfeeb9c4c01407dfcca846d65a29ed71dbbd5e598348c3667e63e354a0c3c15407
7
+ data.tar.gz: 83f3bf5075cf12df8af694c39dd774ac0d1787f1d17efa725afc62dc610e055509e9a95aaa15e3370386bee3d4a780c98c1f31dc9489a6f99a934238625b0f92
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018 Roberto Vasquez Angel
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Rails Add-Ons Query
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'rao-query'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install rao-query
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Rails Add-Ons Query'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
@@ -0,0 +1,70 @@
1
+ module Rao
2
+ module Query
3
+ module Controller
4
+ # Example
5
+ #
6
+ # # app/controllers/posts_controller.rb
7
+ # class PostsController < ApplicationController
8
+ # include Rao::Query::Controller::QueryConcern
9
+ #
10
+ # def index
11
+ # @posts = with_conditions_from_query(Post).all
12
+ # end
13
+ # end
14
+ #
15
+ module QueryConcern
16
+ extend ActiveSupport::Concern
17
+
18
+ private
19
+
20
+ def with_conditions_from_query(scope)
21
+ if query_params.keys.include?('q')
22
+ condition_params = normalize_query_params(query_params)
23
+ else
24
+ condition_params = query_params
25
+ end
26
+
27
+ condition_params.reject.reject { |k,v| v.blank? }.each do |field, condition|
28
+ case field
29
+ when 'limit'
30
+ scope = scope.limit(condition.to_i)
31
+ when 'offset'
32
+ scope = scope.offset(condition.to_i)
33
+ when 'order'
34
+ scope = scope.order(condition)
35
+ when 'includes'
36
+ scope = scope.includes(condition.map(&:to_sym))
37
+ else
38
+ condition_statement = ::Rao::Query::ConditionParser.new(scope, field, condition).condition_statement
39
+ scope = scope.where(condition_statement)
40
+ end
41
+ end
42
+ scope
43
+ end
44
+
45
+ def query_params
46
+ default_query_params
47
+ end
48
+
49
+ def default_query_params
50
+ request.query_parameters.except(*default_query_params_exceptions)
51
+ end
52
+
53
+ def default_query_params_exceptions
54
+ %w(sort_by sort_direction utf8 commit page)
55
+ end
56
+
57
+ def normalize_query_params(params)
58
+ params['q'].each_with_object({}) { |(k, v), m| m[normalize_key(k)] = v }
59
+ end
60
+
61
+ def normalize_key(key)
62
+ splitted_key = key.split('_')
63
+ predicate = splitted_key.last
64
+ attribute = splitted_key[0..-2].join('_')
65
+ "#{attribute}(#{predicate})"
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ module Rao
2
+ module Query
3
+ module Controller
4
+ module QueryFormConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ view_helper Rao::Query::ApplicationHelper, as: :query_helper
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,149 @@
1
+ module Rao
2
+ module Query
3
+ class ConditionParser
4
+ OPERATOR_MAP = {
5
+ gt: :>,
6
+ gt_or_eq: :>=,
7
+ eq: :'=',
8
+ not_eq: :'<>',
9
+ lt_or_eq: :<=,
10
+ lt: :<,
11
+ null: :is_null,
12
+ not_null: :is_not_null,
13
+ cont: :like
14
+ }
15
+
16
+ def initialize(scope, field, condition)
17
+ @scope, @field, @condition = scope, field, condition
18
+ end
19
+
20
+ def condition_statement
21
+ build_condition_statement(@field, @condition)
22
+ end
23
+
24
+ private
25
+
26
+ def build_condition_statement(parent_key, condition, nested = false)
27
+ if is_a_condition?(parent_key) && !nested
28
+ table, column, operator = extract_table_column_and_operator(parent_key)
29
+ return handle_null_condition(column, operator) if is_null_operator?(operator)
30
+ # binding.pry
31
+ if operator == 'cont'
32
+ return ["#{table}.#{column} LIKE ?", "%#{normalized_condition(table, column, condition)}%"]
33
+ else
34
+ return ["#{table}.#{column} = ?", normalized_condition(table, column, condition)]
35
+ end
36
+ # if column_is_boolean?(column)
37
+ # ["#{column} = ?", to_boolean(condition)]
38
+ # else
39
+ # ["#{column} = ?", condition]
40
+ # end
41
+ else
42
+ if nested
43
+ column = extract_column(parent_key)
44
+ { column => condition }
45
+ else
46
+ { parent_key => build_condition_statement(condition.first[0], condition.first[1], true) }
47
+ end
48
+ end
49
+ end
50
+
51
+ def is_null_operator?(operator)
52
+ %w(null not_null).include?(operator)
53
+ end
54
+
55
+ def handle_null_condition(column, operator)
56
+ case operator.to_sym
57
+ when :null
58
+ "#{column} IS NULL"
59
+ when :not_null
60
+ "#{column} IS NOT NULL"
61
+ end
62
+ end
63
+
64
+ def is_a_condition?(obj)
65
+ !!extract_operator(obj)
66
+ end
67
+
68
+ def extract_operator(obj)
69
+ string = obj.to_s
70
+ operator_map.each do |key, value|
71
+ return value if string.end_with?("(#{key})")
72
+ end
73
+ nil
74
+ end
75
+
76
+ def extract_column(obj)
77
+ obj.to_s.split("(").first
78
+ end
79
+
80
+ # def extract_column_and_operator(string)
81
+ # if string =~ /([\.a-z_]{1,})\(([a-z_]{2,})\)/
82
+ # return $~[1], $~[2]
83
+ # end
84
+ # end
85
+
86
+ def extract_table_column_and_operator(string)
87
+ if string =~ /([\.a-z_]{1,})\(([a-z_]{2,})\)/
88
+ table_and_column = $~[1]
89
+ operator = $~[2]
90
+ column, table_or_association = table_and_column.split('.').reverse
91
+ if table_or_association.nil?
92
+ table = @scope.table_name
93
+ else
94
+ table = tables_and_classes_with_associations[table_or_association].table_name
95
+ end
96
+ return table, column, operator
97
+ end
98
+ end
99
+
100
+ def operator_map
101
+ OPERATOR_MAP
102
+ end
103
+
104
+ def column_is_boolean?(table_name, column_name)
105
+ scope, column = get_scope_and_column_from_column_name(column_name, table_name)
106
+ raise "Unknown column: #{column_name}" unless scope.columns_hash.has_key?(column)
107
+ scope.columns_hash[column].type == :boolean
108
+ end
109
+
110
+ def get_scope_and_column_from_column_name(column_name, table_name = nil)
111
+ if table_name == @scope.table_name
112
+ return @scope, column_name
113
+ else
114
+ # tables_and_classes = @scope.reflect_on_all_associations.reject { |a| a.polymorphic? }.each_with_object({}) { |a, memo| memo[a.table_name] = a.klass }
115
+ # associations_and_classes = @scope.reflect_on_all_associations.reject { |a| a.polymorphic? }.each_with_object({}) { |a, memo| memo[a.name.to_s] = a.klass }
116
+ # scope = tables_and_classes.merge(associations_and_classes)[$~[1]]
117
+ scope = tables_and_classes_with_associations[table_name]
118
+ return scope, column_name
119
+ end
120
+ end
121
+
122
+ def tables_and_classes_with_associations
123
+ tables_and_classes = @scope.reflect_on_all_associations.reject { |a| (a.respond_to?(:polymorphic?) && a.polymorphic?) || a.options[:polymorphic] == true }.each_with_object({}) { |a, memo| memo[a.table_name] = a.klass }
124
+ associations_and_classes = @scope.reflect_on_all_associations.reject { |a| (a.respond_to?(:polymorphic?) && a.polymorphic?) || a.options[:polymorphic] == true }.each_with_object({}) { |a, memo| memo[a.name.to_s] = a.klass }
125
+ tables_and_classes.merge(associations_and_classes)
126
+ end
127
+
128
+ def to_boolean(string)
129
+ result = case
130
+ when Rails.version < '4.2'
131
+ ::ActiveRecord::ConnectionAdapters::Column.value_to_boolean(string)
132
+ when Rails.version < '5.0'
133
+ ::ActiveRecord::Type::Boolean.new.type_cast_for_schema(string)
134
+ else
135
+ ::ActiveRecord::Type::Boolean.new.cast(string)
136
+ end
137
+ result.gsub('"', '')
138
+ end
139
+
140
+ def normalized_condition(table, column, condition)
141
+ if column_is_boolean?(table, column)
142
+ to_boolean(condition)
143
+ else
144
+ condition
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,131 @@
1
+ module Rao
2
+ module Query
3
+ # First you have to add the helper and the query concern to your controller:
4
+ #
5
+ # # app/controllers/application_controller.rb
6
+ # class ApplicationController < ActionController::Base
7
+ # include Rao::Query::Controller::QueryConcern
8
+ # view_helper Rao::Query::ApplicationHelper, as: :query_helper
9
+ # # ....
10
+ # end
11
+ #
12
+ class ApplicationHelper < ViewHelper::Base
13
+ # Example:
14
+ #
15
+ # # app/views/posts/index.html.haml
16
+ # = query_helper(self).form_for(@posts, url: posts_path, method: :get) do |f|
17
+ # = f.input :title_cont
18
+ # = f.input :body_cont
19
+ # = f.boolean :published_eq
20
+ # = f.submit nil, class: 'btn btn-primary'
21
+ # = f.reset nil, class: 'btn btn-default'
22
+ #
23
+ # # app/controllers/posts_controller.rb
24
+ # class ApplicationController < ActionController::Base
25
+ # def index
26
+ # @posts = with_conditions_from_query(Post).all
27
+ # end
28
+ #
29
+ # If you are using bootstrap you may want to display the form inline for a
30
+ # more compact view:
31
+ #
32
+ # = query_helper(self).form_for(collection, html: { class: 'form-inline' }) do |f|
33
+ #
34
+ def form_for(collection, options = {}, &block)
35
+ handle_simple_form_missing unless c.respond_to?(:simple_form_for)
36
+ wrapped_collection = SearchableCollection.new(collection, c.params[:q])
37
+ c.simple_form_for(wrapped_collection, options.reverse_merge(as: :q, url: c.collection_path, method: :get, builder: SearchFormBuilder), &block)
38
+ end
39
+
40
+ class SearchFormBuilder < SimpleForm::FormBuilder
41
+ def boolean(name, options = {})
42
+ translated_label = translate_label_for_boolean(name)
43
+ options.reverse_merge!(collection: [[I18n.t("search_form_builder.yes"), 1], [I18n.t("search_form_builder.no"), 0]], include_blank: true, label: translated_label)
44
+ input name, options
45
+ end
46
+
47
+ def input(name, options = {})
48
+ if association = options.delete(:association)
49
+ translated_label = translate_label(name, association)
50
+ input_name = "#{association}.#{name}"
51
+ else
52
+ translated_label = translate_label(name)
53
+ input_name = name
54
+ end
55
+ super(input_name, options.reverse_merge(label: translated_label))
56
+ end
57
+
58
+ def submit(title = nil, options = {})
59
+ title ||= I18n.t('search_form_builder.submit')
60
+ super(title, options)
61
+ end
62
+
63
+ def reset(title = nil, options = {})
64
+ title ||= I18n.t('search_form_builder.reset')
65
+ link_html = options.delete(:link_html) || {}
66
+ template.link_to(title, template.url_for(), link_html)
67
+ end
68
+
69
+ private
70
+
71
+ def translate_label(name, association = nil)
72
+ splitted_name = name.to_s.split('_')
73
+ attribute_name = splitted_name[0..-2].join('_')
74
+ predicate = splitted_name.last
75
+ translated_attribute_name = if association.nil?
76
+ klass_name = object.original_model_class_name
77
+ klass_name.constantize.human_attribute_name(attribute_name)
78
+ else
79
+ klass_name = object.original_model_class.reflect_on_association(association).klass.name
80
+ klass = klass_name.constantize
81
+ "#{klass.model_name.human} #{klass.human_attribute_name(attribute_name)}"
82
+ end
83
+ I18n.t("search_form_builder.predicates.#{predicate}", attribute_name: translated_attribute_name)
84
+ end
85
+
86
+ def translate_label_for_boolean(name)
87
+ splitted_name = name.to_s.split('_')
88
+ attribute_name = splitted_name[0..-2].join('_')
89
+ predicate = splitted_name.last
90
+ if association.nil?
91
+ klass_name = object.original_model_class_name
92
+ else
93
+ klass_name = object.original_model_class.reflect_on_association(association).klass
94
+ end
95
+ translated_attribute_name = klass_name.constantize.human_attribute_name(attribute_name)
96
+ I18n.t("search_form_builder.boolean_label", attribute_name: translated_attribute_name)
97
+ end
98
+ end
99
+
100
+ class SearchableCollection
101
+ include ActiveModel::Model
102
+ extend ActiveModel::Translation
103
+
104
+ def method_missing(method, *args)
105
+ if method.to_s.match(/(.+)_(gt|gt_or_eq|eq|not_eq|lt_or_eq|lt|null|not_null|cont)/)
106
+ @query.send(:[], method)
107
+ else
108
+ super
109
+ end
110
+ end
111
+
112
+ def initialize(collection, query)
113
+ @collection = collection
114
+ @query = query || {}
115
+ end
116
+
117
+ def original_model_class_name
118
+ @collection.class.to_s.deconstantize
119
+ end
120
+
121
+ def original_model_class
122
+ @collection.klass
123
+ end
124
+ end
125
+
126
+ def handle_simple_form_missing
127
+ raise "simple_form_for is not available. Please add simple_form to your Gemfile."
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,9 @@
1
+ module Rao
2
+ module Query
3
+ module Configuration
4
+ def configure
5
+ yield self
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Rao
2
+ module Query
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Rao::Query
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'rao/version'
2
+
3
+ module Rao
4
+ module Query
5
+ VERSION = ::Rao::VERSION
6
+ end
7
+ end
data/lib/rao/query.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "rao-view_helper"
2
+
3
+ require "rao/query/version"
4
+ require "rao/query/configuration"
5
+ require "rao/query/engine"
6
+
7
+ module Rao
8
+ module Query
9
+ extend Configuration
10
+ end
11
+ end
data/lib/rao-query.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rao/query'
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rao-query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3.pre
5
+ platform: ruby
6
+ authors:
7
+ - Roberto Vasquez Angel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rao
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rao-view_helper
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard-bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - roberto@vasquez-angel.de
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - MIT-LICENSE
133
+ - README.md
134
+ - Rakefile
135
+ - app/concerns/rao/query/controller/query_concern.rb
136
+ - app/concerns/rao/query/controller/query_form_concern.rb~
137
+ - app/parsers/rao/query/condition_parser.rb
138
+ - app/view_helpers/rao/query/application_helper.rb
139
+ - lib/rao-query.rb
140
+ - lib/rao/query.rb
141
+ - lib/rao/query/configuration.rb
142
+ - lib/rao/query/engine.rb
143
+ - lib/rao/query/version.rb
144
+ homepage: https://github.com/rao
145
+ licenses:
146
+ - MIT
147
+ metadata: {}
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">"
160
+ - !ruby/object:Gem::Version
161
+ version: 1.3.1
162
+ requirements: []
163
+ rubyforge_project:
164
+ rubygems_version: 2.6.14
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Simple Filter Queries for Ruby on Rails.
168
+ test_files: []