scopable 1.1.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f2ceee764d1cb24f319eb1013a1e99af71f2e51
4
- data.tar.gz: 8d79497d0ac281967f13c74c41d0f6b7c7ab7af3
3
+ metadata.gz: cfe1737f95ddf82cad3f7e18880a932269e8c024
4
+ data.tar.gz: 7c72a34e7fc98dc5c18a9203a0f58162eb408d16
5
5
  SHA512:
6
- metadata.gz: cb49a404433b3bd4124fddc3843e0c2e374b9d8f82c1ff856fd70bf34de6d829cfe63a0049d7569485bb9688482ebec2048145a64ff14695a13cc7b7175654a7
7
- data.tar.gz: e38b3c2f659179047f611aa63b74203e50eb2a77580aae139a446589c55e6b4a415c428f5cdff4858e5b12961e301eb730b28c75158aa79343241c4558c6ac8c
6
+ metadata.gz: ce1539106fde92d8387bcebe041dcaea5e4aecb4400bce5c4c236ff44759c12634083098e047aa938c8963a7ba74fafa51c77f9839dfd3b24180b8efa853b2e4
7
+ data.tar.gz: 17c57c0f4a9b4a66d814163f525474690fae38e41d737171450da1bb0d0addc234f1afe84ccc101eb9439a0f3fdf116c2b3929113dfd632c2a5e31aba527d250
data/.codeclimate.yml CHANGED
@@ -10,4 +10,4 @@ ratings:
10
10
  paths:
11
11
  - "lib/**.rb"
12
12
  exclude_paths:
13
- - spec/**/*
13
+ - test/**/*
data/.rubocop.yml CHANGED
@@ -229,6 +229,7 @@ Metrics/CyclomaticComplexity:
229
229
  A complexity metric that is strongly correlated to the number
230
230
  of test cases needed to validate a method.
231
231
  Enabled: true
232
+ Max: 18
232
233
 
233
234
  Metrics/LineLength:
234
235
  Description: 'Limit lines to 80 characters.'
@@ -528,7 +529,7 @@ Style/DefWithParentheses:
528
529
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
529
530
  Enabled: false
530
531
 
531
- Style/DeprecatedHashMethods:
532
+ Style/PreferredHashMethods:
532
533
  Description: 'Checks for use of deprecated Hash methods.'
533
534
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-key'
534
535
  Enabled: false
@@ -1154,13 +1155,3 @@ Style/WordArray:
1154
1155
  Description: 'Use %w or %W for arrays of words.'
1155
1156
  StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w'
1156
1157
  Enabled: false
1157
- Style/Documentation:
1158
- Enabled: no
1159
- Metrics/LineLength:
1160
- Max: 128
1161
- Metrics/MethodLength:
1162
- Max: 36
1163
- Style/Lambda:
1164
- EnforcedStyle: literal
1165
- Style/EmptyCaseCondition:
1166
- Enabled: false
data/.travis.yml CHANGED
@@ -1,4 +1,19 @@
1
- ---
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID=f8045f650e554707d513f10ef3ba1ed386d711797ffaf44a8610d96f7de7b1da
4
+ - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi)
2
5
  language: ruby
3
6
  rvm:
