Mark
Mark

Reputation: 6445

Rails - apply an array of scopes

I have a class that takes an array of scopes and applies them iteratively:

class AssignableLearningObjectives::Collector
  def initialize(user:, only_self_assignable: false, scopes: [])
    @user = user
    @only_self_assignable = only_self_assignable
    @scopes = scopes
  end

  .....

  def available_objectives
    objectives = assignable_objectives.or(manager_assigned_objectives).or(global_objectives).distinct
    return objectives unless scopes.any?

    scopes.each{ |scope| objectives = objectives.send(scope) }
    objectives
  end

My issue is with

scopes.each{ |scope| objectives = objectives.send(scope) }
objectives

Is there a better way of doing this? I was hoping for a rails method apply_scopes or something like that, however can't find anything like that.

My concern is the scopes are sent from the controller, and it is possible for the user to submit a request with a scope of 'destroy_all' or something equally fun.

Is there an easy way for me to let rails handle this? Or will I need to manually check each scope before I apply it to the collection?

Thanks in advance

EDIT:

I'm happy to validate each scope individually if I have to, but even that's causing issues. There is a method in rails which was dropped in 3.0.9 which I could use, Model.scopes :

https://apidock.com/rails/v3.0.9/ActiveRecord/NamedScope/ClassMethods/scopes

however that's deprecated. Is there any method I can call on a class to list its scopes? I can't believe the feature was there in rails 3 and removed completely...

Upvotes: 1

Views: 1132

Answers (1)

mu is too short
mu is too short

Reputation: 434665

From the fine guide:

14 Scopes
[...]
To define a simple scope, we use the scope method inside the class, passing the query that we'd like to run when this scope is called:

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

This is exactly the same as defining a class method, and which you use is a matter of personal preference:

class Article < ApplicationRecord
  def self.published
    where(published: true)
  end
end

So scope is mostly just a fancy way of creating a class method that is supposed to have certain behavior (i.e. return a relation) and any class method method that returns a relation is a scope. scope used to be something special but now they're just class methods and all the class methods are copied to relations to support chaining.

There is no way to know if method Model.m is a "real" scope that will return a relation or some random class method without running it and checking what it returns or manually examining its source code. The scopes method you seek is gone and will never come back.

You could try to blacklist every class method that you know is bad; this way lies bugs and madness.

The only sane option is to whitelist every class method that you know is good and is something that you want users to be able to call. Then you should filter the scopes array up in the controller and inside AssignableLearningObjectives::Collector. I'd check in both places because you could have different criteria for what is allowed depending on what information is available and what path you're taking through the code; slightly less DRY I suppose but efficiency and robustness aren't friends.

You could apply the scope whitelist in the AssignableLearningObjectives::Collector constructor or in available_objectives.


If you want something prettier than:

scopes.each{ |scope| objectives = objectives.send(scope) }
objectives

then you could use inject:

def available_objectives
  objectives = assignable_objectives....
  scopes.inject(objectives) { |objectives, scope| objectives.send(scope) }
end

Upvotes: 1

Related Questions