procore-sift 0.12.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +261 -0
- data/Rakefile +37 -0
- data/lib/procore-sift.rb +80 -0
- data/lib/sift/filter.rb +84 -0
- data/lib/sift/filter_validator.rb +67 -0
- data/lib/sift/filtrator.rb +53 -0
- data/lib/sift/parameter.rb +42 -0
- data/lib/sift/scope_handler.rb +25 -0
- data/lib/sift/sort.rb +106 -0
- data/lib/sift/subset_comparator.rb +11 -0
- data/lib/sift/type_validator.rb +48 -0
- data/lib/sift/validators/valid_date_range_validator.rb +20 -0
- data/lib/sift/validators/valid_int_validator.rb +24 -0
- data/lib/sift/value_parser.rb +86 -0
- data/lib/sift/version.rb +3 -0
- data/lib/sift/where_handler.rb +11 -0
- data/lib/tasks/filterable_tasks.rake +4 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: edabe4ead843ee8dd551b70d2408899b88dca98d
|
4
|
+
data.tar.gz: bbc128cae2da284235591505bfb60a4f7fcc3e93
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5b699d7819a4ab9e29cedc03ccc26fa113e5603abff8c7e700421ecf2eca79bdd25b65c726898ef5e9018b57ca68e1ca4dfb10b7d13589dd71854e6b90d4391f
|
7
|
+
data.tar.gz: c5bec1b452aa2bf55192602c0316d367d044c32224b8c77d0db41eb6e2508a4725c325d024711e0e39f66d52a83548ffcab74823d781126555f4bcf45ba098b9
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
# Sift
|
2
|
+
|
3
|
+
[](https://travis-ci.org/procore/sift)
|
4
|
+
|
5
|
+
A tool to build your own filters and sorts with Rails and Active Record!
|
6
|
+
|
7
|
+
## Developer Usage
|
8
|
+
Include Sift in your controllers, and define some filters.
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class PostsController < ApplicationController
|
12
|
+
include Sift
|
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
|
+
Sift 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 sift your param was.
|
37
|
+
|
38
|
+
### Filter Types
|
39
|
+
Every filter must have a type, so that Sift 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 Sift
|
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 Sift
|
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 Sift
|
106
|
+
|
107
|
+
filter_on :post_id, type: :int, internal_name: :id
|
108
|
+
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
### Filter on Ranges
|
113
|
+
Some parameter types support ranges. Ranges are expected to be a string with the bounding values separated by `...`
|
114
|
+
|
115
|
+
For example `?filters[price]=3...50` would return records with a price between 3 and 50.
|
116
|
+
|
117
|
+
The following types support ranges
|
118
|
+
* int
|
119
|
+
* decimal
|
120
|
+
* boolean
|
121
|
+
* date
|
122
|
+
* time
|
123
|
+
* datetime
|
124
|
+
|
125
|
+
### Filter on JSON Array
|
126
|
+
`int` type filters support sending the values as an array in the URL Query parameters. For example `?filters[id]=[1,2]`. This is a way to keep payloads smaller for GET requests. When URI encoded this will become `filters%5Bid%5D=%5B1,2%5D` which is much smaller the standard format of `filters%5Bid%5D%5B%5D=1&&filters%5Bid%5D%5B%5D=2`.
|
127
|
+
|
128
|
+
On the server side, the params will be received as:
|
129
|
+
```ruby
|
130
|
+
# JSON array encoded result
|
131
|
+
"filters"=>{"id"=>"[1,2]"}
|
132
|
+
|
133
|
+
# standard array format
|
134
|
+
"filters"=>{"id"=>["1", "2"]}
|
135
|
+
```
|
136
|
+
|
137
|
+
Note that this feature cannot currently be wrapped in an array and should not be used in combination with sending array parameters individually.
|
138
|
+
* `?filters[id][]=[1,2]` => invalid
|
139
|
+
* `?filters[id][]=[1,2]&filters[id][]=3` => invalid
|
140
|
+
* `?filters[id]=[1,2]&filters[id]=3` => valid but only 3 is passed through to the server
|
141
|
+
* `?filters[id]=[1,2]` => valid
|
142
|
+
|
143
|
+
#### A note on encoding for JSON Array feature
|
144
|
+
JSON arrays contain the reserved characters "`,`" and "`[`" and "`]`". When encoding a JSON array in the URL there are two different ways to handle the encoding. Both ways are supported by Rails.
|
145
|
+
For example, lets look at the following filter with a JSON array `?filters[id]=[1,2]`:
|
146
|
+
* `?filters%5Bid%5D=%5B1,2%5D`
|
147
|
+
* `?filters%5Bid%5D%3D%5B1%2C2%5D`
|
148
|
+
|
149
|
+
In both cases Rails will correctly decode to the expected result of
|
150
|
+
```ruby
|
151
|
+
{ "filters" => { "id" => "[1,2]" } }
|
152
|
+
```
|
153
|
+
|
154
|
+
### Sort Types
|
155
|
+
Every sort must have a type, so that Sift knows what to do with it. The current valid sort types are:
|
156
|
+
* int - Sort on an integer column
|
157
|
+
* decimal - Sort on a decimal column
|
158
|
+
* string - Sort on a string column
|
159
|
+
* text - Sort on a text column
|
160
|
+
* date - Sort on a date column
|
161
|
+
* time - Sort on a time column
|
162
|
+
* datetime - Sort on a datetime column
|
163
|
+
* scope - Sort on an ActiveRecord scope
|
164
|
+
|
165
|
+
### Sort on Scopes
|
166
|
+
Just as your sort values are used to scope queries on a column, values you
|
167
|
+
pass to a scope sort will be used as arguments to that scope. For example:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
class Post < ActiveRecord::Base
|
171
|
+
scope :order_on_body_no_params, -> { order(body: :asc) }
|
172
|
+
scope :order_on_body, ->(direction) { order(body: direction) }
|
173
|
+
scope :order_on_body_then_id, ->(body_direction, id_direction) { order(body: body_direction).order(id: id_direction) }
|
174
|
+
end
|
175
|
+
|
176
|
+
class PostsController < ApplicationController
|
177
|
+
include Sift
|
178
|
+
|
179
|
+
sort_on :order_by_body_ascending, internal_name: :order_on_body_no_params, type: :scope
|
180
|
+
sort_on :order_by_body, internal_name: :order_on_body, type: :scope, scope_params: [:direction]
|
181
|
+
sort_on :order_by_body_then_id, internal_name: :order_on_body_then_id, type: :scope, scope_params: [:direction, :asc]
|
182
|
+
|
183
|
+
|
184
|
+
def index
|
185
|
+
render json: filtrate(Post.all)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
`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`)
|
191
|
+
|
192
|
+
Passing `?sort=-order_by_body` will call the `order_on_body` scope with
|
193
|
+
`:desc` as the argument. The direction is the only argument that the consumer has control over.
|
194
|
+
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:
|
195
|
+
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`.
|
196
|
+
|
197
|
+
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.
|
198
|
+
|
199
|
+
`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.
|
200
|
+
|
201
|
+
|
202
|
+
## Consumer Usage
|
203
|
+
|
204
|
+
Filter:
|
205
|
+
`?filters[<field_name>]=<value>`
|
206
|
+
|
207
|
+
Filters are translated to Active Record `where`s and are chained together. The order they are applied is not guarenteed.
|
208
|
+
|
209
|
+
Sort:
|
210
|
+
`?sort=-published_at,position`
|
211
|
+
|
212
|
+
Sort is translated to Active Record `order` The sorts are applied in the order they are passed by the client.
|
213
|
+
the `-` symbol means to sort in `desc` order. By default, keys are sorted in `asc` order.
|
214
|
+
|
215
|
+
## Installation
|
216
|
+
Add this line to your application's Gemfile:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
gem 'procore-sift'
|
220
|
+
```
|
221
|
+
|
222
|
+
And then execute:
|
223
|
+
```bash
|
224
|
+
$ bundle
|
225
|
+
```
|
226
|
+
|
227
|
+
Or install it yourself as:
|
228
|
+
```bash
|
229
|
+
$ gem install procore-sift
|
230
|
+
```
|
231
|
+
|
232
|
+
## Without Rails
|
233
|
+
|
234
|
+
We have some future plans to remove the rails dependency so that other frameworks such as Sinatra could leverage this gem.
|
235
|
+
|
236
|
+
## Contributing
|
237
|
+
|
238
|
+
Running tests:
|
239
|
+
```bash
|
240
|
+
$ bundle exec rake test
|
241
|
+
```
|
242
|
+
|
243
|
+
## License
|
244
|
+
|
245
|
+
The gem is available as open source under the terms of the [MIT
|
246
|
+
License](http://opensource.org/licenses/MIT).
|
247
|
+
|
248
|
+
## About Procore
|
249
|
+
|
250
|
+
<img
|
251
|
+
src="https://www.procore.com/images/procore_logo.png"
|
252
|
+
alt="Procore Logo"
|
253
|
+
width="250px"
|
254
|
+
/>
|
255
|
+
|
256
|
+
The Procore Gem is maintained by Procore Technologies.
|
257
|
+
|
258
|
+
Procore - building the software that builds the world.
|
259
|
+
|
260
|
+
Learn more about the #1 most widely used construction management software at
|
261
|
+
[procore.com](https://www.procore.com/)
|
data/Rakefile
ADDED
@@ -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", __dir__)
|
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 = "Sift"
|
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]
|
data/lib/procore-sift.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "sift/filter"
|
2
|
+
require "sift/filter_validator"
|
3
|
+
require "sift/filtrator"
|
4
|
+
require "sift/sort"
|
5
|
+
require "sift/subset_comparator"
|
6
|
+
require "sift/type_validator"
|
7
|
+
require "sift/parameter"
|
8
|
+
require "sift/value_parser"
|
9
|
+
require "sift/scope_handler"
|
10
|
+
require "sift/where_handler"
|
11
|
+
require "sift/validators/valid_int_validator"
|
12
|
+
require "sift/validators/valid_date_range_validator"
|
13
|
+
|
14
|
+
module Sift
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
|
17
|
+
def filtrate(collection)
|
18
|
+
Filtrator.filter(collection, params, filters)
|
19
|
+
end
|
20
|
+
|
21
|
+
def filter_params
|
22
|
+
params.fetch(:filters, {})
|
23
|
+
end
|
24
|
+
|
25
|
+
def sort_params
|
26
|
+
params.fetch(:sort, "").split(",") if filters.any? { |filter| filter.is_a?(Sort) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def filters_valid?
|
30
|
+
filter_validator.valid?
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter_errors
|
34
|
+
filter_validator.errors.messages
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def filter_validator
|
40
|
+
@_filter_validator ||= FilterValidator.build(
|
41
|
+
filters: filters,
|
42
|
+
sort_fields: self.class.sort_fields,
|
43
|
+
filter_params: filter_params,
|
44
|
+
sort_params: sort_params,
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def filters
|
49
|
+
self.class.filters
|
50
|
+
end
|
51
|
+
|
52
|
+
def sorts_exist?
|
53
|
+
filters.any? { |filter| filter.is_a?(Sort) }
|
54
|
+
end
|
55
|
+
|
56
|
+
class_methods do
|
57
|
+
def filter_on(parameter, type:, internal_name: parameter, default: nil, validate: nil, scope_params: [])
|
58
|
+
filters << Filter.new(parameter, type, internal_name, default, validate, scope_params)
|
59
|
+
end
|
60
|
+
|
61
|
+
def filters
|
62
|
+
@_filters ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
# TODO: this is only used in tests, can I kill it?
|
66
|
+
def reset_filters
|
67
|
+
@_filters = []
|
68
|
+
end
|
69
|
+
|
70
|
+
def sort_fields
|
71
|
+
@_sort_fields ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
def sort_on(parameter, type:, internal_name: parameter, scope_params: [])
|
75
|
+
filters << Sort.new(parameter, type, internal_name, scope_params)
|
76
|
+
sort_fields << parameter.to_s
|
77
|
+
sort_fields << "-#{parameter}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/sift/filter.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Sift
|
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(_sort)
|
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 ||= Sift::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 parameterize(value)
|
55
|
+
ValueParser.new(value: value, type: parameter.type, options: parameter.parse_options).parse
|
56
|
+
end
|
57
|
+
|
58
|
+
def not_processable?(value)
|
59
|
+
value.nil? && default.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
def should_apply_default?(value)
|
63
|
+
value.nil? && !default.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
def mapped_scope_params(params)
|
67
|
+
scope_params.each_with_object({}) do |scope_param, hash|
|
68
|
+
hash[scope_param] = params.fetch(scope_param)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_scope_params?(scope_params)
|
73
|
+
scope_params.is_a?(Array) && scope_params.all? { |symbol| symbol.is_a?(Symbol) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def handler
|
77
|
+
parameter.handler
|
78
|
+
end
|
79
|
+
|
80
|
+
def supports_ranges?
|
81
|
+
parameter.supports_ranges?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
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 Sift
|
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 Sift
|
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,42 @@
|
|
1
|
+
module Sift
|
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 parse_options
|
13
|
+
{
|
14
|
+
supports_boolean: supports_boolean?,
|
15
|
+
supports_ranges: supports_ranges?,
|
16
|
+
supports_json: supports_json?
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def handler
|
21
|
+
if type == :scope
|
22
|
+
ScopeHandler.new(self)
|
23
|
+
else
|
24
|
+
WhereHandler.new(self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def supports_ranges?
|
31
|
+
![:string, :text, :scope].include?(type)
|
32
|
+
end
|
33
|
+
|
34
|
+
def supports_json?
|
35
|
+
type == :int
|
36
|
+
end
|
37
|
+
|
38
|
+
def supports_boolean?
|
39
|
+
type == :boolean
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sift
|
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
|
data/lib/sift/sort.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Sift
|
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 Sift::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,48 @@
|
|
1
|
+
module Sift
|
2
|
+
# TypeValidator validates that the incoming param is of the specified type
|
3
|
+
class TypeValidator
|
4
|
+
DATETIME_RANGE_PATTERN = { format: { with: /\A.+(?:[^.]\.\.\.[^.]).+\z/, message: "must be a range" }, valid_date_range: true }.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
|
+
DATETIME_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 ValidDateRangeValidator < ActiveModel::EachValidator
|
2
|
+
def validate_each(record, attribute, value)
|
3
|
+
record.errors.add attribute, "is invalid" unless valid_date_range?(value)
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def valid_date_range?(date_range)
|
9
|
+
from_date_string, end_date_string = date_range.to_s.split("...")
|
10
|
+
return true unless end_date_string # validated by other validator
|
11
|
+
|
12
|
+
[from_date_string, end_date_string].all? { |date| valid_date?(date) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid_date?(date)
|
16
|
+
!!DateTime.parse(date.to_s)
|
17
|
+
rescue ArgumentError
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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
|
+
if value.is_a?(String)
|
15
|
+
value = Sift::ValueParser.new(value: value).array_from_json
|
16
|
+
end
|
17
|
+
|
18
|
+
value.is_a?(Array) && value.any? && value.all? { |v| integer_or_range?(v) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def integer_or_range?(value)
|
22
|
+
!!(/\A\d+(...\d+)?\z/ =~ value.to_s)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Sift
|
2
|
+
class ValueParser
|
3
|
+
def initialize(value:, type: nil, options: {})
|
4
|
+
@value = value
|
5
|
+
@supports_boolean = options.fetch(:supports_boolean, false)
|
6
|
+
@supports_ranges = options.fetch(:supports_ranges, false)
|
7
|
+
@supports_json = options.fetch(:supports_json, false)
|
8
|
+
@value = normalized_value(value, type)
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse
|
12
|
+
@_result ||=
|
13
|
+
if parse_as_range?
|
14
|
+
range_value
|
15
|
+
elsif parse_as_boolean?
|
16
|
+
boolean_value
|
17
|
+
elsif parse_as_json?
|
18
|
+
array_from_json
|
19
|
+
else
|
20
|
+
value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def array_from_json
|
25
|
+
result = JSON.parse(value)
|
26
|
+
if result.is_a?(Array)
|
27
|
+
result
|
28
|
+
else
|
29
|
+
value
|
30
|
+
end
|
31
|
+
rescue JSON::ParserError
|
32
|
+
value
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :value, :type, :supports_boolean, :supports_json, :supports_ranges
|
38
|
+
|
39
|
+
def parse_as_range?(raw_value=value)
|
40
|
+
supports_ranges && raw_value.to_s.include?("...")
|
41
|
+
end
|
42
|
+
|
43
|
+
def range_value
|
44
|
+
Range.new(*value.split("..."))
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_as_json?
|
48
|
+
supports_json && value.is_a?(String)
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_as_boolean?
|
52
|
+
supports_boolean
|
53
|
+
end
|
54
|
+
|
55
|
+
def boolean_value
|
56
|
+
if Rails.version.starts_with?("5")
|
57
|
+
ActiveRecord::Type::Boolean.new.cast(value)
|
58
|
+
else
|
59
|
+
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def normalized_value(raw_value, type)
|
64
|
+
if type == :datetime && parse_as_range?(raw_value)
|
65
|
+
normalized_date_range(raw_value)
|
66
|
+
else
|
67
|
+
raw_value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def normalized_date_range(raw_value)
|
72
|
+
from_date_string, end_date_string = raw_value.split("...")
|
73
|
+
return unless end_date_string
|
74
|
+
|
75
|
+
parsed_dates = [from_date_string, end_date_string].map do |date_string|
|
76
|
+
begin
|
77
|
+
DateTime.parse(date_string.to_s)
|
78
|
+
rescue StandardError
|
79
|
+
date_string
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
parsed_dates.join("...")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/sift/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: procore-sift
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.12.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Procore Technologies
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-06-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: 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: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
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: rubocop
|
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: sqlite3
|
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
|
+
description: Easily write arbitrary filters
|
98
|
+
email:
|
99
|
+
- dev@procore.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- MIT-LICENSE
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- lib/procore-sift.rb
|
108
|
+
- lib/sift/filter.rb
|
109
|
+
- lib/sift/filter_validator.rb
|
110
|
+
- lib/sift/filtrator.rb
|
111
|
+
- lib/sift/parameter.rb
|
112
|
+
- lib/sift/scope_handler.rb
|
113
|
+
- lib/sift/sort.rb
|
114
|
+
- lib/sift/subset_comparator.rb
|
115
|
+
- lib/sift/type_validator.rb
|
116
|
+
- lib/sift/validators/valid_date_range_validator.rb
|
117
|
+
- lib/sift/validators/valid_int_validator.rb
|
118
|
+
- lib/sift/value_parser.rb
|
119
|
+
- lib/sift/version.rb
|
120
|
+
- lib/sift/where_handler.rb
|
121
|
+
- lib/tasks/filterable_tasks.rake
|
122
|
+
homepage: https://github.com/procore/sift
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: 2.3.0
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 2.6.14
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Summary of Sift.
|
146
|
+
test_files: []
|