-
-
Save jferris/2ed8ecab1ff068a5be3e to your computer and use it in GitHub Desktop.
Code Show and Tell: PolymorphicFinder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ApplicationController < ActionController::Base | |
private | |
def requested_purchaseable | |
PolymorphicFinder. | |
finding(Section, :id, [:section_id]). | |
finding(TeamPlan, :sku, [:team_plan_id]). | |
finding(IndividualPlan, :sku, [:individual_plan_id]). | |
finding(Product, :id, [:product_id, :screencast_id, :book_id, :show_id]). | |
find(params) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Finds one of several possible polymorphic members from params based on a list | |
# of relations to look in and attributes to look for. | |
# | |
# Each polymorphic member will be tried in turn. If an ID is present that | |
# doesn't correspond to an existing row, or if none of the possible IDs are | |
# present in the params, an exception will be raised. | |
class PolymorphicFinder | |
def initialize(finder) | |
@finder = finder | |
end | |
def self.finding(*args) | |
new(NullFinder.new).finding(*args) | |
end | |
def finding(relation, attribute, param_names) | |
new_finder = param_names.inject(@finder) do |fallback, param_name| | |
Finder.new(relation, attribute, param_name, fallback) | |
end | |
self.class.new(new_finder) | |
end | |
def find(params) | |
@finder.find(params) | |
end | |
private | |
class Finder | |
def initialize(relation, attribute, param_name, fallback) | |
@relation = relation | |
@attribute = attribute | |
@param_name = param_name | |
@fallback = fallback | |
end | |
def find(params) | |
if id = params[@param_name] | |
@relation.where(@attribute => id).first! | |
else | |
@fallback.find(params) | |
end | |
end | |
end | |
class NullFinder | |
def find(params) | |
raise( | |
ActiveRecord::RecordNotFound, | |
"Can't find a polymorphic record without an ID: #{params.inspect}" | |
) | |
end | |
end | |
private_constant :Finder, :NullFinder | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'spec_helper' | |
describe PolymorphicFinder do | |
describe '#find' do | |
it 'finds the first given finder when present' do | |
individual_plan = create(:individual_plan, sku: 'abc') | |
result = PolymorphicFinder. | |
finding(IndividualPlan, :sku, [:individual_plan_id]). | |
find(individual_plan_id: 'abc') | |
expect(result).to eq(individual_plan) | |
end | |
it 'finds the first of several possible params' do | |
screencast = create(:screencast) | |
result = PolymorphicFinder. | |
finding(Product, :id, [:book_id, :screencast_id, :product_id]). | |
find(screencast_id: screencast.to_param) | |
expect(result).to eq(screencast) | |
end | |
it 'cascades when the first finder is not present' do | |
create(:individual_plan, sku: 'abc') | |
team_plan = create(:team_plan, sku: 'def') | |
result = PolymorphicFinder. | |
finding(IndividualPlan, :sku, [:individual_plan_id]). | |
finding(TeamPlan, :sku, [:team_plan_id]). | |
find(team_plan_id: 'def') | |
expect(result).to eq(team_plan) | |
end | |
it 'raises an exception for an unknown ID' do | |
create(:individual_plan, sku: 'abc') | |
finder = PolymorphicFinder. | |
finding(IndividualPlan, :sku, [:individual_plan_id]) | |
expect { finder.find(individual_plan_id: 'def') }. | |
to raise_error(ActiveRecord::RecordNotFound) | |
end | |
it 'raises an exception without any ID' do | |
params = { 'key' => 'value' } | |
finder = PolymorphicFinder. | |
finding(IndividualPlan, :sku, [:individual_plan_id]) | |
expect { finder.find(params) }. | |
to raise_error(ActiveRecord::RecordNotFound, /#{Regexp.escape(params.inspect)}/) | |
end | |
end | |
end |
Another solution is to just pass a parameter in the router and not have to guess anything.
resources :books do
resource :purchase, purchaseable: :book
end
resources :screencasts do
resource :purchase, purchaseable: :screencast
end
Hi,
@jferris I've have some case that is something like that
if params[:individual_plan_id] && params[:team_plan_id]
Class.new(params)
else if params[:individual_plan_id]
OtherClass.new(params)
else if params[:section_id]
AnotherClass.new(params)
I've tried to use PolymorphicFinder
in this case but I'm not getting it because it only verifies if at least one parameter is present.
I've tried to change but i'm stuck in these line that verifies only one parameter at time.
Do you have some ideas to fit my need?
Thanks!
@pithyless, I agree, the first example it took me awhile to understand what it was doing, it seemed like too much abstraction. However, your example I understood exactly what was going on in a matter of seconds.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Based on the original code in the blog post, I would have refactored it more along these lines:
https://gist.github.com/pithyless/50457bdaadb21e88d282
The PolymorphicFinder creates unnecessary abstractions and hides the fact that we're just handling an unconventional use-case. In the end, which pattern will help a new developer grok the author's intention with the least amount of head-scratching?