{"id":13879400,"url":"https://github.com/chrisfrank/rack-reducer","last_synced_at":"2025-04-04T14:09:04.483Z","repository":{"id":56890211,"uuid":"122790866","full_name":"chrisfrank/rack-reducer","owner":"chrisfrank","description":"Declaratively filter data via URL params, in any Rack app, with any ORM.","archived":false,"fork":false,"pushed_at":"2019-12-17T13:52:43.000Z","size":141,"stargazers_count":247,"open_issues_count":0,"forks_count":7,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-28T13:11:07.730Z","etag":null,"topics":["filter","params","rack","rack-middleware","rails","roda","sequel","sinatra"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chrisfrank.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-02-25T00:00:27.000Z","updated_at":"2025-02-08T12:41:47.000Z","dependencies_parsed_at":"2022-08-20T16:00:51.310Z","dependency_job_id":null,"html_url":"https://github.com/chrisfrank/rack-reducer","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-reducer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-reducer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-reducer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-reducer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrisfrank","download_url":"https://codeload.github.com/chrisfrank/rack-reducer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247190252,"owners_count":20898702,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["filter","params","rack","rack-middleware","rails","roda","sequel","sinatra"],"created_at":"2024-08-06T08:02:19.772Z","updated_at":"2025-04-04T14:09:04.463Z","avatar_url":"https://github.com/chrisfrank.png","language":"Ruby","readme":"Rack::Reducer\n==========================================\n[![Build Status](https://travis-ci.org/chrisfrank/rack-reducer.svg?branch=master)](https://travis-ci.org/chrisfrank/rack-reducer)\n[![Maintainability](https://api.codeclimate.com/v1/badges/675e7a654c7e11c24b9f/maintainability)](https://codeclimate.com/github/chrisfrank/rack-reducer/maintainability)\n[![Version](https://img.shields.io/gem/v/rack-reducer.svg)](https://rubygems.org/gems/rack-reducer)\n\nDeclaratively filter data via URL params, in any Rack app, with any ORM.\n\nInstall\n------------------------------------------\nAdd `rack-reducer` to your Gemfile:\n\n```ruby\ngem 'rack-reducer', require: 'rack/reducer'\n```\n\nRack::Reducer has no dependencies beyond Rack itself.\n\nUse\n------------------------------------------\nIf your app needs to render a list of database records, you probably want those\nrecords to be filterable via URL params, like so:\n\n```\nGET /artists =\u003e all artists\nGET /artists?name=blake` =\u003e artists named 'blake'\nGET /artists?genre=electronic\u0026name=blake =\u003e electronic artists named 'blake'\n```\n\nRack::Reducer can help. It applies incoming URL params to an array of filter\nfunctions you define, runs only the relevant filters, and returns your filtered\ndata. Here’s how you might use it in a Rails controller:\n\n```ruby\n# app/controllers/artists_controller.rb\nclass ArtistsController \u003c ApplicationController\n\n  # Step 1: Instantiate a reducer\n  ArtistReducer = Rack::Reducer.new(\n    Artist.all,\n    -\u003e(name:) { where('lower(name) like ?', \"%#{name.downcase}%\") },\n    -\u003e(genre:) { where(genre: genre) },\n  )\n\n  # Step 2: Apply the reducer to incoming requests\n  def index\n    @artists = ArtistReducer.apply(params)\n    render json: @artists\n  end\nend\n```\n\nThis example app would handle requests as follows:\n\n```ruby\n# GET /artists =\u003e All artists:\n[\n  { \"name\": \"Blake Mills\", \"genre\": \"alternative\" },\n  { \"name\": \"Björk\", \"genre\": \"electronic\" },\n  { \"name\": \"James Blake\", \"genre\": \"electronic\" },\n  { \"name\": \"Janelle Monae\", \"genre\": \"alt-soul\" },\n  { \"name\": \"SZA\", \"genre\": \"alt-soul\" }\n]\n\n# GET /artists?name=blake =\u003e Artists named \"blake\":\n[\n  { \"name\": \"Blake Mills\", \"genre\": \"alternative\" },\n  { \"name\": \"James Blake\", \"genre\": \"electronic\" }\n]\n\n# GET /artists?name=blake\u0026genre=electronic =\u003e Electronic artists named \"blake\"\n[{ \"name\": \"James Blake\", \"genre\": \"electronic\" }]\n```\n\nAPI Documentation\n---------------------------\nhttps://www.rubydoc.info/gems/rack-reducer\n\nFramework-specific Examples\n---------------------------\nThese examples apply Rack::Reducer in different frameworks and ORMs. The\npairings of ORMs and frameworks are arbitrary, just to demonstrate a few\npossible stacks.\n\n### Sinatra/Sequel\nThis example uses [Sinatra][sinatra] to handle requests, and [Sequel][sequel]\nas an ORM.\n\n```ruby\n# config.ru\nclass SinatraExample \u003c Sinatra::Base\n  DB = Sequel.connect ENV['DATABASE_URL']\n\n  # dataset is a Sequel::Dataset, so filters use Sequel query methods\n  ArtistReducer = Rack::Reducer.new(\n    DB[:artists],\n    -\u003e(genre:) { where(genre: genre) },\n    -\u003e(name:) { grep(:name, \"%#{name}%\", case_insensitive: true) },\n  )\n\n  get '/artists' do\n    @artists = ArtistReducer.apply(params).all\n    @artists.to_json\n  end\nend\n```\n\n### Rack Middleware/Ruby Array\nThis example runs a raw Rack app with Rack::Reducer mounted as middleware.\nIt doesn't use an ORM at all -- it just stores data in a ruby array.\n\n```ruby\n# config.ru\nrequire 'rack'\nrequire 'rack/reducer'\nrequire 'json'\n\nARTISTS = [\n  { name: 'Blake Mills', genre: 'alternative' },\n  { name: 'Björk', genre: 'electronic' },\n  { name: 'James Blake', genre: 'electronic' },\n  { name: 'Janelle Monae', genre: 'alt-soul' },\n  { name: 'SZA', genre: 'alt-soul' },\n]\n\napp = Rack::Builder.new do\n  # dataset is an Array, so filter functions use Array methods\n  use Rack::Reducer::Middleware, dataset: ARTISTS, filters: [\n    -\u003e(genre:) { select { |item| item[:genre].match(/#{genre}/i) } },\n    -\u003e(name:) { select { |item| item[:name].match(/#{name}/i) } },\n    -\u003e(sort:) { sort_by { |item| item[sort.to_sym] } },\n  ]\n  run -\u003e(env) { [200, {}, [env['rack.reduction'].to_json]] }\nend\n\nrun app\n```\n\nWhen Rack::Reducer is mounted as middleware, it stores its filtered data in\nenv['rack.reduction'], then calls the next app in the middleware stack. You can\nchange the `env` key by passing a new name as option to `use`:\n\n```ruby\nuse Rack::Reducer::Midleware, key: 'custom.key', dataset: ARTISTS, filters: [\n  # an array of lambdas\n]\n```\n\n### With Rails scopes\nThe Rails [quickstart example](#use) created a reducer inside a\ncontroller, but if your filters use lots of ActiveRecord scopes, it might make\nmore sense to keep your reducers in your models instead.\n\n```ruby\n# app/models/artist.rb\nclass Artist \u003c ApplicationRecord\n  # filters get instance_exec'd against the dataset you provide -- in this case\n  # it's `self.all` -- so filters can use query methods, scopes, etc\n  Reducer = Rack::Reducer.new(\n    self.all,\n    -\u003e(name:) { by_name(name) },\n    -\u003e(genre:) { where(genre: genre) },\n    -\u003e(sort:) { order(sort.to_sym) }\n  )\n\n  scope :by_name, lambda { |name|\n    where('lower(name) like ?', \"%#{name.downcase}%\")\n  }\nend\n\n# app/controllers/artists_controller.rb\nclass ArtistsController \u003c ApplicationController\n  def index\n    @artists = Artist::Reducer.apply(params)\n    render json: @artists\n  end\nend\n```\n\nDefault filters\n------------------------------------------\nMost of the time it makes sense to use *required* keyword arguments for each\nfilter, and skip running the filter altogether when the keyword argments aren't\npresent.\n\nBut sometimes you'll want to run a filter with a default value, even when the\nrequired params are missing. The code below will order by `params[:sort]` when\nit exists, and by name otherwise.\n\n```ruby\nclass ArtistsController \u003c ApplicationController\n  ArtistReducer = Rack::Reducer.new(\n    Artist.all,\n    -\u003e(genre:) { where(genre: genre) },\n    -\u003e(sort: 'name') { order(sort.to_sym) }\n  )\n\n  def index\n    @artists = ArtistReducer.apply(params)\n    render json: @artists\n  end\nend\n```\n\nCalling Rack::Reducer as a function\n-------------------------------------------\nFor a slight performance penalty (~5%), you can skip instantiating a reducer via\n`::new` and just call Rack::Reducer as a function. This can be useful when\nprototyping, mostly because you don't need to think about naming anything.\n\n```ruby\n# app/controllers/artists_controller.rb\nclass ArtistsController \u003c ApplicationController\n  # Step 1: there is no step 2\n  def index\n    @artists = Rack::Reducer.call(params, dataset: Artist.all, filters: [\n      -\u003e(name:) { where('lower(name) like ?', \"%#{name.downcase}%\") },\n      -\u003e(genre:) { where(genre: genre) },\n    ])\n    render json: @artists\n  end\nend\n```\n\nHow Rack::Reducer Works\n--------------------------------------\nRack::Reducer takes a dataset, an array of lambdas, and a params hash.\n\nTo return filtered data, it calls Enumerable#[reduce][reduce] on your array of\nlambdas, with the reduction's initial value set to `dataset`.\n\nEach reduction looks for keys in the `params` hash that match the\ncurrent lambda's [keyword arguments][keywords]. If the keys exist, it\n`instance_exec`s the lambda against the dataset, passing just those keys as\narguments, and finally passes the filtered dataset on to the next lambda.\n\nLambdas that don't find all their required keyword arguments in `params` don't\nexecute at all, and just pass the unaltered dataset down the chain.\n\nThe reason Reducer works with any ORM is that *you* supply the dataset and\nfilter functions. Reducer doesn't need to know anything about ActiveRecord,\nSequel, Mongoid, etc -- it just `instance_exec`s your own code against your\nown dataset.\n\nPerformance\n---------------------\nFor requests with empty params, Rack::Reducer has no measurable performance\nimpact. For requests with populated params, Rack::Reducer is about 10% slower\nthan a set of hand-coded conditionals, according to `spec/benchmarks.rb`.\n\n```\n Conditionals (full)   530.000  i/100ms\n      Reducer (full)   432.000  i/100ms\nConditionals (empty)   780.000  i/100ms\n     Reducer (empty)   808.000  i/100ms\nCalculating -------------------------------------\n Conditionals (full)      4.864k (± 2.3%) i/s -     24.380k in   5.015551s\n      Reducer (full)      4.384k (± 1.3%) i/s -     22.032k in   5.026651s\nConditionals (empty)      7.889k (± 1.7%) i/s -     39.780k in   5.043797s\n     Reducer (empty)      8.129k (± 1.7%) i/s -     41.208k in   5.070453s\n\nComparison:\n     Reducer (empty):     8129.5 i/s\nConditionals (empty):     7889.3 i/s - same-ish: difference falls within error\n Conditionals (full):     4863.7 i/s - 1.67x  slower\n      Reducer (full):     4383.8 i/s - 1.85x  slower\n```\n\nIn Rails, note that `params` is never empty, so use `request.query_parameters`\ninstead if you want to handle parameterless requests at top speed.\n\n```ruby\n# app/controllers/artists_controller.rb\nclass ArtistController \u003c ApplicationController\n  # ArtistReducer = Rack::Reducer.new(...etc etc)\n\n  def index\n    @artists = ArtistReducer.apply(request.query_parameters)\n    render json: @artists\n  end\nend\n```\n\nAlternatives\n-------------------\nIf you're working in Rails, Plataformatec's excellent [HasScope][has_scope] has\nbeen solving this problem since 2009. I prefer keeping my request logic all in\none place, though, instead of spreading it across my controllers and models.\n\n[Periscope][periscope], by Steve Richert, seems like another solid Rails option.\nIt is Rails-only, but it supports more than just ActiveRecord.\n\nFor Sinatra, Simon Courtois has a [Sinatra port of has_scope][sin_has_scope].\nIt depends on ActiveRecord.\n\nContributors\n---------------\nThank you @danielpuglisi, @nicolasleger, @jeremyshearer, and @shanecav84 for\nhelping improve Rack::Reducer!\n\nContributing\n-------------------------------\n### Bugs\nPlease open [an issue](https://github.com/chrisfrank/rack-reducer/issues) on\nGithub.\n\n### Pull Requests\nPRs are welcome, and I'll do my best to review them promptly.\n\nLicense\n----------\n### MIT\n\nCopyright 2018 Chris Frank\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n\n\n[has_scope]: https://github.com/plataformatec/has_scope\n[sin_has_scope]: https://github.com/simonc/sinatra-has_scope\n[sinatra]: https://github.com/sinatra/sinatra\n[sequel]: https://github.com/jeremyevans/sequel\n[reduce]: http://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-reduce\n[keywords]: https://robots.thoughtbot.com/ruby-2-keyword-arguments\n[periscope]: https://github.com/laserlemon/periscope\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisfrank%2Frack-reducer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisfrank%2Frack-reducer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisfrank%2Frack-reducer/lists"}