Whether you're building a tiny API or a giant monolith, your app probably needs to render a list of database records somewhere. And you probably want those records to be filterable via URL params, like so:
GET /artists?genre=electronic&sort=name
It'd be nice to sort or filter when the relevant params are present, and return a sensible default dataset otherwise.
GET /artists?name=blake` => artists named 'blake'
GET /artists?genre=electronic&sort=name => electronic artists, sorted by name
GET /artists => all artists
You could conditionally apply filters with hand-written if
statements, but that approach gets uglier the more filters you add.
In Rails, you could use Plataformatec's venerable HasScope gem.
But what if you're working in Roda, Sinatra, or Hanami? Even in Rails, what if you'd rather write dedicated query objects than pollute your models and controllers with filtering code?
Rack::Reducer can help. It's a gem that maps incoming URL params to an array of filter functions you define, applies only the applicable filters, and returns your filtered data. It can make your controller logic as minimal as...
@artists = Artist.reduce(params)
...or, if magical, implicit code isn't your thing, you can call Rack::Reducer as a function to build more explicit queries -- maybe right in your controllers, maybe in dedicated query objects, or really anywhere you like.
# app/controllers/artists_controller.rb
class ArtistsController < ApplicationController
def index
@artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [
->(name:) { where('lower(name) like ?', "%#{name.downcase}%") },
->(genre:) { where(genre: genre) },
->(sort:) { order(sort.to_sym) },
])
render json: @artists
end
end
Rack::Reducer works in any Rack-compatible app, with any ORM, and has no dependencies beyond Rack itself. Full documentation is on GitHub. Whether you work in Rails, Sinatra, Roda, Hanami, or raw Rack, I hope it can be of use to you.
Top comments (8)
Very interesting idea! Might I ask what use case brought you to that solution?
I've been working mostly in Roda and Sinatra this year and less in Rails. One of the things I've missed from Rails is Platformatec's HasScope gem, which solves this filtering problem well. So I set out to write a library that would work in any Rack app.
Turns out I like Rack::Reducer's API better than HasScope's, even in Rails. That's not meant as a dig at HasScope—Reducer relies on features of ruby 2.1+, which didn't exist when HasScope was written.
Interesting - I hadn't seen Roda before, but I'm guessing that you're trending away from "batteries included" frameworks :-)
Do you find yourself using Sinatra and/or Roda on bigger projects? Are you doing a lot of ORM in those environments?
I like Sinatra for tiny projects, and I really love Roda (with Sequel as an ORM) for large ones.
Roda’s concept of a “routing tree” is very similar, structurally, to the way react-router manages client-side routing. So when I build an API in Roda and a UI in React, I can reason about them the same way, despite their being in different languages. In general, I think I value structural similarity more than syntactical similarity — which is part of why I still build APIs in ruby/roda, instead of js/express.
This is all very cool! I might try a project using Roda+Sequel, just to get a feel for some Rails ORM alternatives. I use Sinatra quite a bit, but tend to kick over to Rails if I need heavy DB work.
I like your point about structural similarity - I think this same logic is why folks are loving the Elixir/Phoenix + Elm backend/frontend combination. I'm headed in that direction for my larger projects (though I still love Ruby).
This post has bean featured in Issue #11 of Ruby Tuesday: rubytuesday.katafrakt.me/issues/20...
Neat, thank you! I look forward to the next issue.
Awsome! Does it work with Rails 7?