GET /people => List people
POST /people => Create person
PUT /people => Replace the collection
DELETE /people => Delete the collection
GET /people/:id => List attribute of a person
PUT /people/:id => update the attributes of a person
POST /people/:id => create a new collection under a person
DELETE /people/:id => Delete a person
In rails, we use
index: GET /people => List people
create: POST /people => Create person
show: GET /people/:id => List attribute of a person
update: PUT /people/:id => update the attributes of a person
destroy: DELETE /people/:id => Delete a person
Now we may also have:
index: GET /groups => List groups
create: POST /groups => Create group
show: GET /groups/:id => List attribute of a group
update: PUT /groups/:id => update the attributes of a group
destroy: DELETE /groups/:id => Delete a group
Consider a relationship of Many To Many between users and groups.
How do I express the following?
"Add user X to group Y"
First thought may be:
POST /groups/Y/users?user_id=X
But technically, this means "Create a user whose attributes are {user_id => X} under group Y". This is wrong in two ways, the first is that we don't want to create a user inside a group. The second is that the user attributes are wrong, it's {id => X}.
The correct request would be:
POST /groups_users?user_id=X&group_id=Y
This means "Create a new GroupUser linking node whose attributes are {user_id => X, group_id => Y}".
OK now how do I express the following?
"Put user X into a new group, whose attributes are {name => 'My Group'}"
This is *two* requests:
POST /groups?name="My Group" => returns ID Z
POST /groups_users?user_id=X&group_id=Z
However, we can actually combine the requests like this:
POST /users/X/groups?name="My Group"
In a situation where a Group Belongs To a User, this would create a group under the user.
You would get in trouble if Group Belongs To a User and Group Has and Belongs To Many Users. However, in that case, you should be using another name for one of the assets. For example, Group could belong to a Creator, and a User would have a created_groups association. So the routes would actually be different:
POST /users/X/created_groups?name="My Group"
Which would mean create a group, where User X is the creator of that group.
So, how do we handle this with Rails routing and controllers?
resources :groups do
resources :users, :controller => 'GroupsUser', :only => [:index, :create, :destroy]
end
resources :users do
resources :groups, :controller => 'UserGroups', :only => [:index, :create, :destroy]
end
Now, for user and group resources, we would use a traditional controller. For the GroupsUser and UserGroups controller, it would be a bit different.
UserGroups controller:
index: return all the groups this user is in
create: create a new group, and add this user to that group
destroy: remove this group from the user's list of groups
Note we don't have:
show: this is redundant with groups/show.
update: this would update the attributes on a membership. If it's just a join there are no attributes, however this may be useful if the join has attributes (for example, role)
The most interesting action here is create:
@user = User.find(params[:id])
@group = Group.new(params[:group])
Group.transaction do
if @group.save
if GroupUser.create(params[:group_user].merge{:user => @user, :group => @group})
# success
else
# Could not add user to group
end
else
# Could not create group
end
end
Note that this controller is getting a bit dangerous because there are three logic paths. Generally, controllers will only have two logic paths: success and failure.
Note also that we are merging the params[:group_user] when creating it. This is because we may want to have attributes on the GroupUser. This would all have to be in the form for posting to this action.
Technically, we are creating a group under a user. While this makes perfect sense in a belongs_to relationship, it is a little mind-bending in a many-to-many situation where the relationship is reciprocal. So, I'll end with a question. Do you think that this "double action" is a violation of REST?
First, if I'm just doing CRUD reading and writing, I don't really feel the need to have unit tests for the project, so long as I have end-to-end acceptance tests. The logic in Rails is simple enough that a computer could write it, so I don't really feel a high degree of risk there.
However if I'm dealing with a complex domain then I tend to separate out my domain layer from Rails, and treat my controllers as ports (from Alistair Cockburn's ports and adapters architecture) into and out of the web. I will have them talk to an adapter within my domain, which is all well encapsulated and heavily unit tested. I generally don't unit test the controller or the view layer , and just let my end-to-end acceptance tests ensure that everything is plugged together correctly. However if something is particularly hairy I will cover the rails part in an integration test.
The basic approach is similar to what Eric Evans calls the "Anticorruption Layer" in Domain Driven Design (page. 366).
I'm working on a sample application for an auction house which you can at see at https://github.com/gmoeck/auction_house. There isn't much to see in the actual domain layer yet, but it will give you a general idea.
If a form has a bunch of params, some options (radio buttons) some non-required some required, you have to make sure the fields are wired up some how to get passed in to your domain model. Do you test all the paths?
The more complex side is when the controller then queries the "view side" of the domain to get a response object, which then passes that data to the view, or renders an error or something. This is where my integration sort of tests will sometimes come in if I want to unit test that logic because it is getting complex.
I certainly don't feel like I have this all figured out, and I'm personally excited that people are starting to think more along these lines because I feel like the Rails side of the equation is going to clean up a bit in the immediate future. Sometimes I do feel like Rails is a bit of an overkill though since I can use Rack and accomplish most of what I'm wanting to do with the "web framework" part of my application.
So generally your domain objects play into the controller like:
if obj.create(params); head :ok; else; render :json => obj.errors
I've been looking more into goliath and it seems to strike a nice balance of rack-like directness (no magic) and also basic HTTP API niceties like content encoding.
Personally, I've been on an "acceptance test everything no unit tests" kick. It has proven to:
1) make very reliable software
2) make very slow test suites
Mostly just interested because this is the standard way of writing a controller action.
As an aside, I'm not a big fan of inherited resources. I prefer scaffolding. Tracking down bugs and determining behavior w/ inherited resources is a pain. I'd rather type it out.