📜 ⬆️ ⬇️

OCaml and RESTful JSON API using Eliom

Hi, Habr! I present to your attention the translation of the RESTful JSON API manual using Eliom .

This tutorial describes how to create a simple but complete REST API using JSON as the serialization format.

To illustrate our example, suppose we want to provide access to a database of locations that store the description and coordinates (latitude and longitude).

To be RESTful, our interface will conform to the following principles:
')

With this in mind, our goal will be to implement CRUD (Create, Read, Update, Delete) functions to process our resources. We want the following requests to be valid:

GET http: // localhost / will return all available locations.

GET http: // localhost / ID will return the location associated with the ID.

POST http: // localhost / ID with content:

{ "description": "Paris", "coordinates": { "latitude": 48.8567, "longitude": 2.3508 } } 

store this location in the database.

PUT http: // localhost / ID, with some content, will update the location associated with the id.

DELETE http: // localhost / ID will delete the location associated with the ID.

Dependencies



It is assumed that you are already familiar with Eliom , it is necessary to fully understand the tutorial. This tutorial is not an introduction to Eliom.
The following browser extensions can be useful for manually checking the REST API:


Data types


We begin by defining our database types, that is, how we will present our locations and related information. Each location will be associated with a unique and arbitrary identifier, and will also contain the following information: description and coordinates (consisting of latitude and longitude).

We present the coordinates with decimal degrees and use the deriving-yojson library to analyze and serialize our types in JSON.

We use the highlighted type of error returned when something is wrong with the request or with the processing of the request.

As for the database, we use a simple Ocsipersist table.

 type coordinates = { latitude : float; longitude : float; } deriving (Yojson) type location = { description : string option; coordinates : coordinates; } deriving (Yojson) (* List of pairs (identifier * location) *) type locations = (string * location) list deriving (Yojson) type error = { error_message : string; } deriving (Yojson) let db : location Ocsipersist.table = Ocsipersist.open_table "locations" 

Service definition


First, let's define the general service parameters:


 let path = [] let get_params = Eliom_parameter.(suffix (neopt (string "id"))) 

The next step is to define our service APIs. We identify four of them with the same path, using the four HTTP methods at our disposal:


 let read_service = Eliom_service.Http.service ~path ~get_params () let create_service = Eliom_service.Http.post_service ~fallback:read_service ~post_params:Eliom_parameter.raw_post_data () let update_service = Eliom_service.Http.put_service ~path ~get_params () let delete_service = Eliom_service.Http.delete_service ~path ~get_params () 

Handlers


Let's start defining handlers with a few auxiliary values ​​and functions used by handlers.

Since we use the low-level function Eliom_registration.String.send to send our response, we transfer it to three specialized functions: send_json, send_error and send_success (this sends only the 200 OK status code without any content).

Another function helps us verify that the received content type is expected by matching it with the MIME type. In our example, we check that we get JSON.

The read_raw_content function retrieves the specified or standard length number of characters from the Ocsigen stream raw_content.

 let json_mime_type = "application/json" let send_json ~code json = Eliom_registration.String.send ~code (json, json_mime_type) let send_error ~code error_message = let json = Yojson.to_string<error> { error_message } in send_json ~code json let send_success () = Eliom_registration.String.send ~code:200 ("", "") let check_content_type ~mime_type content_type = match content_type with | Some ((type_, subtype), _) when (type_ ^ "/" ^ subtype) = mime_type -> true | _ -> false let read_raw_content ?(length = 4096) raw_content = let content_stream = Ocsigen_stream.get raw_content in Ocsigen_stream.string_of_stream length content_stream 

Then we define our handlers to perform the necessary actions and return a response.

The POST and PUT handlers will read the contents of the original content in JSON and use Yojson to convert it to our types.

In the responses we use HTTP status codes, with values:


