File: has_scope.rb

package info (click to toggle)
ruby-has-scope 0.8.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 180 kB
  • sloc: ruby: 556; makefile: 4
file content (234 lines) | stat: -rw-r--r-- 7,659 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
require 'active_support'
require 'action_controller'

module HasScope
  TRUE_VALUES = ["true", true, "1", 1]

  ALLOWED_TYPES = {
    array:   [[ Array ]],
    hash:    [[ Hash, ActionController::Parameters ]],
    boolean: [[ Object ], -> v { TRUE_VALUES.include?(v) }],
    default: [[ String, Numeric ]],
  }

  def self.deprecator
    @deprecator ||= ActiveSupport::Deprecation.new("1.0", "HasScope")
  end

  def self.included(base)
    base.class_eval do
      extend ClassMethods
      class_attribute :scopes_configuration, instance_writer: false
      self.scopes_configuration = {}
    end
  end

  module ClassMethods
    # Detects params from url and apply as scopes to your classes.
    #
    # == Options
    #
    # * <tt>:type</tt> - Checks the type of the parameter sent. If set to :boolean
    #                    it just calls the named scope, without any argument. By default,
    #                    it does not allow hashes or arrays to be given, except if type
    #                    :hash or :array are set.
    #
    # * <tt>:only</tt> - In which actions the scope is applied. By default is :all.
    #
    # * <tt>:except</tt> - In which actions the scope is not applied. By default is :none.
    #
    # * <tt>:as</tt> - The key in the params hash expected to find the scope.
    #                  Defaults to the scope name.
    #
    # * <tt>:using</tt> - If type is a hash, you can provide :using to convert the hash to
    #                     a named scope call with several arguments.
    #
    # * <tt>:in</tt> - A shortcut for combining the `:using` option with nested hashes.
    #
    # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
    #                  if the scope should apply
    #
    # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
    #                      if the scope should NOT apply.
    #
    # * <tt>:default</tt> - Default value for the scope. Whenever supplied the scope
    #                       is always called.
    #
    # * <tt>:allow_blank</tt> - Blank values are not sent to scopes by default. Set to true to overwrite.
    #
    # == Block usage
    #
    # has_scope also accepts a block. The controller, current scope and value are yielded
    # to the block so the user can apply the scope on its own. This is useful in case we
    # need to manipulate the given value:
    #
    #   has_scope :category do |controller, scope, value|
    #     value != "all" ? scope.by_category(value) : scope
    #   end
    #
    #   has_scope :not_voted_by_me, type: :boolean do |controller, scope|
    #     scope.not_voted_by(controller.current_user.id)
    #   end
    #
    def has_scope(*scopes, &block)
      options = scopes.extract_options!
      options.symbolize_keys!
      options.assert_valid_keys(:type, :only, :except, :if, :unless, :default, :as, :using, :allow_blank, :in)

      if options.key?(:in)
        options[:as] = options[:in]
        options[:using] = scopes

        if options.key?(:default) && !options[:default].is_a?(Hash)
          options[:default] = scopes.each_with_object({}) { |scope, hash| hash[scope] = options[:default] }
        end
      end

      if options.key?(:using)
        if options.key?(:type) && options[:type] != :hash
          raise "You cannot use :using with another :type different than :hash"
        else
          options[:type] = :hash
        end

        options[:using] = Array(options[:using])
      end

      options[:only]   = Array(options[:only])
      options[:except] = Array(options[:except])

      self.scopes_configuration = scopes_configuration.dup

      scopes.each do |scope|
        scopes_configuration[scope] ||= { as: scope, type: :default, block: block }
        scopes_configuration[scope] = self.scopes_configuration[scope].merge(options)
      end
    end
  end

  protected

  # Receives an object where scopes will be applied to.
  #
  #   class GraduationsController < ApplicationController
  #     has_scope :featured, type: true, only: :index
  #     has_scope :by_degree, only: :index
  #
  #     def index
  #       @graduations = apply_scopes(Graduation).all
  #     end
  #   end
  #
  def apply_scopes(target, hash = params)
    scopes_configuration.each do |scope, options|
      next unless apply_scope_to_action?(options)
      key = options[:as]

      if hash.key?(key)
        value, call_scope = hash[key], true
      elsif options.key?(:default)
        value, call_scope = options[:default], true
        if value.is_a?(Proc)
          value = value.arity == 0 ? value.call : value.call(self)
        end
      end

      value = parse_value(options[:type], value)
      value = normalize_blanks(value)

      if value && options.key?(:using)
        scope_value = value.values_at(*options[:using])
        call_scope &&= scope_value.all?(&:present?) || options[:allow_blank]
      else
        scope_value = value
        call_scope &&= value.present? || options[:allow_blank]
      end

      if call_scope
        current_scopes[key] = value
        target = call_scope_by_type(options[:type], scope, target, scope_value, options)
      end
    end

    target
  end

  # Set the real value for the current scope if type check.
  def parse_value(type, value) #:nodoc:
    klasses, parser = ALLOWED_TYPES[type]
    if klasses.any? { |klass| value.is_a?(klass) }
      parser ? parser.call(value) : value
    end
  end

  # Screens pseudo-blank params.
  def normalize_blanks(value) #:nodoc:
    case value
    when Array
      value.select { |v| v.present? }
    when Hash
      value.select { |k, v| normalize_blanks(v).present? }.with_indifferent_access
    when ActionController::Parameters
      normalize_blanks(value.to_unsafe_h)
    else
      value
    end
  end

  # Call the scope taking into account its type.
  def call_scope_by_type(type, scope, target, value, options) #:nodoc:
    block = options[:block]

    if type == :boolean && !options[:allow_blank]
      block ? block.call(self, target) : target.send(scope)
    elsif options.key?(:using)
      block ? block.call(self, target, value) : target.send(scope, *value)
    else
      block ? block.call(self, target, value) : target.send(scope, value)
    end
  end

  # Given an options with :only and :except arrays, check if the scope
  # can be performed in the current action.
  def apply_scope_to_action?(options) #:nodoc:
    return false unless applicable?(options[:if], true) && applicable?(options[:unless], false)

    if options[:only].empty?
      options[:except].empty? || !options[:except].include?(action_name.to_sym)
    else
      options[:only].include?(action_name.to_sym)
    end
  end

  # Evaluates the scope options :if or :unless. Returns true if the proc
  # method, or string evals to the expected value.
  def applicable?(string_proc_or_symbol, expected) #:nodoc:
    case string_proc_or_symbol
    when String
      HasScope.deprecator.warn <<-DEPRECATION.squish
        [HasScope] Passing a string to determine if the scope should be applied
        is deprecated and it will be removed in a future version of HasScope.
      DEPRECATION

      eval(string_proc_or_symbol) == expected
    when Proc
      string_proc_or_symbol.call(self) == expected
    when Symbol
      send(string_proc_or_symbol) == expected
    else
      true
    end
  end

  # Returns the scopes used in this action.
  def current_scopes
    @current_scopes ||= {}
  end
end

require 'has_scope/railtie' if defined?(Rails)

ActiveSupport.on_load :action_controller do
  include HasScope
  helper_method :current_scopes if respond_to?(:helper_method)
end