Notice: The website is currently being udpated. Sorry for any inconvenience.
Customizing Clients
Note: This article concerns the Ruby client for your application specifically
but most of the principles should still be applicable for clients not written
in Ruby.
Depending on how you plan to deploy your Halcyon application, either it will be
accessed via any number of clients (curl et al) or you can provide a
customized client interface for your app (or both, really). A great deal of
this process involves designing a good interface to your application via a
remote client, but looking past that, let’s take a look at the technical
aspects of customizing a client for your application.
The Application
Let’s start with a simple application whose controller looks like this:
1 class Messages < Application 2 3 def list 4 ok Message.limit(10).all 5 end 6 7 def show 8 if (msg = Message[params[:id]]) 9 ok msg 10 else 11 raise NotFound.new 12 end 13 end 14 15 def create 16 ok Message << params 17 end 18 19 def update 20 Message.filter(:id => params[:id]).update(params) 21 ok 22 end 23 24 def delete 25 Message.filter(:id => params[:id]).delete 26 ok 27 end 28 29 end
Though this is a simplistic approach (in production we would want and need much
more in terms of handling errors) it should suffice.
This application manages a single Message resource which we’ll assume
consists of nothing other than a text message of a certain size (say, 140
characters, similar to Twitter). The routes are defined
like this:
1 Halcyon::Application.route do |r| 2 3 r.resources :messages 4 5 end
This means that we will primarily interact with the application with the
following routes:
<pre>
GET /messages
POST /messages
GET /messages/:id
PUT /messages/:id
DELETE /messages/:id
</pre>
In the future we may want to associate users with messages, but for now we’ll
just clump them all together in a single faceless cloud.
The model itself is simple enough: it just provides a mapping for the database,
but we will not define it here (though we are using Sequel
syntax for performing actions on the model).
For our purposes, our application will be called Messanger.
The Client
On the client side we may want to define a pseudo model to behave functionally
like the actual Message model on the server side, but we’ll leave that as an
exercise for the reader; for now we’ll just focus on defining the messaging
client to be able to submit requests and handle responses from the server.
Let’s go ahead and look at what our message client will look like:
1 module Messanger 2 3 class Client < Halcyon::Client 4 5 # get list of messages 6 def list 7 if (msgs = get('/messages'))[:status] == 200 8 # success 9 msgs[:body] # return message 10 else 11 # failure 12 msgs # return status and error message 13 end 14 end 15 16 # get a single message 17 def show(id) 18 if (msg = get('/messages/'+id))[:status] == 200 19 # success 20 msg[:body] # return message 21 else 22 # failure 23 msg # return status and error message 24 end 25 end 26 27 # create a message 28 def create(message) 29 if (msg = post('/messages', :message => message))[:status] == 200 30 # success 31 return msg[:body] # the new message id 32 else 33 # failure 34 return msg 35 end 36 end 37 38 # update a message 39 def update(id, message) 40 if (msg = put('/messages/'+id, :message => message))[:status] == 200 41 # success 42 return true 43 else 44 # failure 45 return msg 46 end 47 end 48 49 # delete a message 50 def delete(id) 51 if (msg = delete('/messages/'+id))[:status] == 200 52 # success 53 return true 54 else 55 # failure 56 return msg 57 end 58 end 59 60 end 61 62 end
Not the best code in the world and pretty repetitive. These are certainly
things that can be improved upon (and should be) with abstraction methods and
possibly even enabling exceptions (where exceptions are raised if a non-200
response is given).
Also, if we chose to use more descriptive HTTP response codes, such as 201
Created instead of just 200 OK for the create method, we could change our
code to better take advantage of this descriptive consistency related to the
REST approach. This is highly recommended.
Let’s take a look at actually using this client in IRB. We’ll assume we’re also
running the Messanger application on port 4647 (a common port for Halcyon
apps).
<pre>
$ irb -r lib/client
>> client = Messanger::Client.new('http://localhost:4647/')
=> #<Messanger::Client>
>> client.list
=> []
>> client.show(12)
=> {:status=>404, :body=>'Not Found'}
>> client.create('Hi!')
=> 1
>> client.list
=> [{:id=>1, :message=>'Hi!'}]
>> client.create('Howdy!')
=> 2
>> client.list
=> [{:id=>1, :message=>'Hi!'}, {:id=>2, :message=>'Howdy!'}]
>> client.show(1)
=> {:id=>1, :message=>'Hi!'}
>> client.update(1, 'Bamboozle...')
=> true
>> client.get('/messages/1')[:body]
=> {:id=>1, :message=>'Bamboozle...'}
>> client.delete(2)
=> true
>> client.delete(2)
=> {:status=>404, :body=>'Not Found'}
>> client.list
=> [{:id=>1, :message=>'Bamboozle...'}]
</pre>
And so on. Hopefully this example is clear enough.
Now that we have a working interface to the resources in the application, we
can write a pseudo model that maintains an active client and can wrap up method
calls to appear almost like working with the real model remotely.