The GET handler either returns one location if provided an identifier, otherwise a list of all existing locations.

 let read_handler id_opt () = match id_opt with | None -> Ocsipersist.fold_step (fun id loc acc -> Lwt.return ((id, loc) :: acc)) db [] >>= fun locations -> let json = Yojson.to_string<locations> locations in send_json ~code:200 json | Some id -> catch (fun () -> Ocsipersist.find db id >>= fun location -> let json = Yojson.to_string<location> location in send_json ~code:200 json) (function | Not_found -> (* [id] hasn't been found, return a "Not found" message *) send_error ~code:404 ("Resource not found: " ^ id)) 

Then let's create a common function for the POST and PUT handlers, which have very similar behavior. The only difference is that a PUT request with a nonexistent identifier will return an error (so it will only accept update requests and reject creation requests), while the same request with the POST method will be successful (a new location will be created with an identifier).

 let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) = if not (check_content_type ~mime_type:json_mime_type content_type) then send_error ~code:400 "Content-type is wrong, it must be JSON" else match id_opt, raw_content_opt with | None, _ -> send_error ~code:400 "Location identifier is missing" | _, None -> send_error ~code:400 "Body content is missing" | Some id, Some raw_content -> read_raw_content raw_content >>= fun location_str -> catch (fun () -> (if create then Lwt.return_unit else Ocsipersist.find db id >>= fun _ -> Lwt.return_unit) >>= fun () -> let location = Yojson.from_string<location> location_str in Ocsipersist.add db id location >>= fun () -> send_success ()) (function | Not_found -> send_error ~code:404 ("Location not found: " ^ id) | Deriving_Yojson.Failed -> send_error ~code:400 "Provided JSON is not valid") let create_handler id_opt content = edit_handler_aux ~create:true id_opt content let update_handler id_opt content = edit_handler_aux ~create:false id_opt content 

To delete locations, you need a fourth handler:

 let delete_handler id_opt _ = match id_opt with | None -> send_error ~code:400 "An id must be provided to delete a location" | Some id -> Ocsipersist.remove db id >>= fun () -> send_success () 

Registration of services


Finally, we register services using the Eliom_registration.Any module in order to have full control over the response sent. Thus, we can send the corresponding HTTP status code depending on what happens during the processing of the request (parsing error, resource not found ...), as shown above when defining handlers.

 let () = Eliom_registration.Any.register read_service read_handler; Eliom_registration.Any.register create_service create_handler; Eliom_registration.Any.register update_service update_handler; Eliom_registration.Any.register delete_service delete_handler; () 

Full source


All that we have in the end
 open Lwt (**** Data types ****) type coordinates = { latitude : float; longitude : float; } deriving (Yojson) type location = { description : string option; coordinates : coordinates; } deriving (Yojson) (* List of pairs (identifier * location) *) type locations = (string * location) list deriving (Yojson) type error = { error_message : string; } deriving (Yojson) let db : location Ocsipersist.table = Ocsipersist.open_table "locations" (**** Services ****) let path = [] let get_params = Eliom_parameter.(suffix (neopt (string "id"))) let read_service = Eliom_service.Http.service ~path ~get_params () let create_service = Eliom_service.Http.post_service ~fallback:read_service ~post_params:Eliom_parameter.raw_post_data () let update_service = Eliom_service.Http.put_service ~path ~get_params () let delete_service = Eliom_service.Http.delete_service ~path ~get_params () (**** Handler helpers ****) let json_mime_type = "application/json" let send_json ~code json = Eliom_registration.String.send ~code (json, json_mime_type) let send_error ~code error_message = let json = Yojson.to_string<error> { error_message } in send_json ~code json let send_success () = Eliom_registration.String.send ~code:200 ("", "") let check_content_type ~mime_type content_type = match content_type with | Some ((type_, subtype), _) when (type_ ^ "/" ^ subtype) = mime_type -> true | _ -> false let read_raw_content ?(length = 4096) raw_content = let content_stream = Ocsigen_stream.get raw_content in Ocsigen_stream.string_of_stream length content_stream (**** Handlers ****) let read_handler id_opt () = match id_opt with | None -> Ocsipersist.fold_step (fun id loc acc -> Lwt.return ((id, loc) :: acc)) db [] >>= fun locations -> let json = Yojson.to_string<locations> locations in send_json ~code:200 json | Some id -> catch (fun () -> Ocsipersist.find db id >>= fun location -> let json = Yojson.to_string<location> location in send_json ~code:200 json) (function | Not_found -> (* [id] hasn't been found, return a "Not found" message *) send_error ~code:404 ("Resource not found: " ^ id)) let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) = if not (check_content_type ~mime_type:json_mime_type content_type) then send_error ~code:400 "Content-type is wrong, it must be JSON" else match id_opt, raw_content_opt with | None, _ -> send_error ~code:400 "Location identifier is missing" | _, None -> send_error ~code:400 "Body content is missing" | Some id, Some raw_content -> read_raw_content raw_content >>= fun location_str -> catch (fun () -> (if create then Lwt.return_unit else Ocsipersist.find db id >>= fun _ -> Lwt.return_unit) >>= fun () -> let location = Yojson.from_string<location> location_str in Ocsipersist.add db id location >>= fun () -> send_success ()) (function | Not_found -> send_error ~code:404 ("Location not found: " ^ id) | Deriving_Yojson.Failed -> send_error ~code:400 "Provided JSON is not valid") let create_handler id_opt content = edit_handler_aux ~create:true id_opt content let update_handler id_opt content = edit_handler_aux ~create:false id_opt content let delete_handler id_opt _ = match id_opt with | None -> send_error ~code:400 "An id must be provided to delete a location" | Some id -> Ocsipersist.remove db id >>= fun () -> send_success () (* Register services *) let () = Eliom_registration.Any.register read_service read_handler; Eliom_registration.Any.register create_service create_handler; Eliom_registration.Any.register update_service update_handler; Eliom_registration.Any.register delete_service delete_handler; () 

Source: RESTful JSON API using Eliom

From translator


I wanted the OCaml community to grow and grow, and the language itself developed faster, the language is good, and sometimes even better than mainstream languages, here are a few of its advantages: it’s going native, its syntax is rather concise and understandable (not immediately, but it is easier for me than Haskell, but in general it is a taste), also a rather convenient type system and a good OOP of course. If this translation was useful to someone or made me look at OCaml and its ecosystem, try it, then I can do more translations or author articles. Please report errors in lichku.

PS:
Introductory articles about OCaml and Ocsigen on Habré with which it is probably worth getting acquainted to beginners:


but of course it is better to get acquainted with the official manuals, because the articles above are 6-7 years old, some basics can be extracted from them (and given the sluggish development of the language, the probability to extract basic knowledge and not be undermined, tends to be 100%), but I I can not vouch that at the moment everything is right there, especially in the article about Oscigen. All good and pleasant path in development.

Source: https://habr.com/ru/post/336336/


All Articles