brita 0.9.3

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f5bb6a00b2f18b8868b43eb4b22803ed179bc63c
4
+ data.tar.gz: ff737c18910108bc92517f19f0b7a3fdc14aa0e7
5
+ SHA512:
6
+ metadata.gz: fc412bdc2c69093bf76631f2498949c60a7d377640bdbc06bba4d6ae91f77ce97029c9a892aa295361ddd999badc6c63dfff1ebfd69f54e35b0552a4d1c70f10
7
+ data.tar.gz: 30830b49f18d75cac7d2249fb9a09e671c4caa1b38ecf3f1a73e3b7514cc2f7049b4ebf77e7f240b147a6ea1d5206f60aa49a9fef4ef29190bbbb78874d8cb5f
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Adam Hess
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.
@@ -0,0 +1,194 @@
1
+ # Brita
2
+
3
+ [![Build Status](https://travis-ci.org/procore/brita.svg?branch=master)](https://travis-ci.org/procore/brita)
4
+
5
+ A tool to build your own filters!
6
+
7
+ ## Developer Usage
8
+ Include Brita in your controllers, and define some filters.
9
+
10
+ ```ruby
11
+ class PostsController < ApplicationController
12
+ include Brita
13
+
14
+ filter_on :title, type: :string
15
+
16
+ def index
17
+ render json: filtrate(Post.all)
18
+ end
19
+ end
20
+ ```
21
+
22
+ This will allow users to pass `?filters[title]=foo` and get the `Post`s with the title `foo`.
23
+
24
+ Brita will also handle rendering errors using the standard rails errors structure. You can add this to your controller by adding,
25
+
26
+ ```ruby
27
+ before_action :render_filter_errors, unless: :filters_valid?
28
+
29
+ def render_filter_errors
30
+ render json: { errors: filter_errors } and return
31
+ end
32
+ ```
33
+
34
+ to your controller.
35
+
36
+ These errors are based on the type that you told brita your param was.
37
+
38
+ ### Filter Types
39
+ Every filter must have a type, so that Brita knows what to do with it. The current valid filter types are:
40
+ * int - Filter on an integer column
41
+ * decimal - Filter on a decimal column
42
+ * boolean - Filter on a boolean column
43
+ * string - Filter on a string column
44
+ * text - Filter on a text column
45
+ * date - Filter on a date column
46
+ * time - Filter on a time column
47
+ * datetime - Filter on a datetime column
48
+ * scope - Filter on an ActiveRecord scope
49
+
50
+ ### Filter on Scopes
51
+ Just as your filter values are used to scope queries on a column, values you
52
+ pass to a scope filter will be used as arguments to that scope. For example:
53
+
54
+ ```ruby
55
+ class Post < ActiveRecord::Base
56
+ scope :with_body, ->(text) { where(body: text) }
57
+ end
58
+
59
+ class PostsController < ApplicationController
60
+ include Brita
61
+
62
+ filter_on :with_body, type: :scope
63
+
64
+ def index
65
+ render json: filtrate(Post.all)
66
+ end
67
+ end
68
+ ```
69
+
70
+ Passing `?filters[with_body]=my_text` will call the `with_body` scope with
71
+ `my_text` as the argument.
72
+
73
+ Scopes that accept no arguments are currently not supported.
74
+
75
+ #### Accessing Params with Filter Scopes
76
+
77
+ Filters with `type: :scope` have access to the params hash by passing in the desired keys to the `scope_params`. The keys passed in will be returned as a hash with their associated values, and should always appear as the last argument in your scope.
78
+
79
+ ```ruby
80
+ class Post < ActiveRecord::Base
81
+ scope :user_posts_on_date, ->(date, options) {
82
+ where(user_id: options[:user_id], blog_id: options[:blog_id], date: date)
83
+ }
84
+ end
85
+
86
+ class UsersController < ApplicationController
87
+ include Brita
88
+
89
+ filter_on :user_posts_on_date, type: :scope, scope_params: [:user_id, :blog_id]
90
+
91
+ def show
92
+ render json: filtrate(Post.all)
93
+ end
94
+ end
95
+ ```
96
+ Passing `?filters[user_posts_on_date]=10/12/20` will call the `user_posts_on_date` scope with
97
+ `10/12/20` as the the first argument, and will grab the `user_id` and `blog_id` out of the params and pass them as a hash, as the second argument.
98
+
99
+ ### Renaming Filter Params
100
+
101
+ A filter param can have a different field name than the column or scope. Use `internal_name` with the correct name of the column or scope.
102
+
103
+ ```ruby
104
+ class PostsController < ApplicationController
105
+ include Brita
106
+
107
+ filter_on :post_id, type: :int, internal_name: :id
108
+
109
+ end
110
+ ```
111
+
112
+ ### Sort Types
113
+ Every sort must have a type, so that Brita knows what to do with it. The current valid sort types are:
114
+ * int - Sort on an integer column
115
+ * decimal - Sort on a decimal column
116
+ * string - Sort on a string column
117
+ * text - Sort on a text column
118
+ * date - Sort on a date column
119
+ * time - Sort on a time column
120
+ * datetime - Sort on a datetime column
121
+ * scope - Sort on an ActiveRecord scope
122
+
123
+ ### Sort on Scopes
124
+ Just as your sort values are used to scope queries on a column, values you
125
+ pass to a scope sort will be used as arguments to that scope. For example:
126
+
127
+ ```ruby
128
+ class Post < ActiveRecord::Base
129
+ scope :order_on_body_no_params, -> { order(body: :asc) }
130
+ scope :order_on_body, ->(direction) { order(body: direction) }
131
+ scope :order_on_body_then_id, ->(body_direction, id_direction) { order(body: body_direction).order(id: id_direction) }
132
+ end
133
+
134
+ class PostsController < ApplicationController
135
+ include Brita
136
+
137
+ sort_on :order_by_body_ascending, internal_name: :order_on_body_no_params, type: :scope
138
+ sort_on :order_by_body, internal_name: :order_on_body, type: :scope, scope_params: [:direction]
139
+ sort_on :order_by_body_then_id, internal_name: :order_on_body_then_id, type: :scope, scope_params: [:direction, :asc]
140
+
141
+
142
+ def index
143
+ render json: filtrate(Post.all)
144
+ end
145
+ end
146
+ ```
147
+
148
+ `scope_params` takes an order-specific array of the scope's arguments. Passing in the param :direction allows the consumer to choose which direction to sort in (ex. `-order_by_body` will sort `:desc` while `order_by_body` will sort `:asc`)
149
+
150
+ Passing `?sort=-order_by_body` will call the `order_on_body` scope with
151
+ `:desc` as the argument. The direction is the only argument that the consumer has control over.
152
+ Passing `?sort=-order_by_body_then_id` will call the `order_on_body_then_id` scope where the `body_direction` is `:desc`, and the `id_direction` is `:asc`. Note: in this example the user has no control over id_direction. To demonstrate:
153
+ Passing `?sort=order_by_body_then_id` will call the `order_on_body_then_id` scope where the `body_direction` this time is `:asc`, but the `id_direction` remains `:asc`.
154
+
155
+ Scopes that accept no arguments are currently supported, but you should note that the user has no say in which direction it will sort on.
156
+
157
+ `scope_params` can also accept symbols that are keys in the `params` hash. The value will be fetched and passed on as an argument to the scope.
158
+
159
+
160
+ ## Consumer Usage
161
+
162
+ Filter:
163
+ `?filters[<field_name>]=<value>`
164
+
165
+ Filters are translated to Active Record `where`s and are chained together. The order they are applied is not guarenteed.
166
+
167
+ Sort:
168
+ `?sort=-published_at,position`
169
+
170
+ Sort is translated to Active Record `order` The sorts are applied in the order they are passed by the client.
171
+ the `-` symbol means to sort in `desc` order. By default, keys are sorted in `asc` order.
172
+
173
+ ## Installation
174
+ Add this line to your application's Gemfile:
175
+
176
+ ```ruby
177
+ gem 'brita'
178
+ ```
179
+
180
+ And then execute:
181
+ ```bash
182
+ $ bundle
183
+ ```
184
+
185
+ Or install it yourself as:
186
+ ```bash
187
+ $ gem install brita
188
+ ```
189
+
190
+ ## Contributing
191
+ Contribution directions go here.
192
+
193
+ ## License
194
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,37 @@
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 "bundler/gem_tasks"
8
+
9
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
10
+ load "rails/tasks/engine.rake"
11
+
12
+ require "rdoc/task"
13
+
14
+ RDoc::Task.new(:rdoc) do |rdoc|
15
+ rdoc.rdoc_dir = "rdoc"
16
+ rdoc.title = "Brita"
17
+ rdoc.options << "--line-numbers"
18
+ rdoc.rdoc_files.include("README.md")
19
+ rdoc.rdoc_files.include("lib/**/*.rb")
20
+ end
21
+
22
+ require "rake/testtask"
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << "lib"
26
+ t.libs << "test"
27
+ t.pattern = "test/**/*_test.rb"
28
+ t.verbose = false
29
+ end
30
+
31
+ require "rubocop/rake_task"
32
+
33
+ RuboCop::RakeTask.new(:rubocop) do |t|
34
+ t.options = ["--display-cop-names"]
35
+ end
36
+
37
+ task default: [:rubocop, :test]
@@ -0,0 +1,78 @@
1
+ require "brita/filter"
2
+ require "brita/filter_validator"
3
+ require "brita/filtrator"
4
+ require "brita/sort"
5
+ require "brita/subset_comparator"
6
+ require "brita/type_validator"
7
+ require "brita/parameter"
8
+ require "brita/scope_handler"
9
+ require "brita/where_handler"
10
+ require "brita/validators/valid_int_validator"
11
+
12
+ module Brita
13
+ extend ActiveSupport::Concern
14
+
15
+ def filtrate(collection)
16
+ Filtrator.filter(collection, params, filters)
17
+ end
18
+
19
+ def filter_params
20
+ params.fetch(:filters, {})
21
+ end
22
+
23
+ def sort_params
24
+ params.fetch(:sort, "").split(",") if filters.any? { |filter| filter.is_a?(Sort) }
25
+ end
26
+
27
+ def filters_valid?
28
+ filter_validator.valid?
29
+ end
30
+
31
+ def filter_errors
32
+ filter_validator.errors.messages
33
+ end
34
+
35
+ private
36
+
37
+ def filter_validator
38
+ @_filter_validator ||= FilterValidator.build(
39
+ filters: filters,
40
+ sort_fields: self.class.sort_fields,
41
+ filter_params: filter_params,
42
+ sort_params: sort_params,
43
+ )
44
+ end
45
+
46
+ def filters
47
+ self.class.filters
48
+ end
49
+
50
+ def sorts_exist?
51
+ filters.any? { |filter| filter.is_a?(Sort) }
52
+ end
53
+
54
+ class_methods do
55
+ def filter_on(parameter, type:, internal_name: parameter, default: nil, validate: nil, scope_params: [])
56
+ filters << Filter.new(parameter, type, internal_name, default, validate, scope_params)
57
+ end
58
+
59
+ def filters
60
+ @_filters ||= []
61
+ end
62
+
63
+ # TODO: this is only used in tests, can I kill it?
64
+ def reset_filters
65
+ @_filters = []
66
+ end
67
+
68
+ def sort_fields
69
+ @_sort_fields ||= []
70
+ end
71
+
72
+ def sort_on(parameter, type:, internal_name: parameter, scope_params: [])
73
+ filters << Sort.new(parameter, type, internal_name, scope_params)
74
+ sort_fields << parameter.to_s
75
+ sort_fields << "-#{parameter}"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,94 @@
1
+ module Brita
2
+ # Filter describes the way a parameter maps to a database column
3
+ # and the type information helpful for validating input.
4
+ class Filter
5
+ attr_reader :parameter, :default, :custom_validate, :scope_params
6
+
7
+ def initialize(param, type, internal_name, default, custom_validate = nil, scope_params = [])
8
+ @parameter = Parameter.new(param, type, internal_name)
9
+ @default = default
10
+ @custom_validate = custom_validate
11
+ @scope_params = scope_params
12
+ raise ArgumentError, "scope_params must be an array of symbols" unless valid_scope_params?(scope_params)
13
+ raise "unknown filter type: #{type}" unless type_validator.valid_type?
14
+ end
15
+
16
+ def validation(_)
17
+ type_validator.validate
18
+ end
19
+
20
+ # rubocop:disable Lint/UnusedMethodArgument
21
+ def apply!(collection, value:, active_sorts_hash:, params: {})
22
+ if not_processable?(value)
23
+ collection
24
+ elsif should_apply_default?(value)
25
+ default.call(collection)
26
+ else
27
+ handler.call(collection, parameterize(value), params, scope_params)
28
+ end
29
+ end
30
+ # rubocop:enable Lint/UnusedMethodArgument
31
+
32
+ def always_active?
33
+ false
34
+ end
35
+
36
+ def validation_field
37
+ parameter.param
38
+ end
39
+
40
+ def type_validator
41
+ @type_validator ||= Brita::TypeValidator.new(param, type)
42
+ end
43
+
44
+ def type
45
+ parameter.type
46
+ end
47
+
48
+ def param
49
+ parameter.param
50
+ end
51
+
52
+ private
53
+
54
+ def not_processable?(value)
55
+ value.nil? && default.nil?
56
+ end
57
+
58
+ def should_apply_default?(value)
59
+ value.nil? && !default.nil?
60
+ end
61
+
62
+ def mapped_scope_params(params)
63
+ scope_params.each_with_object({}) do |scope_param, hash|
64
+ hash[scope_param] = params.fetch(scope_param)
65
+ end
66
+ end
67
+
68
+ def parameterize(value)
69
+ if supports_ranges? && value.to_s.include?("...")
70
+ Range.new(*value.split("..."))
71
+ elsif type == :boolean
72
+ if Rails.version.starts_with?("5")
73
+ ActiveRecord::Type::Boolean.new.cast(value)
74
+ else
75
+ ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
76
+ end
77
+ else
78
+ value
79
+ end
80
+ end
81
+
82
+ def valid_scope_params?(scope_params)
83
+ scope_params.is_a?(Array) && scope_params.all? { |symbol| symbol.is_a?(Symbol) }
84
+ end
85
+
86
+ def handler
87
+ parameter.handler
88
+ end
89
+
90
+ def supports_ranges?
91
+ parameter.supports_ranges?
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,67 @@
1
+ # Here be dragons:
2
+ # there are two forms of metaprogramming in this file
3
+ # instance variables are being dynamically set based on the param name
4
+ # and we are class evaling `validates` to create dynamic validations
5
+ # based on the filters being validated.
6
+ module Brita
7
+ class FilterValidator
8
+ include ActiveModel::Validations
9
+
10
+ def self.build(filters:, sort_fields:, filter_params:, sort_params:)
11
+ unique_validations_filters = filters.uniq(&:validation_field)
12
+
13
+ klass = Class.new(self) do
14
+ def self.model_name
15
+ ActiveModel::Name.new(self, nil, "temp")
16
+ end
17
+
18
+ attr_accessor(*unique_validations_filters.map(&:validation_field))
19
+
20
+ unique_validations_filters.each do |filter|
21
+ if has_custom_validation?(filter, filter_params)
22
+ validate filter.custom_validate
23
+ elsif has_validation?(filter, filter_params, sort_fields)
24
+ validates filter.validation_field.to_sym, filter.validation(sort_fields)
25
+ end
26
+ end
27
+ end
28
+
29
+ klass.new(filters, filter_params: filter_params, sort_params: sort_params)
30
+ end
31
+
32
+ def self.has_custom_validation?(filter, filter_params)
33
+ filter_params[filter.validation_field] && filter.custom_validate
34
+ end
35
+
36
+ def self.has_validation?(filter, filter_params, sort_fields)
37
+ (filter_params[filter.validation_field] && filter.validation(sort_fields)) || filter.validation_field == :sort
38
+ end
39
+
40
+ def initialize(filters, filter_params:, sort_params:)
41
+ @filter_params = filter_params
42
+ @sort_params = sort_params
43
+
44
+ filters.each do |filter|
45
+ instance_variable_set("@#{filter.validation_field}", to_type(filter))
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader(:filter_params, :sort_params)
52
+
53
+ def to_type(filter)
54
+ if filter.type == :boolean
55
+ if Rails.version.starts_with?("5")
56
+ ActiveRecord::Type::Boolean.new.cast(filter_params[filter.param])
57
+ else
58
+ ActiveRecord::Type::Boolean.new.type_cast_from_user(filter_params[filter.param])
59
+ end
60
+ elsif filter.validation_field == :sort
61
+ sort_params
62
+ else
63
+ filter_params[filter.param]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,53 @@
1
+ module Brita
2
+ # Filtrator takes a collection, params and a set of filters
3
+ # and applies them to create a new active record collection
4
+ # with those filters applied.
5
+ class Filtrator
6
+ attr_reader :collection, :params, :filters, :sort
7
+
8
+ def self.filter(collection, params, filters, sort = [])
9
+ new(collection, params, sort, filters).filter
10
+ end
11
+
12
+ def initialize(collection, params, _sort, filters = [])
13
+ @collection = collection
14
+ @params = params
15
+ @filters = filters
16
+ @sort = params.fetch(:sort, "").split(",") if filters.any? { |filter| filter.is_a?(Sort) }
17
+ end
18
+
19
+ def filter
20
+ active_filters.reduce(collection) do |col, filter|
21
+ apply(col, filter)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def apply(collection, filter)
28
+ filter.apply!(collection, value: filter_params[filter.param], active_sorts_hash: active_sorts_hash, params: params)
29
+ end
30
+
31
+ def filter_params
32
+ params.fetch(:filters, {})
33
+ end
34
+
35
+ def active_sorts_hash
36
+ active_sorts_hash = {}
37
+ Array(sort).each do |s|
38
+ if s.starts_with?("-")
39
+ active_sorts_hash[s[1..-1].to_sym] = :desc
40
+ else
41
+ active_sorts_hash[s.to_sym] = :asc
42
+ end
43
+ end
44
+ active_sorts_hash
45
+ end
46
+
47
+ def active_filters
48
+ filters.select do |filter|
49
+ filter_params[filter.param].present? || filter.default || filter.always_active?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ module Brita
2
+ # Value Object that wraps some handling of filter params
3
+ class Parameter
4
+ attr_reader :param, :type, :internal_name
5
+
6
+ def initialize(param, type, internal_name = param)
7
+ @param = param
8
+ @type = type
9
+ @internal_name = internal_name
10
+ end
11
+
12
+ def supports_ranges?
13
+ ![:string, :text, :scope].include?(type)
14
+ end
15
+
16
+ def handler
17
+ if type == :scope
18
+ ScopeHandler.new(self)
19
+ else
20
+ WhereHandler.new(self)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ module Brita
2
+ class ScopeHandler
3
+ def initialize(param)
4
+ @param = param
5
+ end
6
+
7
+ def call(collection, value, params, scope_params)
8
+ collection.public_send(@param.internal_name, *scope_parameters(value, params, scope_params))
9
+ end
10
+
11
+ def scope_parameters(value, params, scope_params)
12
+ if scope_params.empty?
13
+ [value]
14
+ else
15
+ [value, mapped_scope_params(params, scope_params)]
16
+ end
17
+ end
18
+
19
+ def mapped_scope_params(params, scope_params)
20
+ scope_params.each_with_object({}) do |scope_param, hash|
21
+ hash[scope_param] = params.fetch(scope_param)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,106 @@
1
+ module Brita
2
+ # Sort provides the same interface as a filter,
3
+ # but instead of applying a `where` to the collection
4
+ # it applies an `order`.
5
+ class Sort
6
+ attr_reader :parameter, :scope_params
7
+
8
+ WHITELIST_TYPES = [:int,
9
+ :decimal,
10
+ :string,
11
+ :text,
12
+ :date,
13
+ :time,
14
+ :datetime,
15
+ :scope].freeze
16
+
17
+ def initialize(param, type, internal_name = param, scope_params = [])
18
+ raise "unknown filter type: #{type}" unless WHITELIST_TYPES.include?(type)
19
+ raise "scope params must be an array" unless scope_params.is_a?(Array)
20
+ @parameter = Parameter.new(param, type, internal_name)
21
+ @scope_params = scope_params
22
+ end
23
+
24
+ def default
25
+ # TODO: we can support defaults here later
26
+ false
27
+ end
28
+
29
+ # rubocop:disable Lint/UnusedMethodArgument
30
+ # rubocop:disable Metrics/PerceivedComplexity
31
+ def apply!(collection, value:, active_sorts_hash:, params: {})
32
+ if type == :scope
33
+ if active_sorts_hash.keys.include?(param)
34
+ collection.public_send(internal_name, *mapped_scope_params(active_sorts_hash[param], params))
35
+ elsif default.present?
36
+ # Stubbed because currently Brita::Sort does not respect default
37
+ # default.call(collection)
38
+ collection
39
+ else
40
+ collection
41
+ end
42
+ elsif type == :string || type == :text
43
+ if active_sorts_hash.keys.include?(param)
44
+ collection.order("LOWER(#{internal_name}) #{individual_sort_hash(active_sorts_hash)[internal_name]}")
45
+ else
46
+ collection
47
+ end
48
+ else
49
+ collection.order(individual_sort_hash(active_sorts_hash))
50
+ end
51
+ end
52
+ # rubocop:enable Metrics/PerceivedComplexity
53
+ # rubocop:enable Lint/UnusedMethodArgument
54
+
55
+ def always_active?
56
+ true
57
+ end
58
+
59
+ def validation_field
60
+ :sort
61
+ end
62
+
63
+ def validation(sort)
64
+ {
65
+ inclusion: { in: SubsetComparator.new(sort) },
66
+ allow_nil: true,
67
+ }
68
+ end
69
+
70
+ def type
71
+ parameter.type
72
+ end
73
+
74
+ def param
75
+ parameter.param
76
+ end
77
+
78
+ private
79
+
80
+ def mapped_scope_params(direction, params)
81
+ scope_params.map do |scope_param|
82
+ if scope_param == :direction
83
+ direction
84
+ elsif scope_param.is_a?(Proc)
85
+ scope_param.call
86
+ elsif params.include?(scope_param)
87
+ params[scope_param]
88
+ else
89
+ scope_param
90
+ end
91
+ end
92
+ end
93
+
94
+ def individual_sort_hash(active_sorts_hash)
95
+ if active_sorts_hash.include?(param)
96
+ { internal_name => active_sorts_hash[param] }
97
+ else
98
+ {}
99
+ end
100
+ end
101
+
102
+ def internal_name
103
+ parameter.internal_name
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,11 @@
1
+ module Brita
2
+ class SubsetComparator
3
+ def initialize(array)
4
+ @array = array
5
+ end
6
+
7
+ def include?(other)
8
+ @array.to_set >= other.to_set
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,48 @@
1
+ module Brita
2
+ # TypeValidator validates that the incoming param is of the specified type
3
+ class TypeValidator
4
+ RANGE_PATTERN = { format: { with: /\A.+(?:[^.]\.\.\.[^.]).+\z/, message: "must be a range" } }.freeze
5
+ DECIMAL_PATTERN = { numericality: true, allow_nil: true }.freeze
6
+ BOOLEAN_PATTERN = { inclusion: { in: [true, false] }, allow_nil: true }.freeze
7
+
8
+ WHITELIST_TYPES = [:int,
9
+ :decimal,
10
+ :boolean,
11
+ :string,
12
+ :text,
13
+ :date,
14
+ :time,
15
+ :datetime,
16
+ :scope].freeze
17
+
18
+ def initialize(param, type)
19
+ @param = param
20
+ @type = type
21
+ end
22
+
23
+ attr_reader :param, :type
24
+
25
+ def validate
26
+ case type
27
+ when :datetime, :date, :time
28
+ RANGE_PATTERN
29
+ when :int
30
+ valid_int?
31
+ when :decimal
32
+ DECIMAL_PATTERN
33
+ when :boolean
34
+ BOOLEAN_PATTERN
35
+ end
36
+ end
37
+
38
+ def valid_type?
39
+ WHITELIST_TYPES.include?(type)
40
+ end
41
+
42
+ private
43
+
44
+ def valid_int?
45
+ { valid_int: true }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ class ValidIntValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ record.errors.add attribute, (options[:message] || "must be integer, array of integers, or range") unless
4
+ valid_int?(value)
5
+ end
6
+
7
+ private
8
+
9
+ def valid_int?(value)
10
+ integer_array?(value) || integer_or_range?(value)
11
+ end
12
+
13
+ def integer_array?(value)
14
+ value.is_a?(Array) && value.any? && value.all? { |v| integer_or_range?(v) }
15
+ end
16
+
17
+ def integer_or_range?(value)
18
+ !!(/\A\d+(...\d+)?\z/ =~ value.to_s)
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Brita
2
+ VERSION = "0.9.3".freeze
3
+ end
@@ -0,0 +1,11 @@
1
+ module Brita
2
+ class WhereHandler
3
+ def initialize(param)
4
+ @param = param
5
+ end
6
+
7
+ def call(collection, value, _params, _scope_params)
8
+ collection.where(@param.internal_name => value)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :brita do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brita
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.3
5
+ platform: ruby
6
+ authors:
7
+ - Procore Technologies
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-26 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: 4.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
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: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
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: rubocop
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: sqlite3
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
+ description: Easily write arbitrary filters
84
+ email:
85
+ - dev@procore.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - MIT-LICENSE
91
+ - README.md
92
+ - Rakefile
93
+ - lib/brita.rb
94
+ - lib/brita/filter.rb
95
+ - lib/brita/filter_validator.rb
96
+ - lib/brita/filtrator.rb
97
+ - lib/brita/parameter.rb
98
+ - lib/brita/scope_handler.rb
99
+ - lib/brita/sort.rb
100
+ - lib/brita/subset_comparator.rb
101
+ - lib/brita/type_validator.rb
102
+ - lib/brita/validators/valid_int_validator.rb
103
+ - lib/brita/version.rb
104
+ - lib/brita/where_handler.rb
105
+ - lib/tasks/filterable_tasks.rake
106
+ homepage: https://github.com/procore/brita
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: 2.3.0
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.6.13
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Summary of Brita.
130
+ test_files: []