has_scope

repository·main·Indexed 23 days ago

https://github.com/heartcombo/has_scope

A tool for dynamically applying named ActiveRecord scopes to resources based on incoming request parameters. It simplifies filtering logic in Rails controllers and can also be used in standalone Ruby objects (POROs) by including the HasScope module. It provides configuration options for parameter type validation, custom mapping via blocks, and a current_scopes method to track active filters.

Tokens
2.4K
Snippets
6
Records
12
Agent score
33%

What's inside has_scope

  1. Handle Boolean scopes with has_scope

    main

    When type: :boolean is set, the scope is called without arguments if the parameter is a "true" value.

    • True values: 'true' and '1' are parsed as true.
    • False values: Everything else is parsed as false.
    • Important: A bare parameter (e.g., ?active) results in nil, which is treated as false. To pass true as an argument to a scope, use ?active=true or ?active=1.

    If allow_blank: true is set, the scope will be called with the actual value (including blank values) instead of just being triggered by truthiness.

  2. Use HasScope in Plain Old Ruby Objects (PORO)

    main

    To use HasScope outside of a Rails controller (e.g., in a Query Object), include the HasScope module. When calling apply_scopes, you must pass the collection as the first argument and the parameters hash as the second argument.

    Example implementation:

    class GraduationsSearchQuery
      include HasScope
    
      has_scope :featured, type: :boolean
      has_scope :by_degree
      has_scope :by_period, using: %i[started_at ended_at], type: :hash
    
      def perform(collection: Graduation, params: {})
        apply_scopes(collection, params)
      end
    end
    
    # Usage in a controller:
    # @graduations = GraduationsSearchQuery.new.perform(collection: Graduation, params: params)
    class GraduationsSearchQuery
      include HasScope
    
      has_scope :featured, type: :boolean
      has_scope :by_degree
      has_scope :by_period, using: %i[started_at ended_at], type: :hash
    
      def perform(collection: Graduation, params: {})
        apply_scopes(collection, params)
      end
    end
  3. Use HasScope in Rails Controllers

    main

    In Rails, has_scope is automatically available in all controllers. Use it to declare which model scopes are permitted to be applied based on incoming request parameters. To actually apply these scopes to a resource, call apply_scopes(ResourceClass).

    Example declaration and usage:

    class GraduationsController < ApplicationController
      # Declare permitted scopes
      has_scope :featured, type: :boolean
      has_scope :by_degree
      has_scope :by_period, using: %i[started_at ended_at], type: :hash
    
      def index
        # Apply the declared scopes to the Graduation model
        @graduations = apply_scopes(Graduation).all
      end
    end
    class GraduationsController < ApplicationController
      has_scope :featured, type: :boolean
      has_scope :by_degree
      has_scope :by_period, using: %i[started_at ended_at], type: :hash
    
      def index
        @graduations = apply_scopes(Graduation).all
      end
    end
  4. Use blocks with has_scope for custom logic

    main

    You can pass a block to has_scope to manually manipulate the value or the scope chain.

    Standard block arguments:

    • controller (or context/query): The instance of the controller or object.
    • scope: The current scope chain.
    • value: The value extracted from the parameters.

    Example: Custom value manipulation

    # If value is 'all', return the original scope without applying the filter
    has_scope :category do |controller, scope, value|
      value != 'all' ? scope.by_category(value) : scope
    end

    Example: Boolean block (2 arguments) When used with type: :boolean and without allow_blank, the block receives only two arguments and is only invoked if the parameter is truthy:

    has_scope :not_voted_by_me, type: :boolean do |controller, scope|
      scope.not_voted_by(controller.current_user.id)
    end

    Example: Keyword arguments If your model scope uses keyword arguments, you must use a block:

    # Model
    scope :for_course, lambda { |course_id:| where(course_id: course_id) }
    
    # Controller
    has_scope :for_course do |controller, scope, value|
      scope.for_course(course_id: value)
    end
  5. Retrieve applied scopes with current_scopes

    main

    The current_scopes method returns a hash of all scopes that were successfully applied during the current request. The keys are the scope names and the values are the values used to call them.

    • In Controllers/Views: It is available as a helper method.
    • In POROs: It is a protected method. To access it externally, you must explicitly make it public using public :current_scopes.

    Example output: If ?featured=true&by_degree=phd is passed, current_scopes returns:

    { featured: true, by_degree: 'phd' }
    # Inside a controller or view
    current_scopes
    #=> { featured: true, by_degree: 'phd' }
  6. Configure has_scope options

    main

    The has_scope method accepts several configuration options to control how parameters are mapped to scopes:

    OptionDescription
    :typeValidates parameter type. Supports :boolean, :hash, and :array. Defaults to not allowing hashes/arrays unless specified.
    :onlySpecifies which controller actions the scope is applied in.
    :exceptSpecifies which controller actions the scope is NOT applied in.
    :asThe key in the params hash to look for. Defaults to the scope name.
    :usingSubkeys to be used as arguments when type: :hash is used.
    :inA shortcut for combining :using with nested hashes.
    :ifA method or proc to call to determine if the scope should apply.
    :unlessA method or proc to call to determine if the scope should NOT apply.
    :defaultThe default value for the scope. If provided, the scope is always called.
    :allow_blankIf true, blank values will be sent to scopes (defaults to false).

    Note: Symbols are never permitted as parameter values to prevent memory leaks; ensure routing constraints use string values.

  7. Use `:using` to map hash parameters to scope arguments

    main

    If you receive a hash of parameters but your scope expects multiple individual arguments, use the :using option. This requires :type to be set to :hash.

    Example: If your URL is ?filter[start_date]=2023-01-01&filter[end_date]=2023-12-31 and your scope is by_period(start, end):

    # The :using option tells HasScope to extract :start_date and :end_date
    # from the :filter hash and pass them as arguments.
    has_scope :filter, type: :hash, as: :filter, using: [:start_date, :end_date]
  8. Use `:in` as a shortcut for `:as` and `:using`

    main

    The :in option is a convenience shortcut. When you use :in, it automatically sets :as to the value of :in and sets :using to the list of scope names provided.

    # This is equivalent to:
    # has_scope :filter, type: :hash, as: :filter, using: [:start_date, :end_date]
    has_scope :filter, in: :filter, using: [:start_date, :end_date]
  9. Access applied scopes via `current_scopes`

    main

    When apply_scopes is called, it populates a current_scopes hash containing the keys and values that were actually used to trigger the scopes. This is useful for tracking which filters are currently active in your UI.

    In Rails, current_scopes is registered as a helper_method, making it available in your views.

  10. Configure scopes in Rails controllers with `has_scope`

    main

    Use the has_scope class method in your controllers to declare which parameters should be automatically applied as scopes to your models. This allows you to map URL parameters directly to ActiveRecord (or similar) scopes.

    Configuration Options

    OptionTypeDescription
    :type:boolean, :hash, :array, :defaultValidates the parameter type. :boolean calls the scope without arguments. :hash and :array allow complex structures.
    :onlyArrayList of controller actions where the scope is applied. Defaults to :all.
    :exceptArrayList of controller actions where the scope is NOT applied. Defaults to :none.
    :asSymbolThe key in params to look for. Defaults to the scope name.
    :inSymbolShortcut for setting :as and :using simultaneously.
    :usingArrayIf :type is :hash, this defines which keys in the hash are passed as arguments to the scope.
    :if / :unlessProc, Symbol, or StringConditions to determine if the scope should be applied. (Note: String is deprecated).
    :defaultAnyA default value used if the parameter is missing. Can be a Proc that accepts the controller instance.
    :allow_blankBooleanIf true, blank values are passed to the scope instead of being ignored.

    Customizing Scope Application with Blocks

    You can pass a block to has_scope to manually control how the scope is called. The block yields (controller, target, value).

    class GraduationsController < ApplicationController
      has_scope :featured, type: :boolean, only: :index
      has_scope :by_degree, only: :index
      has_scope :category do |controller, scope, value|
        value != "all" ? scope.by_category(value) : scope
      end
    
      def index
        @graduations = apply_scopes(Graduation).all
      end
    end
  11. Apply scopes to a target using `apply_scopes`

    main

    The apply_scopes method is used within a controller action to execute the scopes defined via has_scope on a target object (usually a model class).

    It inspects the params hash (or a provided hash), validates the values against the configured :type, and applies the resulting scopes to the target object. It returns the modified target.

    # Inside a controller action
    def index
      # Graduation is the target class
      # apply_scopes will return Graduation modified by any matching params
      @graduations = apply_scopes(Graduation).all
    end
    def index
      @graduations = apply_scopes(Graduation).all
    end