Привет! Представляю вашему вниманию перевод туториала RESTful JSON API using Eliom.
В этом туториале рассказывается, как создать простой, но полный REST API с использованием JSON в качестве формата сериализации.
Чтобы проиллюстрировать наш пример, предположим, что мы хотим предоставить доступ к базе данных местоположений, хранящих описание и координаты (широта и долгота).
Чтобы быть RESTful, наш интерфейс будет соответствовать следующим принципам:
- URL-адреса и GET-параметры определяют ресурсы
- Методы HTTP (GET, POST, PUT, DELETE) используются для определения действий
- Действие GET безопасно (без побочных эффектов)
- Действия PUT и DELETE являются идемпотентными
- Запросы являются stateless (в период между запросами клиента никакая информация о состоянии клиента на сервере не хранится)
Имея это в виду, наша цель будет заключаться в реализации функций CRUD (Create, Read, Update, Delete) для обработки наших ресурсов. Мы хотим, чтобы следующие запросы были действительными:
GET http:// localhost/ вернет все доступные местоположения.
GET http:// localhost/ID вернет местоположение, связанное с ID.
POST http:// localhost/ID с содержимым:
{
"description": "Paris",
"coordinates": {
"latitude": 48.8567,
"longitude": 2.3508
}
}
сохранит это местоположение в базе данных.
PUT http:// localhost/ID, с некоторым содержимым, обновит местоположение, связанное с идентификатором.
DELETE http:// localhost/ID удалит местоположение, связанное с ID.
Зависимости
- eliom >= 4.0
- yojson
- deriving-yojson
Предполагается что вы уже знакомы с Eliom, это нужно что бы понять туториал полностью. Этот туториал не является введением в Eliom.
Следующие браузерные расширения могут быть полезны для ручной проверки REST API:
Типы данных
Начнем с определения наших типов баз данных, то есть того, как мы будем представлять наши местоположения и связанную с ними информацию. Каждое местоположение будет связано с уникальным и произвольным идентификатором, а так же будет содержать следующую информацию: описание и координаты (состоящие из широты и долготы).
Мы представляем координаты с десятичными градусами и используем библиотеку deriving-yojson для анализа и сериализации наших типов в JSON.
Мы используем выделенный тип ошибки, возвращаемый, когда что-то не так с запросом или с обработкой запроса.
Что касается базы данных, мы используем простую таблицу Ocsipersist.
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"
Определение служб
Во-первых, давайте определим общие параметры обслуживания:
- path(путь) API: одинаковый для всех служб.
- Параметр GET, который является необязательным идентификатором, указанным в качестве суффикса URL. Устанавливаем его как необязательный, чтобы мы могли отличать запросы GET для одного или всех ресурсов и возвращать подробную ошибку, если идентификатор отсутствует в запросах POST, PUT и DELETE. Альтернативой будет использование двух служб на одном пути (одна с id, а другая без).
let path = []
let get_params =
Eliom_parameter.(suffix (neopt (string "id")))
Следующий шаг — определить наши API службы. Мы определяем четыре из них с одним и тем же путем, используя четыре метода HTTP в нашем распоряжении:
- Метод GET будет использоваться для доступа к базе данных, для любого из ресурсов, если не указан идентификатор, или только для единственного ресурса. Если ресурс не будет соответствовать идентификатору, будет возвращена ошибка.
- Метод POST будет использоваться для создания нового ресурса (или его обновления, если он уже существует). Мы устанавливаем один параметр POST: Eliom_parameter.raw_post_data, чтобы получить необработанный JSON и обойти блокировку параметров после декодирования.
- Метод PUT будет использоваться для обновления существующего ресурса. Если ресурс не будет соответствовать идентификатору, будет возвращена ошибка. Нам не нужно определять параметр POST, PUT-службы принимают значение Eliom_parameter.raw_post_data как содержимое по умолчанию.
- Метод DELETE будет использоваться для удаления существующего ресурса. Если ресурс не будет соответствовать идентификатору, будет возвращена ошибка.
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
()
Обработчики
Давайте начнем определение обработчиков с помощью нескольких вспомогательных значений и функций, используемых обработчиками.
Поскольку мы используем функцию низкого уровня Eliom_registration.String.send для отправки нашего ответа, мы переносим его на три специализированные функции: send_json, send_error и send_success (эта отправляет только код состояния 200 OK без какого-либо содержимого).
Другая функция помогает нам проверить, что полученный тип содержимого является ожидаемым, сопоставляя его с MIME-типом. В нашем примере проверим, что мы получаем JSON.
Функция read_raw_content извлекает указанное или стандартное length количество символов из потока Ocsigen 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
Затем мы определяем наши обработчики для выполнения необходимых действий и возврата ответа.
Обработчики POST и PUT будут считывать содержимое исходного контента в JSON и использовать Yojson для преобразования его в наши типы.
В ответах мы используем коды состояния HTTP, с значениями:
- 200 (OK): запрос выполнен успешно.
- 400 (неверный запрос): что-то не так с запросом (отсутствующий параметр, ошибка синтаксического анализа ...).
- 404 (Не найдено): ресурс не соответствует предоставленному идентификатору.
Обработчик GET либо возвращает одно местоположение, если предоставлен идентификатор, иначе список всех существующих местоположений.
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))
Затем давайте создадим общую функцию для обработчиков POST и PUT, которые имеют очень похожее поведение. Единственное различие заключается в том, что запрос PUT с несуществующим идентификатором будет возвращать ошибку(таким образом, он будет только принимать запросы на обновление и отклонять запросы на создание), тогда как тот же запрос с методом POST будет успешным (будет создано новое местоположение, связанное с идентификатором).
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 ()
Регистрация служб
Наконец, мы регистрируем службы с помощью модуля Eliom_registration.Any, чтобы иметь полный контроль над отправляемым ответом. Таким образом, мы сможем отправить соответствующий код статуса HTTP в зависимости от того, что происходит во время обработки запроса (ошибка синтаксического анализа, ресурс не найден ...), как это показано выше при определении обработчиков.
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;
()
Полный исходник
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;
()
Источник: RESTful JSON API using Eliom
От переводчика
Хотелось что бы сообщество OCaml было больше и росло, а сам язык развивался быстрее, язык хороший, а местами даже лучше мэйнстримовых языков, вот несколько его плюсов: он собирается в натив, синтаксис у него довольно лаконичен и понятен(не сразу, но как по мне он легче дается, чем Haskell, но вообще это вкусовщина), также довольно удобная система типов и хорошее ООП конечно. Если этот перевод кому-то пригодился или заставил взглянуть на OCaml и его экосистему, попробовать его, то я могу делать еще переводы или авторские статьи. Об ошибках прошу сообщать в личку.
P.S.:
Вводные статьи про OCaml и Ocsigen на Хабре, с которыми возможно стоит ознакомиться новичкам:
- Введение в OCaml: The Basics [1]
- Введение в OCaml: Структура программ на OCaml [2]
- Введение в OCaml: Типы данных и сопоставление [3]
- Введение в OCaml: Нулевые указатели, утверждения и предупреждения [4]
- Динамические приложения с Ocsigen или Йоба возвращается
но конечно лучше ознакомиться с официальными мануалами, потому что статьям выше по 6-7 лет, какие-то основы из них извлечь конечно можно(а с учетом вялого развития языка вероятность извлечь базовые знания и не подорваться, стремится к 100%), но я не ручаюсь, что на данный момент там все правильно, особенно в статье про Oscigen. Всем добра и приятного пути в развитии.
Автор: nbytes