Search code examples
ruby-on-railsruby-on-rails-3activerecorddynamic-scope

Ruby on Rails combining scopes dynamically


I'd like to be able to dynamically combine scopes at runtime, to create a custom query based on user input. Say I have a library application with a Book model, and I have the following scopes defined: checked_out, fiction, non_fiction, overdue, and checked_out_by.

The last scope, checked_out_by, is a lambda that takes a library_user_id as an argument. The application allows the librarian to run different kinds of queries that combine these scopes dynamically, based on user input. So if the librarian wants to create a report showing all non-fiction books that are checked out, it's easy to do something like this:

criteria = ["non_fiction", "checked_out"]  <= array built based on what the user selected  
books = Book.scoped  
criteria.each {|criteria| books = books.send(criteria)}  

But how do I do this with the scope that is set up as a lambda? For instance, if the user wants to create a query showing books that are overdue and checked_out_by a particular person? In this case, I'd want the criteria array to contain the "overdue" and "checked_out_by" scopes, but I can't figure out how to set that up with the lambda scope. I tried:

criteria = ["non_fiction", {:checked_out_by => 6}]  

but that gives me an error saying "{:checked_out_by => 6} is not a symbol". I also tried

criteria = ["non_fiction", [checked_out_by, 6]]

and a few other variants, but none of them worked. Does anyone know how to do this?


Solution

  • You can send parameters as the second parameter to send. Building on your example, you could do something like this:

    criteria = [:non_fiction, { checked_out_by: 6 }]
    
    criteria.each do |c|
      case c
        when Hash then c.keys.each { |key| books = books.send key, c[key] }
        when Symbol then books = books.send c
      end
    end
    

    Or you can use an OpenStruct in place of the Hash.

       criteria = [:non_fiction, OpenStruct.new(key: :checked_out_by, value: 6)]
    
       criteria.each do |c|
          books = case c
            when OpenStruct then books.send c.key, c.value
            when Symbol then books.send c
          end
        end
    

    Or, better yet, create an class that has the key and value that you need.