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.
@@ -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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.4
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in doure.gemspec
6
+ gemspec
@@ -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.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,8 @@
1
+ require "doure/version"
2
+
3
+ require 'doure/filter'
4
+ require 'doure/filterable'
5
+
6
+ module Doure
7
+ # Your code goes here...
8
+ end
@@ -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
@@ -0,0 +1,12 @@
1
+ module Doure
2
+ module Filterable
3
+ def filter_class(klass)
4
+ @filter_class = klass
5
+ end
6
+
7
+ def filter(params = {})
8
+ @filter_class or raise "No filter model specified"
9
+ @filter_class.new(self).apply(params)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Doure
2
+ VERSION = "0.1.0"
3
+ 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: []