Skip to content

Instantly share code, notes, and snippets.

@dhh
Last active September 9, 2024 08:13
Show Gist options
  • Save dhh/10022098 to your computer and use it in GitHub Desktop.
Save dhh/10022098 to your computer and use it in GitHub Desktop.
# config/routes.rb
resources :documents do
scope module: 'documents' do
resources :versions do
post :restore, on: :member
end
resource :lock
end
end
# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
include ProjectScoped
def index
@documents = @project.documents
end
def show
@document = @project.documents.find(params[:id])
end
def new
@document = Document.new
end
def create
@document = @project.documents.create! document_params.merge(creator: current_person)
end
end
# app/controllers/documents/locks_controller.rb
module Documents
class LocksController < ApplicationController
include DocumentScoped, ProjectScoped
def update
@document.lock!(current_person)
end
def destroy
@document.unlock!(current_person)
end
end
end
# app/controllers/documents/versions_controller.rb
module Documents
class VersionsController < ApplicationController
include DocumentScoped, ProjectScoped
before_action :set_version
def show
end
def restore
@document.restore!(@version)
end
private
def set_version
@version = @document.versions.find(params[:id])
end
end
end
# app/controllers/concerns/document_scoped.rb
module DocumentScoped
extend ActiveSupport::Concern
included do
before_action :set_document
end
private
def set_document
@document = @project.documents.find(params[:document_id])
end
end
@dhh
Copy link
Author

dhh commented Apr 7, 2014

@atrauzzi, I've yet to see a compelling "make action a service object" example in the wild. Maybe they exist somewhere, though. Then again, maybe unicorns are real too.

If your controller action is shared between browser and API, just use respond_to for the API. Don't make it a separate controller.

@atrauzzi
Copy link

atrauzzi commented Apr 7, 2014

That makes sense and can definitely stay clean provided the surface area of both the web site and the API are identical.

Although an API that mimics the routes of a web site may not always be the nicest to use and the needs sometimes will not be 1:1. When that divergence happens, there is some risk of duplication.

@marcelmorgan
Copy link

@dhh thanks for this. Since we are comparing one form to another, it would be nice to see what the controller looks like with all the actions stuffed in there. Is there a separate gist with that?

@nambrot
Copy link

nambrot commented Apr 7, 2014

I assume ProjectScoped works similarly to DocumentScoped. In that case, does it matter in which order you include DocumentScoped/ProjectScoped?

@dhh
Copy link
Author

dhh commented Apr 7, 2014

@nambrot, yeah, same concept. It matters because ProjectScoped must be loaded first, since DocumentScoped depends on it. It's a Ruby quirk that #include loads things in reverse order, so "include DocumentScoped, ProjectScoped" will load ProjectScoped first, then DocumentScoped.

@mcmorgan, our code in Basecamp started this way, so don't have the before. Wouldn't be hard to recreate though.

@atrauzzi, when they diverge, you can't reuse the logic anyway. Per definition.

@pixeltrix
Copy link

@dhh you can simplify the routes by wrapping the nested resources with a scope, e.g:

resources :documents do
  scope module: 'documents' do
    resources :versions do
      post :restore, on: :member
    end

    resource :lock
  end
end

@kenn
Copy link

kenn commented Apr 7, 2014

@dhh Re: DocumentScoped / ProjectScoped order, is it a Ruby quirk or ActiveSupport::Concern's? I thought ActiveSupport::Concern saves dependencies in an Array and we could reverse it if that's what we want. Was there any reason that we don't do it that way?

@dhh
Copy link
Author

dhh commented Apr 16, 2014

@pixeltrix, yes of course. I've updated the gist to reflect that. Definitely nicer!

@kenn, that's a Ruby quirk. Concern is a module itself, it doesn't change how #include operates.

@seanpdoyle
Copy link

@dhh Wouldn't including ProjectScoped in DocumentScoped make the module dependency explicit, and thus allow you to remove ProjectScoped's inclusion from Documents::VersionsController and Documents::LocksController, avoiding the module ordering quirk entirely?

Also, any particular reason why you call Document.new here, instead of @project.documents.build?

@jakubstraka
Copy link

@dhh would you then apply this namespacing also for models?

So document's versions would go to document folder as version.rb with class Document::Version? Same thing with locks to lock.rb to document folder with class Document::Lock.

Final tree structure would be then

models/document/version.rb
models/document/lock

class Document::Version
   # code here
end

or wrap those classes in plural module?

models/documents/version.rb
models/documents/lock

module Documents
  class Version
    # code here
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment