Skip to content

Latest commit

 

History

History
513 lines (454 loc) · 20.6 KB

README.md

File metadata and controls

513 lines (454 loc) · 20.6 KB

Pharo Rest Tutorial

Introduction

In this tutorial, you will learn how to set up a REST backend in a very short time with Pharo. This tutotial will use some Open Data to populate the service. Our service will expose the list of Automated External Defibrillator (AED) available to the public in France. Data is provided by OpenStreetMap organisation at https://www.data.gouv.fr/fr/datasets/defibrillateurs-automatiques-externes-issus-dopenstreetmap/.

REST

A RESTful web application exposes information about itself in the form of information about its resources. It also enables the client to take actions on those resources, such as create new resources (i.e. create a new user) or change existing resources (i.e. edit a post). When a RESTful API is called, the server will transfer to the client a representation of the state of the requested resource (any object the API can provide information about). The representation of the state is often in a JSON format but could also be in another format like XML, HTML. What the server does when the client, call one of its APIs depends on 2 things that you need to provide to the server:

  1. An identifier for the resource you are interested in. This is the URL for the resource, also known as the endpoint. In fact, URL stands for Uniform Resource Locator.
  2. The operation you want the server to perform on that resource, in the form of an HTTP method, or verb. The common HTTP methods are GET for reading a resource, POST for creating a new resource, PUT for updating or replacing an existing resource, and DELETE.

For example, fetching a specific Twitter user, using Twitter’s RESTful API, will require a URL that identify that user and the HTTP method GET.

Usage example: get the list of magazines in JSON format

GET /api/v1/magazines.json HTTP/1.1
Host: www.example.gov.au
Accept: application/json, text/javascript

Preparation

Install Pharo Launcher

Pharo Launcher is the fastest way to get a working Pharo environment: image (an object space with Pharo Core libraries) + virtual machine. Pharo Launcher is a tool allowing you to easily download Pharo core images (Pharo stable version, Pharo development version, old Pharo versions, Pharo Mooc) and automatically get the appropriate virtual machine to run these images. You can install Pharo Launcher from https://pharo.org/download. Pharo Launcher documentation is available at https://pharo-project.github.io/pharo-launcher/installation/.

Set up Data

  1. Download the dataset at https://www.data.gouv.fr/fr/datasets/r/e153b245-a704-422b-82c3-95eb6a296a0f. Once unzipped, you will find following files:

    • license.txt
    • metadata.csv
    • data.csv

    We will only use the data.csvfile.

  2. Download a fresh Pharo 10.0 - 64bit image through Pharo Launcher and launch it

  3. Clean the data We could load the data with the NeoCSV library but we will rather do it with pure Pharo for this tutorial. An AED information can be splitted on many lines (description with line breaks). Let's uniformize the content.

csvFile := FileLocator home / 'aed_csv' / 'data.csv'.
"Get lines from the data file without the header"
lines := csvFile contents withInternalLineEndings lines copyWithoutFirst.
" Iterate over lines and merge them when it does not begin with a number"
records := OrderedCollection new: lines size.
lines do: [ :line |
	(line first isDigit or: [ line first = $- ])
		ifTrue: [ records add: line ]
		ifFalse: [ records atLast: 1 put: records last , line ]
	 ].

Create our domain objects

Since our domain is about AED, let's create an AED object.

Object << #AED
	slots: { #id . #latitude . #longitude . #city . #freeAccess . #accessInfo };
	package: 'AA-RestTuto'

Once the AED class defined, we can generate accessors (getter and setters) automatically with the system browser (right-click on the class, then generate accessors). Since one record is now bound to one AED, we can build our domain objects.

records collect: [ :each | | record |
	record := $; split: each.
	AED new
		latitude: (record at: 5);
		longitude: (record at: 6);
		city: (record at: 11);
		freeAccess:  (record at: 13) = 'oui';
		accessInfo: (record at: 17);
		yourself. ].

We now need to store our domain objects to made them available to our REST API. We could store them in a database but we will simply use an in-memory store for simplicity.

Object << #AEDStore
	slots: { #items };
	sharedVariables: { #Default };
	package: 'AA-RestTuto'

The Default shared variable (variable that is shared by all instances) will keep an instance of AEDStore, itself keeping the data: a collection of AEDs. We will define a method to access the default store:

default
	^ Default ifNil: [ Default := self new ]

To simplify the loading of the data, all the code we seen has been integrated into the AEDStore class. Import the AEDStore-init.st file to get an AEDSotre able to load data from a file (drag and drop the file onto the Pharo image): Then, we will load the data in the store:

	AEDStore default loadFromFile: FileLocator home / 'aed_csv' / 'data.csv'.

Let's now take a look at the data we imported. You can open a Playground (CTRL+OP) and inspect the following expression (Do It button):

AEDStore default.

The information displayed is not very useful. With Pharo, you can adapt tools to your needs, your domain. Let's add a custom inspection method on the AEDStore:

inspectionItems: aBuilder
	<inspectorPresentationOrder: 0 title: 'AED'> 
	
	^ aBuilder newTable
		addColumn: (SpStringTableColumn new 
			title: 'Index';
			evaluated: [ :each | each id ];
			beNotExpandable;
			yourself);
		addColumn: (SpStringTableColumn new 
			title: 'Latitude';
			evaluated: [ :each | each latitude ];
			beNotExpandable;
			yourself);
		addColumn: (SpStringTableColumn new  
			title: 'Longitude';
			beNotExpandable;
			evaluated: [ :each | each longitude ];
			yourself);
		addColumn: (SpStringTableColumn new 
			title: 'City';
			evaluated: [ :each | each city ];
			sortFunction: #city ascending;
			yourself);
		addColumn: (SpCheckBoxTableColumn new 
			title: 'Free access';
			evaluated: [ :each | each freeAccess ];
			sortFunction: #freeAccess ascending;
			yourself);
		addColumn: (SpStringTableColumn new  
			title: 'Access info'; 
			evaluated: [ :each | each accessInfo ];
			yourself);
		items: items asOrderedCollection;
		yourself

This methods adds a custom tab named 'AED' when we inspect an AED instance. In the method above, we describe the UI to be displayed: a table with columns.

Basic REST back-end

We will load Tealight library that includes a micro web framework (Teapot) as well as small layer on top it to ease its integration into Pharo. First, we will load Teapot (that comes with Tealight) to get an updated version that is working on the very latest Pharo version:

Metacello new 
	repository: 'github://demarey/Teapot/repository';
	baseline: 'Teapot';
	load

Then, we can load Tealight:

Metacello new 
	repository: 'github://astares/Tealight/repository';
	baseline: 'Tealight';
	load

You can find some documentation on the project page: https://github.com/astares/Tealight.

Working with the Tealight server

After you have the framework installed you can easily start a Tealight web server by selecting

"Tealight" -> "WebServer" -> "Start webserver"

from the Pharo world menu. Internally this starts a Teapot server with some defaults.

Tealight menu You can also easily stop the server from the Tealight web server menu by using "Stop webserver" or open a webbrowser on the server by using "Browse".

Accessing the default Teapot

After you started the server you can easily access the running Teapot instance from your code or playground

TLWebserver teapot.

You can easily experiment with Teapot routes, for instance using

TLWebserver teapot 
	GET: '/hi' -> 'HelloWorld'.

If you point your browser to http://localhost:8080/hi you will receive your first "HelloWorld" greeting.

If you open an inspector on the Teapot instance

TLWebserver teapot inspect.

you will see that a dynamic route was added:

Inspector on the teapot

So you can dynamically add new routes for GET, POST, DELETE or other HTTP methods interactively.

We recommend to read the Teapot chapter of the Pharo Enterprise Book to get a better understanding of the possibilities of the underlying Teapot framework.

Bind the REST server with the domain

We will now try to get the data of an AED. Let us add a new route that will return a random AED.

TLWebserver teapot 
	GET: '/random' -> [ AEDStore default items atRandom ].

If you point your browser to http://localhost:8080/random you will receive "an AED" ... What happened? Our webserver receives an HTTP GET request on /random URL. It then executes the associated action, getting a random AED in the store, and then provide it back in the HTTP answer. Objects cannot be transfered through HTTP. They need to be serialized. The default serialization used by our web server is the text representation of the object, i.e. the result of #printStringmethod applied to the object.

aed := AEDStore default items atRandom.
aed printString  "'an AED'"

We now need to define a serialization for our domain object. A widely used format is JSON. In pharo, we use the NeoJSON library. We will add a method neoJsonMapping: on the AED class that will specify how to map an AED to a Json object. A class method in Pharo can be seen as a static method in Jave (as opposed to an instance method that can only be used on an instance and not a class).

neoJsonMapping: mapper
	mapper mapAllInstVarsFor: self.

Now, let's take a look at the JSON produced for an AED:

NeoJSONWriter toStringPretty: aed. 

will return:

{
"id" : 6231,
"latitude" : "48.1739017998959",
"longitude" : "6.44966",
"city" : "Épinal",
"freeAccess" : false,
"accessInfo" : ""
}

We can now ask our webserver to encode objects using json as default.

TLWebserver teapot
	output: TeaOutput json.

You can now point your browser to http://localhost:8080/random and you will see the difference. You can now reset the web server routes:

TLWebserver teapot removeAllDynamicRoutes.

Defining a REST based interface

REST API in annotated methods

While it is nice to experiment with dynamic routes by adding them one by one to the Teapot instance it would be even more convinient

  • if we could define the REST API using regular Smalltalk methods,
  • if we could map each URL easily.

To support that Tealight adds a special utility class (called TLRESTAPIBuilder) to help you easily build an API. Lets see how we can use it.

First of all we need to create a simple class in the system either from the browser or with an expression:

Object << #PharoWorkshopRESTAPI
	slots: { };
	package: 'AA-RestTuto'

Now we can define a class side method:

random: aRequest
	<REST_API: 'GET' pattern: 'random'>
	
	^ AEDStore default items atRandom

As you see we use a pragma in this class marking the class side method as REST API method and defining the kind of HTTP method we support as well as the function path for our REST service.

Generate our web API

Now we can use the utility class to generate the dynamic routes for us, sending a message to our class ending up in our method:

TLRESTAPIBuilder buildAPI 

This creates the dynamic routes for us.

To simplify the update of the API and do not loose the server configuration, we can define a class method on PharoWorkshopRESTAPI to store the configuration:

configuration

	^ TLWebserver defaultConfiguration copyWith: #defaultOutput -> #json

Then we will add a #build method to simplify the building:

build

	TLWebserver defaultServer configuration: self configuration.
	TLRESTAPIBuilder buildAPI.
	TLWebserver start. "ensure the web server is started"

Also note that by default, there is an "api" prefix generated into the URL for all REST based methods so you need to point your browser to: http://localhost:8080/api/random.

We can now add a new method to get an AED given its id. Just before, we will define another method that will provide the AED store as most of the api will need it.

store
	^ AEDStore default
aed: aRequest
	<REST_API: 'GET' pattern: 'aeds/<id>'>
	
	^ self store aedWithId: (aRequest at: #id) asInteger

Before trying this new route, we need to add the #aedWithId: method (instance side) on AEDStore.

aedWithId: anId
	^ self items detect: [ :aed | aed id = anId ]

You can notice our pattern now includes an id parameter that will be retrieved from the URL. In a browser, we can now point to: http://localhost:8080/api/aeds/1

We can also add a route to delete an AED:

removeAed: aRequest
	<REST_API: 'DELETE' pattern: 'aeds/<id>'>
	
	^ self store removeAedWithId: (aRequest at: #id) asInteger

Before trying this new route, we need to add the #removeAedWithId: method (instance side) on AEDStore.

removeAedWithId: anId
	^ self items remove: (self aedWithId: anId)

To try to remove the AED with id #2, we can use curl command in a terminal. First, we will fetch the AED with #id 2.

$ curl -X GET http://localhost:8080/api/aeds/2
{"id":2,"latitude":"42.3143360005073","longitude":"9.2671767","city":"Sermano","freeAccess":false,"accessInfo":""}
$ curl -X DELETE http://localhost:8080/api/aeds/2

If you then try to get the resource, you will get a Not Found error:

$ curl -X GET http://localhost:8080/api/aeds/2
NotFound: [ :aed | aed id = anId ] not found in OrderedCollection

Add a link to the localisation of the AED

We can also provide in our API a way to easily locate an AED on a map. We can do that quickly by adding a new method on the AED object:

mapLink
	^ 'https://www.openstreetmap.org/search?query=' , self latitude , ',' , self longitude, '#map=18/' , self latitude , ',' , self longitude, '&layers=N'

and by adding a new route on our API:

aedLocalisation: aRequest
	<REST_API: 'GET' pattern: 'aeds/<id>/map'>
	
	| aed |
	aed := self store aedWithId: (aRequest at: #id) asInteger.
	^ #map -> aed mapLink

Now, you can try to browse an AED localization at http://localhost:8080/api/aeds/1/map. Click on the provided link.

Navigation (HATEOAS)

So far, we have a web-based service that handles the core operations involving AED data. But that’s not enough to make things "RESTful". In fact, what we have built is better described as RPC (Remote Procedure Call). That is because there is no way to know how to interact with this service. You would have to write a document to describe its usage. So what is missing? Hypermedia links to allow resources discovery and navigation. A side effect of NOT including hypermedia in the representation is that clients MUST hard code URIs to navigate the API. This is what is called HATEOAS: Hypermedia As The Engine of Application State.

A decorator to add links to our domain objects

We will define a new class that will decorate a class of our domain. It is a simple decorator that will add links to resources to an existing domain object. Let's create a HateoasEntity class:

Object << #HateoasEntity
	slots: { #model . #links };
	package: 'AA-RestTuto'

It will have a method on class side to easily instantiate it on the decorated object:

on: aModel
	^ self new
		initializeWith: aModel;
		yourself

On instance side, we define the #initialization method as follows:

initializeWith: aModel
	model := aModel.
	links := Dictionary new.

We will add a method to easily add a self link:

addSelfLink: relativeUrl
	links at: #self put: (self serverUrl / relativeUrl) asString 

and a method to get the server base URL:

serverUrl 
	^ PharoWorkshopRESTAPI serverUrl

PharoWorkshopRESTAPI class >> serverUrlis defined as follows:

serverUrl
	^ TLWebserver defaultServer teapot server url / 'api'

It is the sever url of the default Teapot server we use to serve our REST resources. We now need to ensure that our HateoasEntity objects can be serialized in JSON since the API will now use this decorator to add links to our resources and send it as the answer to the HTPP request. We need to merge the decorator and the decorated object (domain object) into the same structure. We will use a simple dictionary that is easily convetible to JSON.

asDictionary

	| dict |
	dict := Dictionary new.
	model class slotNames do: [ :slotName | 
		dict at: slotName put: (model instVarNamed: slotName) ].
	dict at: #links put: links.
	^ dict

To avoid the hardcoding of a domain object variables (slots), we use Pharo instrospection capabilities to add decorated object instance variable one by one to the dictionary. Last, we add all the links we want to add to this object. We also need to define the JSON serialization of the HateoasEntity objects. We define the mapping in a HateoasEntity class method:

neoJsonMapping: mapper

	mapper
		for: self
		customDo: [ :mapping | 
		mapping encoder: [ :entity | entity asDictionary ] ]

Let's now try to generate a JSON entity for an AED with a self link:

aed := AEDStore default items atRandom.
NeoJSONWriter toString: 
    ((HateoasEntity on: aed) 
        addSelfLink: 'aed/' , aed id asString;
        yourself) asDictionary

Evaluating and printing (CTRL+P) this expression in a playground, you should see something like:

{
    "latitude":"48.1739017998959",
    "city":"Épinal",
    "accessInfo":"",
    "freeAccess":false,
    "longitude":"6.44966",
    "id":6231,
    "links":{
        "self":"http://localhost:8080/api/aed/1"
    }
}

Define a route to get all AEDs

We now have everything to define a route to get a list of ALL AEDs. We will not give all information on AED since data size would be too big. Instead, we will just provide a list of links to discover AEDs. In PharoWorkshopRESTAPI class, add an #aeds: method:

aeds: aRequest
	<REST_API: 'GET' pattern: 'aeds'>
	
	^ self store items collect: [ :aed | (self serverUrl / 'aeds' / aed id asString) asString ]

For each AED in the store, we collect the link to access the AED resource. This is the list of links that we will send back to the client. Here is a sample output if you try to access http://localhost:8080/api/aeds:

[
	"http://localhost:8080/api/aeds/1",
	"http://localhost:8080/api/aeds/2",
	"http://localhost:8080/api/aeds/3",
	"http://localhost:8080/api/aeds/4",
	"http://localhost:8080/api/aeds/5",
	"http://localhost:8080/api/aeds/6",
	"http://localhost:8080/api/aeds/7",
	"http://localhost:8080/api/aeds/8",
	"http://localhost:8080/api/aeds/9",
	"http://localhost:8080/api/aeds/10"
]

You can now click on any resource to perform a new rest request and discover the resource.

Add a self link to AED entities

A good practice in REST is to provide a self link to resources that are sent to the client. We can update our #aed: method to include a self link:

aed: aRequest
	<REST_API: 'GET' pattern: 'aeds/<id>'>
	
	| aed |
	aed := self store aedWithId: (aRequest at: #id) asInteger.
	^ (HateoasEntity on: aed)
		addSelfLink: 'aeds/' , aed id asString;
		yourself

Here is the result of a call to a specific AED:

{
    "latitude":"48.1739017998959",
    "city":"Épinal",
    "accessInfo":"",
    "freeAccess":false,
    "longitude":"6.44966",
    "id":6231,
    "links":{
        "self":"http://localhost:8080/api/aed/1"
    }
}

Resources

To build this tutorial, I used some existing resources:

Libraries used in this project are:

Pharo ecosystem also have other frameworks you can use to build REST services: