doure 0.1.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/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/README.md +143 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doure.gemspec +28 -0
- data/lib/doure.rb +8 -0
- data/lib/doure/filter.rb +73 -0
- data/lib/doure/filterable.rb +12 -0
- data/lib/doure/version.rb +3 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 699a3e3244c2fe33d075967a17478c02b176e9b8
|
4
|
+
data.tar.gz: 837ef221bb0e702fac32a3a7a6c6429b561a1da9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 36b4859bbe22a9baf42ad38da0c49dd49c8e52309d880886fb5e985d69886cee9f68ed8e0e2864cad5208786cd8690a85345ee494b17c6e25bf0fb6fb1ef6eeb
|
7
|
+
data.tar.gz: 788e22ff0aa88d28892f3aa3cce3c6b0651f685a88e02dd43f87ff9d3b7605b22a97f83a13558fa5010cb06b53466902ba9b8f3371917aed018a34be54668e3d
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# Doure
|
2
|
+
|
3
|
+
Doure is a minimal abstraction to write parameterized filters for ActiveRecord models. It allows you to write named filters that accept one parameter, and use those later with `filter`, ex:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class PostFilter < Doure::Filter
|
7
|
+
cont_filter(:title)
|
8
|
+
filter(:author_role_in) { |s, v| s.joins(:author).where(authors: { role: v }) }
|
9
|
+
end
|
10
|
+
|
11
|
+
Post.filter(title_cont: "Dev", author_role_in: ["editor", "admin"])
|
12
|
+
```
|
13
|
+
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'doure'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install doure
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
Given you have an ActiveRecord model, you need to extend the filterable module and define which class you will use to declare the filters, for example:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
# app/models/post.rb
|
37
|
+
|
38
|
+
class Post < ApplicationRecord
|
39
|
+
extend Doure::Filterable
|
40
|
+
|
41
|
+
filter_class PostFilter
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
Then declare the filters in that class, inheriting from `Doure::Filter`:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# app/filters/post_filter.rb
|
49
|
+
|
50
|
+
class PostFilter < Doure::Filter
|
51
|
+
cont_filter(:title)
|
52
|
+
filter(:author_role_in) { |s, v| s.joins(:author).where(authors: { role: v }) }
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### Declaring filters
|
57
|
+
|
58
|
+
Each filter has a name and receive a parameter, which you use to implement the desired clause.
|
59
|
+
|
60
|
+
The most basic method to declare filters is `filter(name, &block)`. The block receives an AR relation and the value, and must return another AR relation.
|
61
|
+
|
62
|
+
You can also use the additional argument `:as` to `filter` (or any of the other predefined filters) in order to apply an automatic casting to the value passed from `Model#filter(hash)`. Example:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class PostFilter < Doure::Filter
|
66
|
+
filter(:is_visible, as: :boolean) { |s, v|
|
67
|
+
# 'v' is a boolean here even if used as `Post.filter(is_visible: 'true')`
|
68
|
+
s.where(active: v)
|
69
|
+
}
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
The supported type castings are:
|
74
|
+
|
75
|
+
- `:boolean`: Will be casted using the default Rails semantics around booleans coming from forms, so that '1', 'true', 'T', etc will be `true`. See: https://github.com/rails/rails/blob/47eadb68bfcae1641b019e07e051aa39420685fb/activemodel/lib/active_model/type/boolean.rb#L17
|
76
|
+
|
77
|
+
- `:date`: Will be casted using `Date.parse`
|
78
|
+
|
79
|
+
- `:datetime`: Will be casted using `Time.parse`
|
80
|
+
|
81
|
+
If you want to modify or extend the supported type castings, you can always define the `cast_value(type, value)` method on the filter class. Ex:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class PostFilter < Doure::Filter
|
85
|
+
eq_filter(:publish_date, as: :special_date_format)
|
86
|
+
|
87
|
+
def cast_value(type, value)
|
88
|
+
case type
|
89
|
+
when :special_date_format
|
90
|
+
Date.strptime(value, "formatting string")
|
91
|
+
else
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
|
99
|
+
### Predefined filters
|
100
|
+
|
101
|
+
Some of the most commonly used filters are already provided. The name of the resulting filter is always <filter_name>_<prefix>, for example "title_cont" for a filter like "cont_filter(:title)". In particular the provided filters are:
|
102
|
+
|
103
|
+
- `cont_filter(name)`: Implements `ILIKE '%#{value}%'`. Ex: `Post.filter(title_cont: 'dev')`
|
104
|
+
- `eq_filter(name)`: Implements equality. Ex: `Post.filter(id_eq: 12)`
|
105
|
+
- `not_eq_filter(name)`: Non-equality. Ex: `Post.filter(id_not_eq: 12)`
|
106
|
+
- `present_filter(name)`: This is a boolean filter by default. Implements equality / non-equality against NULL. Ex: `Post.filter(slug_present: false)`
|
107
|
+
- Numerical comparators, `gt_filter, lt_filter, gteq_filter, lteq_filter`: Implements numerical comparators, the passed value is left as-is. Ex: `Post.filter(views_count_gt: 10)`
|
108
|
+
|
109
|
+
|
110
|
+
### Using Model#filter
|
111
|
+
|
112
|
+
The `#filter` method is chainable with other scopes, for example:
|
113
|
+
|
114
|
+
`Post.where(category_id: 12).filter(title_cont: "Dev")`
|
115
|
+
|
116
|
+
Or with named scopes you have declared in the model:
|
117
|
+
|
118
|
+
`Post.not_deleted.visible.filter(title_cont: "Dev", category_id_eq: '88')`
|
119
|
+
|
120
|
+
The expected use case for Doure is to implement search features. Since it's a common scenario to use `Model#filter` passing a hash of values coming from a view form, the hash will usually have most of their keys with nil values or empty strings. In order to not give incorrect results, then, `nil` and the empty string (`""`) values are ignored by default. For example:
|
121
|
+
|
122
|
+
`Post.filter(title_cont: "", is_visible: "f")`
|
123
|
+
|
124
|
+
Will only apply the `is_visible` filter but not the `title_cont`.
|
125
|
+
|
126
|
+
|
127
|
+
### FAQ
|
128
|
+
|
129
|
+
- The equality filters are not working when using nil values.
|
130
|
+
|
131
|
+
Since nil and empty strings are ignored by default, the equality with nil is not a supported scenario. Instead, you can create an specific presence filter for this (`present_filter(:title)`).
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
## Development
|
136
|
+
|
137
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
138
|
+
|
139
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rogercampos/doure.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "doure"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/doure.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "doure/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "doure"
|
8
|
+
spec.version = Doure::VERSION
|
9
|
+
spec.authors = ["Roger Campos"]
|
10
|
+
spec.email = ["roger@rogercampos.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Minimal abstraction to write parameterized filters for ActiveRecord models}
|
13
|
+
spec.description = %q{Minimal abstraction to write parameterized filters for ActiveRecord models}
|
14
|
+
spec.homepage = "https://github.com/rogercampos/doure"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "activerecord", ">= 4.0.0"
|
24
|
+
spec.add_dependency "activesupport", ">= 4.0.0"
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
28
|
+
end
|
data/lib/doure.rb
ADDED
data/lib/doure/filter.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Doure
|
2
|
+
class Filter
|
3
|
+
class_attribute :mapping
|
4
|
+
self.mapping = ActiveSupport::HashWithIndifferentAccess.new
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def inherited(subclass)
|
8
|
+
subclass.mapping = self.mapping.dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def filter(name, opts = {}, &apply)
|
12
|
+
mapping[name] = [opts, apply]
|
13
|
+
end
|
14
|
+
|
15
|
+
def eq_filter(name, opts = {})
|
16
|
+
block = lambda { |s, value| s.where(name => value) }
|
17
|
+
mapping["#{name}_eq"] = [opts, block]
|
18
|
+
end
|
19
|
+
|
20
|
+
def not_eq_filter(name, opts = {})
|
21
|
+
block = lambda { |s, value| s.where.not(name => value) }
|
22
|
+
mapping["#{name}_not_eq"] = [opts, block]
|
23
|
+
end
|
24
|
+
|
25
|
+
def cont_filter(name, opts = {})
|
26
|
+
block = lambda { |s, value| s.where(s.arel_table[name].matches("%#{value}%")) }
|
27
|
+
mapping["#{name}_cont"] = [opts, block]
|
28
|
+
end
|
29
|
+
|
30
|
+
def present_filter(name, opts = {})
|
31
|
+
block = lambda { |s, value| value ? s.where.not(name => nil) : s.where(name => nil) }
|
32
|
+
mapping["#{name}_present"] = [opts.merge(as: :boolean), block]
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(gt lt gteq lteq).each do |comparison|
|
36
|
+
define_method("#{comparison}_filter") do |name, opts = {}|
|
37
|
+
block = lambda { |s, value| s.where(s.arel_table[name].send(comparison, value)) }
|
38
|
+
mapping["#{name}_#{comparison}"] = [opts, block]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(scope)
|
44
|
+
@scope = scope.all # Force AR:Relation from possible AR:Base
|
45
|
+
end
|
46
|
+
|
47
|
+
def apply(params = {})
|
48
|
+
params.each do |key, value|
|
49
|
+
if !value.nil? && value != "" && mapping.key?(key)
|
50
|
+
casted_value = mapping[key][0].key?(:as) ? cast_value(mapping[key][0][:as], value) : value
|
51
|
+
@scope = mapping[key][1].call(@scope, casted_value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
@scope
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def cast_value(type, value)
|
61
|
+
case type
|
62
|
+
when :boolean
|
63
|
+
ActiveRecord::Type::Boolean.new.cast(value)
|
64
|
+
when :date
|
65
|
+
(String === value) ? Date.parse(value) : value
|
66
|
+
when :datetime
|
67
|
+
(String === value) ? Time.parse(value) : value
|
68
|
+
else
|
69
|
+
value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: doure
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Roger Campos
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.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.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.15'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.15'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
description: Minimal abstraction to write parameterized filters for ActiveRecord models
|
84
|
+
email:
|
85
|
+
- roger@rogercampos.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".travis.yml"
|
92
|
+
- Gemfile
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- bin/console
|
96
|
+
- bin/setup
|
97
|
+
- doure.gemspec
|
98
|
+
- lib/doure.rb
|
99
|
+
- lib/doure/filter.rb
|
100
|
+
- lib/doure/filterable.rb
|
101
|
+
- lib/doure/version.rb
|
102
|
+
homepage: https://github.com/rogercampos/doure
|
103
|
+
licenses: []
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.5.2
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: Minimal abstraction to write parameterized filters for ActiveRecord models
|
125
|
+
test_files: []
|