That’s right Web Servicification. Servicifying, just like the subtle art of Strategery, is an oft derided but subtly powerful part of my toolkit.
Or at least it is now. I have been working on a monitoring application so that we could figure out when things were going pear shaped — as opposed to finding out after the fact. This monitoring application was somewhat novel in that it successfully got me out of doing hard things of little quantifiable value and let me focus on doing easy things of much greater value (see above goal). Using RRD is a good example of outsourcing a hard problem — what to do with all that data ?!?– to something that handled it for me. Not an especially hard leap to make, thanks to lots of SysAdmins who feel exactly the same way, but still, I’m really happy that I’m not collapsing data to keep my disk footprint somewhat finite.
This whole strategy of ‘doing more with less effort’ is really fun, I’m searching for something non geeky to try it in. If I get the same efficiency boost in my personal life I’ll have enough time to write a best seller, become a kickboxing champion, or both.
In the context of my (geeky) monitoring app, Web Servicification is something else that gets me out of a couple of fairly hard to do things. Wait, let me back up a bit. Web Servicification with ActiveResource gets me out of a couple of fairly hard to do things.
The first is writing a web service. Go ahead and sneer at the difficulties of writing an XML consuming service, but until you’ve rolled your own in Java, you just don’t have that Juan Valdez “I wrote this one bean at a time” feeling.
The second hard thing this gets me out of is writing monitors for a bunch of heterogenous systems. We’ve got some things implemented in Java, some in Ruby, some in Perl, etc. I either slap a bunch of web interfaces on all of those systems, then write some centralized code to poll those interfaces, or I slap a web interface on my monitor app and let other people figure out which statistics are meaningful to them, and how often they should be updated.
A Web Service by Default
ActiveResource comes more or less enabled by default in Rails 2.0. Every method in a default generated controller can be accessed either by the UI or an REST action. Here is an example controller generated for one of my resources.
# GET /samples
# GET /samples.xml
def index
…
respond_to do |format|
format.html #index.html.erb
format.xml { render :xml => @monitor_instances }
end
end
# GET /monitor_instances/foo
# GET /samples/1
# GET /samples/1.xml
def show
end
# GET /samples/new
# GET /samples/new.xml
def new
end
# GET /samples/1/edit
def edit
end
# POST /samples
# POST /samples.xml
def create
end
# PUT /samples/1
# PUT /samples/1.xml
def update
end
# DELETE /samples/1
# DELETE /samples/1.xml
def destroy
end
Notice that the standard REST verbs are implemented in the same methods that handle rails application page requests. That’s pretty cool, and it means that you’ve got basic CRUD from the get go. The secret is in the render method (in bold above), which returns either a page or XML content depending on the requested format. If the request ends in xml, it’s assumed to be REST, otherwise it’s assumed to be a standard page request.
Routing for both REST and page based requests is provided in routes.rb:
map.resources :monitor_instances
provides routing access to the default methods defined above.
Accessing the Default Web Service
ActiveResource::Base is the class that abstracts the wire format and provides basic CRUD access to the resource. To access the MonitorInstance objects defined above, I could do the following:
class MonitorInstance < ActiveResource::Base
# define what you need in here
end
The ActiveResource based MonitorInstance acts similarly to an ActiveRecord based MonitorInstance:
monitor_instance = MonitorInstance.create(:name=>monitor_name,:monitor_instance_id=>parent_monitor.id,:frequency_id=>frequency.id,:status_id=>@status_by_name[‘good’].id,:monitor_type_id=>@monitor_type_by_name[“stand_alone”].id)
creates a monitor with the parameters as specified above.
MonitorInstance.delete(monitor.id) OR
monitor_instance.destroy
removes the monitor instance.
monitor_instance = MonitorInstance.find(1) finds me the monitor instance with an ID of 1.
removes that Monitor. So far, so good.
Find (not by ID)
What if I want to find something by a secondary attribute, like name? The default rails app expects qualifying parameters to be passed in a params hash:
monitor = MonitorInstance.find(:first,:params=>{:name=>monitor_name})
My MonitorInstances can be nested under other MonitorInstances. In the Rails app model, each MonitorInstance model specifies that it belongs_to :monitor_instance.
This doesn’t quite have a corollary in the ActiveResource world. ActiveResource is concerned with abstracting basic access of web based resources, and that associations are not available via that abstraction layer. When I want to find a nested MonitorInstance, I do the following:
def get_monitor(monitor_name,parent_name = nil)
if(parent_name != nil)
parent = get_monitor(parent_name)
@logger.debug(“finding first instance of monitor #{monitor_name} under #{parent_name}”)
monitor = MonitorInstance.find(:first,:params=>{:name=>monitor_name,:monitor_instance_id=>parent.id})
else
@logger.debug(“finding first instance of monitor #{monitor_name}”)
monitor = MonitorInstance.find(:first,:params=>{:name=>monitor_name})
end
…
end
So I need to first get the parent resource, then make a request with the parent ID in the params hash, as indicated by the bolded text above.
Updating — Avoid Non Writeable Parameters!
It was hard to find any updating doc that didn’t just say “to update, just invoke the ActiveResource-derived object save method”. Which sounds great in theory, but didn’t work, because the default implementation of save POSTS all attributes, even those that are considered immutable, to the web service endpoint. For instance, my MonitorInstance class has an id field that is immutable. That field is posted with all other (mutable) fields. There is a method in ActiveResource to remove all immutable/protected attributes, but that method calls an undefined logger object to notify you that you are trying to modify an immuatble attribute, and an exception is raised.
To get around this, I stripped the immutable attribute — the id — out of the incoming params hash of the controller update method (in the Rails app) — see the bolded text below:
class StatisticsController
…
# PUT /statistics/1
# PUT /statistics/1.xml
def update
@statistic = Statistic.find(params[:id])
if(params[:statistic])
logger.debug(params[:statistic].inspect)
if(params[:statistic][:id] != nil)
logger.debug(“removing ID from input params!”)
params[:statistic].delete(:id)
end
end
respond_to do |format|
if @statistic.update_attributes(params[:statistic])
…
end
…
end
….
end
Nested Resources
The StatisticsController above handles all posts to Statistics resources, which are 1..N measurements associated with a monitor. In order to enforce that kind of scoping in the request path, I need to update the monitor_instances routes to scope the statistics routes:
#map.resources :statistics
map.resources :monitor_instances, :has_many => [:statistics]
In the statistics controller, I now need to always be aware of the ‘owner’ MonitorInstance. I do this by adding a before_filter, a method that gets invoked prior to every method being called:
before_filter :find_monitor_instance
This before_filter corresponds to the find_monitor_instance method, which returns the appropriate MonitorInstance:
private
def find_monitor_instance
@monitor_instance = MonitorInstance.find(params[:monitor_instance_id])
end
Now I have an attribute that I can refer to in my controller. Note that in all of the controller methods that handle both REST and page requests, I need to scope my model requests/updates with the @monitor_instance variable:
def index
@statistics = Statistic.find(:all,:conditions=>{:monitor_instance_id=>@monitor_instance.id})
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @statistics }
end
end
I can also take advantage of RAILS path freebies. For instance, after a create request for statistic, I redirect to the appropriate monitor_instance scoped path like this:
if @statistic.save
flash[:notice] = ‘Statistic was successfully created.’
format.html { redirect_to(monitor_instance_statistic_path(@monitor_instance,@statistic)) }
format.xml { render :xml => @statistic.to_xml, :status => :created, :location => monitor_instance_statistic_path(@monitor_instance,@statistic) }
else
monitor_instance_statistic_path generates a path that looks like {path to server}/monitor_instances/1/statistics/3.html or .xml depending on the requested output format.
Some Helpful Links:
ActiveResource RDoc
REST + ActiveResource
Comments from this Railscast
You must be logged in to post a comment.