Halcyon

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.