brita 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []