I’ve been writing a gem to implement and extend common controller
functionality so that Rails can be used with Javascript frameworks like
AngularJS (which we are using), Ember.js, etc. in such a way that the
user
doesn’t have to tweak a a bunch of rails g controller boilerplate code
to
provide services for use in these frameworks that in turn would require
various changes to fit the normal Rails way to specify things.
The gem is here:
It needs a heck of a lot of work still, no working tests at the moment,
and
integrating roar-rails in its 3.0.0 branch.
Our aim/current strategy for web development is to:
- Keep # of controllers (and amount of code required for each) to a
minimum, but they shouldn’t be overly monolithic to the point that they
become difficult to maintain. - (Only) use REST where it makes sense.
- Try to keep logic and operational knowledge out of the client side.
More specifically:
- Unlike the typical historical Rails app where the controller was
really
the controller accessing the model and serving up the view, when you are
using AngularJS and Ember.js heavily, the primary role of Rails REST-ish
service provider; not everything fits REST (hypertext/hypermedia doesn’t
fit every application) and there are definitely going to be custom
actions. - Services need to be able to be defined quickly and need to return
errors
in JSON format with relevant status codes, etc. - You need to be able to transactionally make alterations to
associations
and collections of associations, not just to a single resource. - The services also should make it as easy as possible to integrate with
these frameworks. To persist an associated model you shouldn’t have to
tack
on _attributes to the key in the JSON because odds are that the
Javascript
app is just storing it as whatever the association name is, or perhaps
some
other name that makes more sense. You should be able to send in the JSON
assocation data for something and the controller should know via mass
assignment security that you don’t have rights to write the association,
but you do have rights to change the list of associations, etc. so it
would
just change those associations if you told it to allow that, etc. In
other
words, accepts_nested_attributes_for is inadequate.
What we’ve tried and looked into as a DRY way to using Rails in large
part
as JSON service provider:
- RABL: provides an way to do json views (to replace sending options
into
as_json/to_json) does not handle incoming JSON to be persisted in a
similar
way. - ActiveModel::Serializers available now and coming in Rails 4 - similar
to
RABL in that it does not map incoming JSON to be persisted. - strong_parameters available now and coming in Rails 4 - keeps you from
being able to accidentally persist something that the controller doesn’t
specifically define, but does not define JSON view. - roar-rails - provides a way to specify both the JSON view and what is
accepted, so we are attempting to integrate it currently.
Where complication rears its ugly head:
When you see things like this, it looks easy:
def index
@companies = Company.all
respond_with @companies
end
But respond_with makes assumptions about what should be called, and then
you should handle errors because it should try to return those as JSON
with
an appropriate HTTP status code (:ok, :unprocessable_entity, :created,
:forbidden, :internal_server_error, etc.), then there is location url
which
I’m not sure if has a place in a service meant for consumption by a
service
meant to be consumed by a javascript app?, etc. For an idea of the
various
things that people have to do and what they run into:
http://forums.pragprog.com/forums/191/topics/8247
etc.
So you maybe end up with something like this just to handle a POST:
this makes sense for Rails served view, as a failed create, but does
it
makes sense in a JSON service-oriented controller serving to a page
served
by a different controller? We don’t need to retain state in that case,
so
new is never called on its own/doesn’t need to be separated out?
def new
@company = Company.new
respond_with @company
end
I have not tested this- just a possibility of something that would
use
roar-rails which provides consume! and deserialization.
def create
# another method to implement that relies on proper authorization
if can_create?
respond_with(errors: [‘Access denied to create
#{self.class.name}’],
status: forbidden)
end
begin
@company = Company.new(params[:company])
consume! @company
if @company.errors
respond_with(errors: [@company.errors], location: users_url,
status: unprocessable_entity)
else
respond_with(@company, location: users_url, status: created)
end
rescue
puts $!.inspect, [email protected]
# TODO: add support for other formats
respond_to do |format|
format.json { render json: {errors: [$!.message]}, status:
(:internal_server_error) }
end
end
end
If you were writing a more generic REST-ish (not necessarily a
hypertext/hypermedia driven app) controller that could be used to make
everything I described as easy and DRY as possible, such that the
Javascript app writer hardly had to think about Rails at all, and
providing
robust services was mostly just a matter of writing some models and JSON
representations for various views, how would you do it?
I am guessing the standard response is “these things depend on the
environment, so it doesn’t make sense to abstract them into a controller
that will just add another layer of things in the way”, but I’m really
trying to provide something that will help here; we have a ton of legacy
models, etc. that are currently handled by another SOA system that we
need
to replace in piecemeal over time, so what may seem like minor
differences
in the amount of code required will make a big difference for us when it
comes time to us having to upgrade Rails, etc.