4
- - 2.3.4
7
+ - 2.4.1
8
+ before_script:
9
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
10
+ - chmod +x ./cc-test-reporter
11
+ script:
12
+ - bundle exec rake
13
+ # Preferably you will run test-reporter on branch update events. But
14
+ # if you setup travis to build PR updates only, you don't need to run
15
+ # the line below
16
+ - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
17
+ # In the case where travis is setup to build PR updates only,
18
+ # uncomment the line below
19
+ # - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/Gemfile CHANGED
@@ -1,6 +1,2 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
- group :test do
4
- gem 'simplecov'
5
- gem 'codeclimate-test-reporter', '~> 1.0.0'
6
- end
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright © 2013 Arthur Corenzan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,126 +1,124 @@
1
- [![RubyGems](https://img.shields.io/gem/dt/scopable.svg?style=flat-square)](https://rubygems.org/gems/scopable)
2
- [![Build](https://img.shields.io/travis/corenzan/scopable.svg?style=flat-square)](https://travis-ci.org/corenzan/scopable)
3
- [![Code Climate](https://img.shields.io/codeclimate/github/corenzan/scopable.svg?style=flat-square)](https://codeclimate.com/github/corenzan/scopable)
4
- [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/corenzan/scopable.svg?style=flat-square)](https://codeclimate.com/github/corenzan/scopable/coverage)
1
+ [![RubyGems](https://img.shields.io/gem/dt/scopable.svg?style=flat)](https://rubygems.org/gems/scopable)
2
+ [![Build](https://img.shields.io/travis/corenzan/scopable.svg?style=flat)](https://travis-ci.org/corenzan/scopable)
3
+ [![Code Climate](https://img.shields.io/codeclimate/github/corenzan/scopable.svg?style=flat)](https://codeclimate.com/github/corenzan/scopable)
4
+ [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/corenzan/scopable.svg?style=flat)](https://codeclimate.com/github/corenzan/scopable/coverage)
5
5
 
6
6
  # Scopable
7
7
 
8
- > Apply or skip model scopes based on options and request parameters.
8
+ > Easy parametric query building in Rails.
9
9
 
10
10
  ## Installation
11
11
 
12
- Add this line to your application's Gemfile:
12
+ Simply add it to your Gemfile.
13
13
 
14
14
  ```ruby
15
15
  gem 'scopable'
16
16
  ```
17
17
 
18
- And then execute:
18
+ And update your Gems.
19
19
 
20
20
  ```shell
21
- $ bundle install
21
+ $ bundle update
22
22
  ```
23
23
 
24
- Or install it yourself with:
24
+ Please note that as of version 2.0 **the API has drastically changed**. If you're **already using version 1.x** and don't want to update your application right now, you should stick with it:
25
25
 
26
- ```shell
27
- $ gem install scopable
26
+ ```ruby
27
+ gem 'scopable', '~> 1.0'
28
28
  ```
29
29
 
30
- ## Usage
30
+ ## About
31
31
 
32
- First you need to set scopes in your controller:
32
+ **Scopable** is useful when you need to build one or more queries based on incoming parameters in the request. Very much like [has_scope](https://github.com/plataformatec/has_scope) except it's decoupled from the controller, making it easier to test and much more flexible.
33
33
 
34
- ```ruby
35
- class PostsController < ApplicationController
36
- include Scopable
34
+ ### Example
37
35
 
38
- scope :search, param: :q
36
+ Say you have a model and a controller for your blog. Something like this:
39
37
 
40
- # ...
41
- end
38
+ ```
39
+ └── app
40
+ ├── models
41
+ └── post.rb
42
+ └── controllers
43
+ └── posts_controller.rb
42
44
  ```
43
45
 
44
- Then apply them when querying the model:
46
+ First let's create a new directory named `scopes` along with `models` and `controllers`. There you create a file called `post_scope.rb`, and inside it you define a class that inherits from `Scopable`.
45
47
 
46
48
  ```ruby
47
- class PostsController < ApplicationController
48
- include Scopable
49
-
50
- scope :search, param: :q
49
+ class PostScope < Scopable
50
+ model Post
51
51
 
52
- def index
53
- @posts = scoped(Post, params)
52
+ scope :search do
53
+ where('title LIKE ?', value)
54
54
  end
55
- end
56
- ```
57
-
58
- Now whenever the parameter `q` is present in `params`, the scope `#search` will be called on your model and given the value of `params[:q]` as argument. Otherwise you would have to write something like this:
59
55
 
60
- ```ruby
61
- if params[:q].present?
62
- @posts = Post.search(params[:q])
63
- else
64
- @posts = Post.all
56
+ scope :published_on do
57
+ where(published_at: value.to_time)
65
58
  end
59
+ end
66
60
  ```
67
61
 
68
- What would be fine, except you usually have multiple scopes, that might get combined depending on the presence or absence of parameters to produce the final query. Look how simple it becomes when using Scopable:
62
+ Finally, in `PostsController` you use `PostScope.resolve` to conditionally resolve the scopes based on given parameters.
69
63
 
70
64
  ```ruby
71
- class PostController < ApplicationController
72
- include Scopable
73
-
74
- # Filter by category.
75
- scope :category do |relation, value|
76
- relation.where(category_id: value.to_i)
65
+ class PostsController < ApplicationController
66
+ def index
67
+ @posts = PostScope.resolve(scope_params)
77
68
  end
78
69
 
79
- # Fix N+1.
80
- scope :includes, force: :author
81
-
82
- # Pagination.
83
- scope :page, default: 1
84
-
85
- # Sort by creation date.
86
- scope :order, force: { created_at: :desc }
70
+ private
87
71
 
88
- def index
89
- @posts = scoped(Post, params)
72
+ def scope_params
73
+ params.permit(:search, :published_on).to_h
90
74
  end
91
75
  end
92
76
  ```
93
77
 
94
- Now say a request is made looking like this:
78
+ Now when any combination of the parameters `search` and `published_on` are present, their respective conditions are going to be applied on the relation. i.e. If your request path looks like this:
95
79
 
96
80
  ```
97
- /posts?category=2
81
+ /?search=bananas&published_on=2007-07-19
98
82
  ```
99
83
 
100
- The resulting query would be:
84
+ `@posts` will look like this:
101
85
 
102
- ```ruby
103
- Post.where(category_id: 2).includes(:author).page(1).order(created_at: :desc)
86
+ ```
87
+ Post.where('title LIKE ?', 'bananas').where(published_on: '2007-07-19 00:00:00 +0000')
104
88
  ```
105
89
 
106
- Please note that **order matters**. The scopes will be applied in the same order they are configured.
107
-
108
- Also values like `true/false`, `on/off`, `yes/no` are **cast as boolean**, and when given a boolean value the scope is either called with no arguments or skipped entirely. For instance, if you set a scope like `scope :draft` then request the URL `/posts?draft=yes` it would be like just calling `Post.draft`. But if you request `/posts?draft=no` it does nothing.
109
-
110
- ### Options
90
+ You can also pass some options when you're defining scopes for more advanced use cases. Read on.
111
91
 
112
- No option is required. By default it assumes both scope and parameter have the same name.
92
+ #### Options
113
93
 
114
94
  Key | Description
115
95
  ------------|--------------------------------------------------------------------------------------------------------------
116
96
  `:param` | Name of the parameter that activates the scope.
117
97
  `:default` | Default value for the scope in case the parameter is missing.
118
- `:force` | Force a value to the scope regardless of the request parameters.
98
+ `:value` | Force a value to the scope regardless of the request parameters.
119
99
  `:required` | Calls `#none` on the model if parameter is absent (blank or nil) and there's no default value set.
120
- `:only` | String, Symbol or an Array of those. The scope will **only** be applied to these actions.
121
- `:except` | String, Symbol or an Array of those. The scope will be applied to all actions **except** these.
122
- `&block` | Block will be called in the context of the controller's action and will be given two parameters: the current relation and evaluated value.
100
+ `:if` | ...
101
+ `:unless` | ...
102
+ `&block` | Block will be used to produce the resulting relation with two parameters: the relation at this step and the scope value from params.
103
+
104
+ ## Collaboration
105
+
106
+ If you'd like to contribute to the project, in any form, you're most welcome, but please bear in mind that as it is with any open-source software your suggestions are subject to the discretion of the project maintainers, and what you see as an issue, someone else may see as a feature.
107
+
108
+ We encourage you to:
109
+
110
+ - Open a new issue with suggestions, concerns, or problems you may have.
111
+ - Chip in existing discussions and present your opinion on the subject.
112
+ - Send pull-requests with test covered bug fixes or new features.
113
+ - Send pull-requests with new tests you miss in the suite.
114
+
115
+ Remember to:
116
+
117
+ - Be respectful.
118
+ - Use proper grammar in discussions and in commit messages.
119
+ - Follow the established coding style.
120
+ - Explain why you're making the changes in a pull-request.
123
121
 
124
122
  ## License
125
123
 
126
- See [LICENSE](LICENSE).
124
+ [The MIT License](LICENSE.md) © 2013 Arthur Corenzan
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
- require 'rspec/core/rake_task'
2
- RSpec::Core::RakeTask.new('spec')
3
- task default: :spec
1
+ task :test do
2
+ require_relative 'test/test_helper.rb'
3
+ end
4
+ task default: :test
data/lib/scopable.rb CHANGED
@@ -1,56 +1,50 @@
1
- module Scopable
2
- extend ActiveSupport::Concern
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
3
 
4
- def scopes
5
- self.class.scopes
6
- end
7
-
8
- def scoped(model, params)
9
- scopes.reduce(model) do |relation, scope|
10
- name, options = *scope
11
-
12
- # Controller actions where this scope should be applied.
13
- # Accepts either a literal value or a lambda. nil with disable the option.
14
- only = options[:only]
15
- only = instance_exec(&only) if only.respond_to?(:call)
16
-
17
- # Enfore :only option.
18
- next relation unless only.nil? || Array.wrap(only).map(&:to_s).include?(action_name)
4
+ class Scopable
5
+ attr_reader :model, :scopes
19
6
 
20
- # Controller actions where this scope should be ignored.
21
- # Accepts either a literal value or a lambda. nil with disable the option.
22
- except = options[:except]
23
- except = instance_exec(&except) if except.respond_to?(:call)
24
-
25
- # Enfore :except option.
26
- next relation if except.present? && Array.wrap(except).map(&:to_s).include?(action_name)
7
+ def initialize(model = nil, scopes = nil)
8
+ @model = model || self.class.model
9
+ @scopes = scopes || self.class.scopes
10
+ end
27
11
 
28
- # Name of the request parameters which value will be used in this scope.
29
- # Defaults to the name of the scope.
30
- param = options.fetch(:param, name)
12
+ def delegator(relation, value, params)
13
+ SimpleDelegator.new(relation).tap do |delegator|
14
+ delegator.define_singleton_method(:value) do
15
+ value
16
+ end
17
+ delegator.define_singleton_method(:params) do
18
+ params
19
+ end
20
+ end
21
+ end
31
22
 
32
- # Use the value from the request parameter or fall back to the default.
33
- value = params[param]
23
+ def resolve(params = {})
24
+ params = params.with_indifferent_access
34
25
 
35
- # If parameter is not present use the :default option.
36
- # Accepts either a literal value or a lambda.
37
- value = options[:default] if value.nil?
26
+ scopes.reduce(model) do |relation, scope|
27
+ name, options = *scope
38
28
 
39
- # Forces the scope to use the given value given in the :force option.
40
- # Accepts either a literal value or a lambda.
41
- value = options[:force] if options.key?(:force)
29
+ # Resolve param name.
30
+ param = options[:param] || name
42
31
 
43
- # If either :default or :force options were procs, evaluate them.
44
- value = instance_exec(&value) if value.respond_to?(:call)
32
+ # Resolve a value for the scope.
33
+ value = options[:value] || params[param] || options[:default]
45
34
 
46
- # The :required option makes sure there's a value present, otherwise return an empty scope (Model#none).
47
- required = options[:required]
48
- required = instance_exec(&required) if required.respond_to?(:call)
35
+ # When value is empty treat it as nil.
36
+ value = nil if value.respond_to?(:empty?) && value.empty?
49
37
 
50
- # Enforce the :required option.
51
- break relation.none if required && value.nil?
38
+ # When a nil value was given either skip the scope or bail with #none (if the required options was used).
39
+ if value.nil?
40
+ if options[:required]
41
+ break relation.none
42
+ else
43
+ next relation
44
+ end
45
+ end
52
46
 
53
- # Parses values like 'on/off', 'true/false', and 'yes/no' to an actual boolean value.
47
+ # Cast boolean-like strings.
54
48
  case value.to_s
55
49
  when /\A(false|no|off)\z/
56
50
  value = false
@@ -58,20 +52,20 @@ module Scopable
58
52
  value = true
59
53
  end
60
54
 
61
- # For advanced scopes that require more than a method call on the model.
62
- # When a block is given, it is ran no matter the scope value.
63
- # The proc will be given the model being scoped and the resulting value from the
64
- # options above, and it'll be executed inside the context of the controller's action.
65
- block = options[:block]
55
+ # Enforce 'if' option.
56
+ if options[:if]
57
+ next relation unless delegator(relation, value, params).instance_exec(&options[:if])
58
+ end
66
59
 
67
- if block.nil? && value.blank?
68
- next relation
60
+ # Enforce 'unless' option.
61
+ if options[:unless]
62
+ next relation if delegator(relation, value, params).instance_exec(&options[:unless])
69
63
  end
70
64
 
71
- case
72
- when block.present?
73
- instance_exec(relation, value, &block)
74
- when value == true
65
+ # When a block is present, use that, otherwise call the scope method.
66
+ if options[:block].present?
67
+ delegator(relation, value, params).instance_exec(&options[:block])
68
+ elsif value == true
75
69
  relation.send(name)
76
70
  else
77
71
  relation.send(name, value)
@@ -79,13 +73,19 @@ module Scopable
79
73
  end
80
74
  end
81
75
 
82
- module ClassMethods
83
- def scopes
84
- @scopes ||= {}
85
- end
76
+ def self.resolve(params = {})
77
+ new.resolve(params)
78
+ end
86
79
 
87
- def scope(name, options = {}, &block)
88
- scopes.store name, options.merge(block: block)
89
- end
80
+ def self.model(model = nil)
81
+ @model ||= model
82
+ end
83
+
84
+ def self.scopes
85
+ @scopes ||= {}
86
+ end
87
+
88
+ def self.scope(name, options = {}, &block)
89
+ scopes.store name, options.merge(block: block)
90
90
  end
91
91
  end
@@ -1,3 +1,3 @@
1
- module Scopable
2
- VERSION = "1.1.4"
1
+ class Scopable
2
+ VERSION = '2.0.0'.freeze
3
3
  end
data/scopable.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Scopable::VERSION
9
9
  spec.authors = ['Arthur Corenzan']
10
10
  spec.email = ['arthur@corenzan.com']
11
- spec.summary = %q{Apply or skip model scopes based on options and request parameters.}
12
- spec.description = %q{}
11
+ spec.summary = 'Easy parametric query building in Rails.'
12
+ spec.description = 'Scopable allows you to create objects that produce complex queries based on incoming parameters hash which are testable and easy to understand.'
13
13
  spec.homepage = 'https://github.com/corenzan/scopable'
14
14
  spec.license = 'MIT'
15
15
 
@@ -20,9 +20,10 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.required_ruby_version = '>= 2.2.2'
22
22
 
23
- spec.add_runtime_dependency 'activesupport', '>= 3.2'
23
+ spec.add_runtime_dependency 'activesupport', '~> 5.0'
24
24
 
25
- spec.add_development_dependency 'bundler', '~> 1.7'
26
- spec.add_development_dependency 'rake', '~> 10.0'
27
- spec.add_development_dependency 'rspec', '~> 3.5'
25
+ spec.add_development_dependency 'bundler', '~> 1.15'
26
+ spec.add_development_dependency 'minitest', '~> 5.10'
27
+ spec.add_development_dependency 'rubocop', '~> 0.49'
28
+ spec.add_development_dependency 'simplecov', '~> 0.14'
28
29
  end
@@ -0,0 +1,139 @@
1
+ class ScopableTest < Minitest::Test
2
+ extend ActiveSupport::Testing::Declarative
3
+
4
+ def assert_scope(scopable, scope, value)
5
+ assert_includes(scopable.scopes, scope)
6
+ assert_equal(value, scopable.scopes[scope])
7
+ end
8
+
9
+ def refute_scope(scopable, scope)
10
+ refute_includes(scopable.scopes, scope)
11
+ assert_nil(scopable.scopes[scope])
12
+ end
13
+
14
+ test 'initialize' do
15
+ model = Model.new
16
+ scopable = Scopable.new(model, fuzzy: {}, jumbo: {})
17
+ assert_equal(model, scopable.model)
18
+ assert_includes(scopable.scopes, :fuzzy)
19
+ assert_includes(scopable.scopes, :jumbo)
20
+ end
21
+
22
+ test 'dsl' do
23
+ model = Model.new
24
+ scopable = Class.new(Scopable) do
25
+ model model
26
+ scope :fuzzy
27
+ scope :jumbo
28
+ end
29
+ assert_equal(model, scopable.new.model)
30
+ assert_includes(scopable.new.scopes, :fuzzy)
31
+ assert_includes(scopable.new.scopes, :jumbo)
32
+ end
33
+
34
+ test 'one scope, matching parameter' do
35
+ model = Model.new
36
+ scopable = Scopable.new(model, fuzzy: {})
37
+ params = { fuzzy: 'fuzzy' }
38
+ assert_scope(scopable.resolve(params), :fuzzy, 'fuzzy')
39
+ end
40
+
41
+ test 'multiple scopes, matching parameters' do
42
+ model = Model.new
43
+ scopable = Scopable.new(model, fuzzy: {}, jumbo: {})
44
+ params = { fuzzy: 'fuzzy', jumbo: 'jumbo' }
45
+ assert_scope(scopable.resolve(params), :fuzzy, 'fuzzy')
46
+ assert_scope(scopable.resolve(params), :jumbo, 'jumbo')
47
+ end
48
+
49
+ test 'multiple scopes, missing one parameter' do
50
+ model = Model.new
51
+ scopable = Scopable.new(model, fuzzy: {}, jumbo: {})
52
+ params = { jumbo: 'jumbo' }
53
+ assert_scope(scopable.resolve(params), :jumbo, 'jumbo')
54
+ refute_scope(scopable.resolve(params), :fuzzy)
55
+ end
56
+
57
+ test 'matching param with blank value' do
58
+ model = Model.new
59
+ scopable = Scopable.new(model, fuzzy: {})
60
+ params = { fuzzy: '' }
61
+ refute_scope(scopable.resolve(params), :fuzzy)
62
+ end
63
+
64
+ test 'absent param' do
65
+ model = Model.new
66
+ scopable = Scopable.new(model, fuzzy: {})
67
+ refute_scope(scopable.resolve, :fuzzy)
68
+ end
69
+
70
+ test 'matching param with true-like value' do
71
+ model = Model.new
72
+ scopable = Scopable.new(model, fuzzy: {})
73
+ assert_scope(scopable.resolve(fuzzy: 'on'), :fuzzy, true)
74
+ assert_scope(scopable.resolve(fuzzy: 'yes'), :fuzzy, true)
75
+ assert_scope(scopable.resolve(fuzzy: 'true'), :fuzzy, true)
76
+ end
77
+
78
+ test 'matching param with false-like value' do
79
+ model = Model.new
80
+ scopable = Scopable.new(model, fuzzy: {})
81
+ assert_scope(scopable.resolve(fuzzy: 'no'), :fuzzy, false)
82
+ assert_scope(scopable.resolve(fuzzy: 'off'), :fuzzy, false)
83
+ assert_scope(scopable.resolve(fuzzy: 'false'), :fuzzy, false)
84
+ end
85
+
86
+ test 'param option' do
87
+ model = Model.new
88
+ scopable = Scopable.new(model, fuzzy: { param: :f })
89
+ params = { f: 'fuzzy' }
90
+ assert_scope(scopable.resolve(params), :fuzzy, 'fuzzy')
91
+ end
92
+
93
+ test 'value option' do
94
+ model = Model.new
95
+ scopable = Scopable.new(model, fuzzy: { value: 'fuzzy' })
96
+ assert_scope(scopable.resolve, :fuzzy, 'fuzzy')
97
+ end
98
+
99
+ test 'default option' do
100
+ model = Model.new
101
+ scopable = Scopable.new(model, fuzzy: { default: 'fuzzy' })
102
+ assert_scope(scopable.resolve, :fuzzy, 'fuzzy')
103
+ params = { fuzzy: 'jumbo' }
104
+ assert_scope(scopable.resolve(params), :fuzzy, 'jumbo')
105
+ end
106
+
107
+ test 'required option' do
108
+ model = Model.new
109
+ scopable = Scopable.new(model, fuzzy: { required: true })
110
+ assert_equal(:none, scopable.resolve)
111
+ params = { fuzzy: 'fuzzy' }
112
+ assert_scope(scopable.resolve(params), :fuzzy, 'fuzzy')
113
+ end
114
+
115
+ test 'if option' do
116
+ model = Model.new
117
+ scopable = Scopable.new(model, fuzzy: { if: -> { params[:quack] } })
118
+ params = { fuzzy: 'fuzzy' }
119
+ refute_scope(scopable.resolve(params), :fuzzy)
120
+ params = { fuzzy: 'fuzzy', quack: true }
121
+ assert_scope(scopable.resolve(params), :fuzzy, 'fuzzy')
122
+ end
123
+
124
+ test 'unless option' do
125
+ model = Model.new
126
+ scopable = Scopable.new(model, fuzzy: { unless: -> { params[:quack] } })
127
+ params = { fuzzy: 'fuzzy', quack: true }
128
+ refute_scope(scopable.resolve(params), :fuzzy)
129
+ params = { fuzzy: 'fuzzy' }
130
+ assert_scope(scopable.resolve(params), :fuzzy, 'fuzzy')
131
+ end
132
+
133
+ test 'block option' do
134
+ model = Model.new
135
+ scopable = Scopable.new(model, fuzzy: { block: -> { jumbo(value) } })
136
+ params = { fuzzy: 'fuzzy' }
137
+ assert_scope(scopable.resolve(params), :jumbo, 'fuzzy')
138
+ end
139
+ end
@@ -0,0 +1,34 @@
1
+ require 'simplecov'
2
+
3
+ # Generate coverage report.
4
+ SimpleCov.start
5
+
6
+ require 'minitest/autorun'
7
+ require 'active_support/testing/declarative'
8
+
9
+ # Mock model for testing scope chains.
10
+ class Model
11
+ def scopes
12
+ @scopes ||= {}
13
+ end
14
+
15
+ def fuzzy(value = true)
16
+ scopes.store(:fuzzy, value)
17
+ self
18
+ end
19
+
20
+ def jumbo(value = true)
21
+ scopes.store(:jumbo, value)
22
+ self
23
+ end
24
+
25
+ def none
26
+ :none
27
+ end
28
+ end
29
+
30
+ # Load the Gem.
31
+ require_relative '../lib/scopable'
32
+
33
+ # Run test suite.
34
+ require_relative 'scopable_test'
metadata CHANGED
@@ -1,72 +1,87 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scopable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arthur Corenzan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-04-28 00:00:00.000000000 Z
11
+ date: 2017-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '3.2'
19
+ version: '5.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '3.2'
26
+ version: '5.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.7'
33
+ version: '1.15'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.15'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.10'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.7'
54
+ version: '5.10'
41
55
  - !ruby/object:Gem::Dependency
42
- name: rake
56
+ name: rubocop
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '10.0'
61
+ version: '0.49'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '10.0'
68
+ version: '0.49'
55
69
  - !ruby/object:Gem::Dependency
56
- name: rspec
70
+ name: simplecov
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '3.5'
75
+ version: '0.14'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '3.5'
69
- description: ''
82
+ version: '0.14'
83
+ description: Scopable allows you to create objects that produce complex queries based
84
+ on incoming parameters hash which are testable and easy to understand.
70
85
  email:
71
86
  - arthur@corenzan.com
72
87
  executables: []
@@ -75,20 +90,17 @@ extra_rdoc_files: []
75
90
  files:
76
91
  - ".codeclimate.yml"
77
92
  - ".gitignore"
78
- - ".rspec"
79
93
  - ".rubocop.yml"
80
94
  - ".travis.yml"
81
95
  - Gemfile
82
- - LICENSE
96
+ - LICENSE.md
83
97
  - README.md
84
98
  - Rakefile
85
99
  - lib/scopable.rb
86
100
  - lib/scopable/version.rb
87
101
  - scopable.gemspec
88
- - spec/scopable_spec.rb
89
- - spec/spec_helper.rb
90
- - spec/support/controller.rb
91
- - spec/support/model.rb
102
+ - test/scopable_test.rb
103
+ - test/test_helper.rb
92
104
  homepage: https://github.com/corenzan/scopable
93
105
  licenses:
94
106
  - MIT
@@ -109,12 +121,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
121
  version: '0'
110
122
  requirements: []
111
123
  rubyforge_project:
112
- rubygems_version: 2.6.8
124
+ rubygems_version: 2.6.12
113
125
  signing_key:
114
126
  specification_version: 4
115
- summary: Apply or skip model scopes based on options and request parameters.
127
+ summary: Easy parametric query building in Rails.
116
128
  test_files:
117
- - spec/scopable_spec.rb
118
- - spec/spec_helper.rb
119
- - spec/support/controller.rb
120
- - spec/support/model.rb
129
+ - test/scopable_test.rb
130
+ - test/test_helper.rb
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --require spec_helper
data/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- Copyright (c) 2015-2017 Arthur Corenzan
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -1,302 +0,0 @@
1
- require 'active_support/all'
2
- require_relative '../lib/scopable.rb'
3
- require_relative 'support/controller.rb'
4
- require_relative 'support/model.rb'
5
-
6
- describe Scopable do
7
- it 'creates class variable #scopes' do
8
- expect(Controller).to respond_to(:scopes)
9
- expect(Controller.scopes).to eq({})
10
- end
11
-
12
- it 'adds class method #scope' do
13
- expect(Controller).to respond_to(:scope)
14
- end
15
-
16
- it 'adds instance method #scoped' do
17
- expect(Controller.new).to respond_to(:scoped)
18
- end
19
-
20
- #
21
- # Test single scope, no options.
22
- #
23
- describe 'with one optional scope' do
24
- let :controller do
25
- Class.new(Controller) do
26
- scope :search
27
- end
28
- end
29
-
30
- context 'with the parameter absent' do
31
- subject :action do
32
- controller.new
33
- end
34
-
35
- it 'should skip the scope' do
36
- expect(action.relation.scopes).to be_empty
37
- end
38
- end
39
-
40
- context 'with the parameter present' do
41
- subject :action do
42
- controller.new(nil, search: 'test')
43
- end
44
-
45
- it 'should apply the scope' do
46
- expect(action.relation.scopes).to include(search: 'test')
47
- end
48
- end
49
-
50
- context 'with the parameter present but blank' do
51
- subject :action do
52
- controller.new(nil, search: '')
53
- end
54
-
55
- it 'should skip the scope' do
56
- expect(action.relation.scopes).to be_empty
57
- end
58
- end
59
- end
60
-
61
- #
62
- # Test two optional scopes, with 0, 1, and 2 matching parameters.
63
- #
64
- describe 'with two optional scopes' do
65
- let :controller do
66
- Class.new(Controller) do
67
- scope :color
68
- scope :size
69
- end
70
- end
71
-
72
- context 'without parameters' do
73
- subject :action do
74
- controller.new
75
- end
76
-
77
- it 'should skip the scopes' do
78
- expect(action.relation.scopes).to be_empty
79
- end
80
- end
81
-
82
- context 'with parameter matching the first scope' do
83
- subject :action do
84
- controller.new(nil, color: 'black')
85
- end
86
-
87
- it 'should apply only the first scope' do
88
- expect(action.relation.scopes.size).to eq(1)
89
- expect(action.relation.scopes).to include(color: 'black')
90
- end
91
- end
92
-
93
- context 'with parameter matching the second scope' do
94
- subject :action do
95
- controller.new(nil, size: 'm')
96
- end
97
-
98
- it 'should apply only the second scope' do
99
- expect(action.relation.scopes.size).to eq(1)
100
- expect(action.relation.scopes).to include(size: 'm')
101
- end
102
- end
103
-
104
- context 'with parameters matching both scopes' do
105
- subject :action do
106
- controller.new(nil, size: 'm', color: 'black')
107
- end
108
-
109
- it 'should apply both scopes' do
110
- expect(action.relation.scopes.size).to eq(2)
111
- expect(action.relation.scopes).to include(size: 'm', color: 'black')
112
- end
113
- end
114
- end
115
-
116
- #
117
- # Test :required option.
118
- #
119
- describe 'with :required option' do
120
- let :controller do
121
- Class.new(Controller) do
122
- scope :active, required: true
123
- end
124
- end
125
-
126
- context 'with no matching parameters' do
127
- subject :action do
128
- controller.new
129
- end
130
-
131
- it 'should apply #none' do
132
- expect(action.relation.scopes).to include(none: true)
133
- end
134
- end
135
-
136
- context 'with one parameter matching' do
137
- subject :action do
138
- controller.new(nil, active: 'yes')
139
- end
140
-
141
- it 'should apply one of the scopes' do
142
- expect(action.relation.scopes.size).to eq(1)
143
- expect(action.relation.scopes).to include(active: true)
144
- end
145
- end
146
- end
147
-
148
- #
149
- # Test :except option.
150
- #
151
- describe 'with :except option' do
152
- let :controller do
153
- Class.new(Controller) do
154
- scope :filter, except: :index
155
- end
156
- end
157
-
158
- context 'with matching parameter in the exception action' do
159
- subject :action do
160
- controller.new(:index, filter: 'yes')
161
- end
162
-
163
- it 'should skip the scope' do
164
- expect(action.relation.scopes).to be_empty
165
- end
166
- end
167
-
168
- context 'with matching parameter in a different action' do
169
- subject :action do
170
- controller.new(nil, filter: 'yes')
171
- end
172
-
173
- it 'should apply the scope' do
174
- expect(action.relation.scopes).to include(filter: true)
175
- end
176
- end
177
- end
178
-
179
- #
180
- # Test :only option.
181
- #
182
- describe 'with :only option' do
183
- let :controller do
184
- Class.new(Controller) do
185
- scope :filter, only: :index
186
- end
187
- end
188
-
189
- context 'with matching parameter in the only action' do
190
- subject :action do
191
- controller.new(:index, filter: 'yes')
192
- end
193
-
194
- it 'should apply the scope' do
195
- expect(action.relation.scopes).to include(filter: true)
196
- end
197
- end
198
-
199
- context 'with matching parameter in a different action' do
200
- subject :action do
201
- controller.new(nil, filter: 'yes')
202
- end
203
-
204
- it 'should skip the scope' do
205
- expect(action.relation.scopes).to be_empty
206
- end
207
- end
208
- end
209
-
210
- #
211
- # Test :param option.
212
- #
213
- describe 'with :param option' do
214
- let :controller do
215
- Class.new(Controller) do
216
- scope :search, param: :q
217
- end
218
- end
219
-
220
- context 'with matching parameter' do
221
- subject :action do
222
- controller.new(nil, q: 'test')
223
- end
224
-
225
- it 'should apply the scope' do
226
- expect(action.relation.scopes).to include(search: 'test')
227
- end
228
- end
229
-
230
- context 'without matching parameter' do
231
- subject :action do
232
- controller.new(nil, search: 'test')
233
- end
234
-
235
- it 'should skip the scope' do
236
- expect(action.relation.scopes).to be_empty
237
- end
238
- end
239
- end
240
-
241
- #
242
- # Test :default option.
243
- #
244
- describe 'with :default option' do
245
- let :controller do
246
- Class.new(Controller) do
247
- scope :page, default: 1
248
- end
249
- end
250
-
251
- context 'with matching parameter' do
252
- subject :action do
253
- controller.new(nil, page: 2)
254
- end
255
-
256
- it 'should overwrite the default value' do
257
- expect(action.relation.scopes).to include(page: 2)
258
- end
259
- end
260
-
261
- context 'without matching parameter' do
262
- subject :action do
263
- controller.new(nil)
264
- end
265
-
266
- it 'should use the default value' do
267
- expect(action.relation.scopes).to include(page: 1)
268
- end
269
- end
270
- end
271
-
272
- #
273
- # Test :force option.
274
- #
275
- describe 'with :force option' do
276
- let :controller do
277
- Class.new(Controller) do
278
- scope :sort, force: :id
279
- end
280
- end
281
-
282
- context 'without matching parameter' do
283
- subject :action do
284
- controller.new(nil)
285
- end
286
-
287
- it 'should use the forced value' do
288
- expect(action.relation.scopes).to include(sort: :id)
289
- end
290
- end
291
-
292
- context 'with matching parameter' do
293
- subject :action do
294
- controller.new(nil, sort: :name)
295
- end
296
-
297
- it 'should still use the forced value' do
298
- expect(action.relation.scopes).to include(sort: :id)
299
- end
300
- end
301
- end
302
- end
data/spec/spec_helper.rb DELETED
@@ -1,99 +0,0 @@
1
- require 'simplecov'
2
- SimpleCov.start
3
-
4
- # This file was generated by the `rspec --init` command. Conventionally, all
5
- # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
6
- # The generated `.rspec` file contains `--require spec_helper` which will cause
7
- # this file to always be loaded, without a need to explicitly require it in any
8
- # files.
9
- #
10
- # Given that it is always loaded, you are encouraged to keep this file as
11
- # light-weight as possible. Requiring heavyweight dependencies from this file
12
- # will add to the boot time of your test suite on EVERY test run, even for an
13
- # individual file that may not need all of that loaded. Instead, consider making
14
- # a separate helper file that requires the additional dependencies and performs
15
- # the additional setup, and require it from the spec files that actually need
16
- # it.
17
- #
18
- # The `.rspec` file also contains a few flags that are not defaults but that
19
- # users commonly want.
20
- #
21
- # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
22
- RSpec.configure do |config|
23
- # rspec-expectations config goes here. You can use an alternate
24
- # assertion/expectation library such as wrong or the stdlib/minitest
25
- # assertions if you prefer.
26
- config.expect_with :rspec do |expectations|
27
- # This option will default to `true` in RSpec 4. It makes the `description`
28
- # and `failure_message` of custom matchers include text for helper methods
29
- # defined using `chain`, e.g.:
30
- # be_bigger_than(2).and_smaller_than(4).description
31
- # # => "be bigger than 2 and smaller than 4"
32
- # ...rather than:
33
- # # => "be bigger than 2"
34
- expectations.include_chain_clauses_in_custom_matcher_descriptions = true
35
- end
36
-
37
- # rspec-mocks config goes here. You can use an alternate test double
38
- # library (such as bogus or mocha) by changing the `mock_with` option here.
39
- config.mock_with :rspec do |mocks|
40
- # Prevents you from mocking or stubbing a method that does not exist on
41
- # a real object. This is generally recommended, and will default to
42
- # `true` in RSpec 4.
43
- mocks.verify_partial_doubles = true
44
- end
45
-
46
- # The settings below are suggested to provide a good initial experience
47
- # with RSpec, but feel free to customize to your heart's content.
48
- =begin
49
- # These two settings work together to allow you to limit a spec run
50
- # to individual examples or groups you care about by tagging them with
51
- # `:focus` metadata. When nothing is tagged with `:focus`, all examples
52
- # get run.
53
- config.filter_run :focus
54
- config.run_all_when_everything_filtered = true
55
-
56
- # Allows RSpec to persist some state between runs in order to support
57
- # the `--only-failures` and `--next-failure` CLI options. We recommend
58
- # you configure your source control system to ignore this file.
59
- config.example_status_persistence_file_path = "spec/examples.txt"
60
-
61
- # Limits the available syntax to the non-monkey patched syntax that is
62
- # recommended. For more details, see:
63
- # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
64
- # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
65
- # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
66
- config.disable_monkey_patching!
67
-
68
- # This setting enables warnings. It's recommended, but in some cases may
69
- # be too noisy due to issues in dependencies.
70
- config.warnings = true
71
-
72
- # Many RSpec users commonly either run the entire suite or an individual
73
- # file, and it's useful to allow more verbose output when running an
74
- # individual spec file.
75
- if config.files_to_run.one?
76
- # Use the documentation formatter for detailed output,
77
- # unless a formatter has already been configured
78
- # (e.g. via a command-line flag).
79
- config.default_formatter = 'doc'
80
- end
81
-
82
- # Print the 10 slowest examples and example groups at the
83
- # end of the spec run, to help surface which specs are running
84
- # particularly slow.
85
- config.profile_examples = 10
86
-
87
- # Run specs in random order to surface order dependencies. If you find an
88
- # order dependency and want to debug it, you can fix the order by providing
89
- # the seed, which is printed after each run.
90
- # --seed 1234
91
- config.order = :random
92
-
93
- # Seed global randomization in this process using the `--seed` CLI option.
94
- # Setting this allows you to use `--seed` to deterministically reproduce
95
- # test failures related to randomization by passing the same `--seed` value
96
- # as the one that triggered the failure.
97
- Kernel.srand config.seed
98
- =end
99
- end
@@ -1,19 +0,0 @@
1
- class Controller
2
- include Scopable
3
-
4
- def initialize(action_name = nil, params = {})
5
- @action_name, @params = action_name, params.freeze
6
- end
7
-
8
- def params
9
- @params
10
- end
11
-
12
- def action_name
13
- @action_name.to_s
14
- end
15
-
16
- def relation
17
- scoped(Model.new, params)
18
- end
19
- end
@@ -1,10 +0,0 @@
1
- class Model
2
- def scopes
3
- @scopes ||= {}
4
- end
5
-
6
- def method_missing(name, value = true)
7
- scopes.store(name, value)
8
- self
9
- end
10
- end