Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.
So far this chapter has used little bits of code to show how to do different parts of a service. This section will take those bits and unify them into a complete service that can be used as a basis for your own applications. Most (but not all) of the code here is from the previous sections.
In general, a REST service will want to do one of two things: either work records in Mnesia or another data store, or interact with some form of backend application by sending messages back and forth. Here is a full example using Mnesia.
In this example, when a GET event comes in, it will query Mnesia, return a list of all the airports, and return them to the user.
When the user sends a POST request, the system will add a new record to the Mnesia data store. If needed we could also take other actions here, such as invalidating a cache or calling other functions to take other actions.
When the user sends a PUT request, we will update an existing record. In this case we will look it up by its IATA code and update the airport for new information. We cannot handle the case where an airport changes its IATA code, but this should be rare enough a case that we could delete the record and create it again.
When the user sends a DELETE request, we will delete the record from the data store.
There is also an extra clause at the end to catch any requests that are not one of the four major HTTP requests and return a “405 Method Not Allowed” response.
In order for all this to work, we need to have an airport data format; in this case it is very simple and shown in Example 4-2. This record includes only the airport IATA code, name, city, and country.
We must also set up a table in the Mnesia data store, as in Example 4-13. This must be done before the code is run and normally would be done in an .erlang file that Yaws will run on startup.
The calls to io:format
serialize all server activity through the IO server; remove them for
production.
Example 4-13. Setting up Mnesia
%% Add this to the .erlang fileapplication:start(mnesia).mnesia:create_table(airport,[{attributes,record_info(fields,airport)},{index,[country]}]).
Example 4-14 brings all of the airport example code together.
Example 4-14. Full airport example
-module(rest).-include("/usr/lib/erlang/lib/stdlib-1.17.3/include/qlc.hrl").-include("/usr/lib/yaws/include/yaws_api.hrl").-export([out/1,addAirport/4,handle/2]).%-compile(export_all).-define(RECORD_TYPE,airport).-define(RECORD_KEY_FIELD,code).-record(?RECORD_TYPE,{?RECORD_KEY_FIELD,city,country,name}).out(Arg)->Method=method(Arg),io:format("~p:~p~pRequest~n",[?MODULE,?LINE,Method]),handle(Method,Arg).method(Arg)->Rec=Arg#arg.req,Rec#http_request.method.convert_to_json(Lines)->Data=[{obj,[{airport,Line#?RECORD_TYPE.code},{city,Line#?RECORD_TYPE.city},{country,Line#?RECORD_TYPE.country},{name,Line#?RECORD_TYPE.name}]}||Line<-Lines],JsonData={obj,[{data,Data}]},rfc4627:encode(JsonData).addAirport(Code,City,Country,Name)->NewRec=#?RECORD_TYPE{?RECORD_KEY_FIELD=Code,city=City,country=Country,name=Name},io:format("~p:~pAdding Airport~p~n",[?MODULE,?LINE,NewRec]),Add=fun()->mnesia:write(NewRec)end,{atomic,_Rec}=mnesia:transaction(Add),NewRec.handle('GET',_Arg)->io:format("~n~p:~pGET Request~n",[?MODULE,?LINE]),Records=do(qlc:q([X||X<-mnesia:table(?RECORD_TYPE)])),Json=convert_to_json(Records),io:format("~n~p:~pGET Request Response~p~n",[?MODULE,?LINE,Json]),{html,Json};handle('POST',Arg)->{ok,Json,_}=rfc4627:decode(Arg#arg.clidata),io:format("~n~p:~pPOST request~p~n",[?MODULE,?LINE,Json]),Airport=rfc4627:get_field(Json,"airport",<<>>),City=rfc4627:get_field(Json,"city",<<>>),Country=rfc4627:get_field(Json,"country",<<>>),Name=rfc4627:get_field(Json,"name",<<>>),_Status=addAirport(Airport,City,Country,Name),[{status,201},{html,Arg#arg.clidata}];handle('PUT',Arg)->[IndexValue,_]=string:tokens(Arg#arg.pathinfo),{ok,Json,_}=rfc4627:decode(Arg#arg.clidata),io:format("~p:~pPUT request~p~p~n",[?MODULE,?LINE,IndexValue,Json]),Airport=rfc4627:get_field(Json,"airport",<<>>),City=rfc4627:get_field(Json,"city",<<>>),Country=rfc4627:get_field(Json,"country",<<>>),Name=rfc4627:get_field(Json,"name",<<>>),NewRec=#?RECORD_TYPE{?RECORD_KEY_FIELD=Airport,city=City,country=Country,name=Name},io:format("~p:~pRenaming~p",[?MODULE,?LINE,NewRec]),ChangeName=fun()->mnesia:delete({?RECORD_KEY_FIELD,IndexValue}),mnesia:write(NewRec)end,{atomic,_Rec}=mnesia:transaction(ChangeName),[{status,200},{html,IndexValue}];handle('DELETE',Arg)->[IndexValue,_]=string:tokens(Arg#arg.pathinfo),io:format("~p:~pDELETE request~p",[?MODULE,?LINE,IndexValue]),Delete=fun()->mnesia:delete({?RECORD_KEY_FIELD,IndexValue})end,Resp=mnesia:transaction(Delete),caseRespof{atomic,ok}->[{status,204}];{_,Error}->io:format("~p:~pError~p",[?MODULE,?LINE,Error]),[{status,400},{html,Error}]end;handle(Method,_)->[{error,"Unknown method "++Method},{status,405},{header,"Allow: GET, HEAD, POST, PUT, DELETE"}].do(Q)->F=fun()->qlc:e(Q)end,{atomic,Value}=mnesia:transaction(F),Value.
Finally, we need a frontend to use all this with. I created a simple frontend in CoffeeScript with ExtJS (see http://sencha.com) and it is included in Example 4-15. This creates a UI in the browser that looks like Figure 4-1.
Example 4-15. CoffeeScript frontend (airport.coffee)
makeModel=->Ext.define("Airport",extend:"Ext.data.Model",fields:[{name:"airport"}{name:"city"}{name:"country"}{name:"name"}])makeStore=->model=makeModel()store=Ext.create("Ext.data.Store",autoLoad:trueautoSync:truemodel:modelproxy:type:"rest"url:"airports.yaws"#Willneedtochangethebackendherereader:type:"json"root:"data"writer:type:"json")setupAirports=->store=makeStore()rowEditing=Ext.create"Ext.grid.plugin.RowEditing"grid=Ext.create"Ext.grid.Panel"renderTo:document.bodyplugins:[rowEditing]width:500height:300title:"Airports"store:storecolumns:[{text:'Airport',width:60sortable:truedataIndex:"airport"editor:{allowBlank:false}}{text:"City"dataIndex:"city"sortable:trueeditor:{allowBlank:false}}{text:"Country"dataIndex:"country"sortable:trueeditor:{allowBlank:false}}{text:"Airport Name"dataIndex:"name"sortable:trueeditor:{allowBlank:false}}]dockedItems:[xtype:"toolbar"items:[{text:"Add"handler:->store.insert(0,newAirport())rowEditing.startEdit(0,0)}{itemId:'delete'text:"Delete"handler:()->selection=grid.getView().getSelectionModel().getSelection()[0]if(selection)store.remove(selection)}]]Ext.onReadysetupAirports