Привет, Хабр! Представляю вашему вниманию перевод руководства RESTful JSON API using Eliom.
В этом руководстве рассказывается, как создать простой, но полный REST API с использованием JSON в качестве формата сериализации.
Чтобы проиллюстрировать наш пример, предположим, что мы хотим предоставить доступ к базе данных местоположений, хранящих описание и координаты (широта и долгота).
Чтобы быть RESTful, наш интерфейс будет соответствовать следующим принципам:
Имея это в виду, наша цель будет заключаться в реализации функций CRUD (Create, Read, Update, Delete) для обработки наших ресурсов. Мы хотим, чтобы следующие запросы были действительными:
GET http:// localhost/ вернет все доступные местоположения.
GET http:// localhost/ID вернет местоположение, связанное с ID.
POST http:// localhost/ID с содержимым:
сохранит это местоположение в базе данных.
PUT http:// localhost/ID, с некоторым содержимым, обновит местоположение, связанное с идентификатором.
DELETE http:// localhost/ID удалит местоположение, связанное с ID.
Предполагается что вы уже знакомы с Eliom, это нужно что бы понять туториал полностью. Этот туториал не является введением в Eliom.
Следующие браузерные расширения могут быть полезны для ручной проверки REST API:
Начнем с определения наших типов баз данных, то есть того, как мы будем представлять наши местоположения и связанную с ними информацию. Каждое местоположение будет связано с уникальным и произвольным идентификатором, а так же будет содержать следующую информацию: описание и координаты (состоящие из широты и долготы).
Мы представляем координаты с десятичными градусами и используем библиотеку deriving-yojson для анализа и сериализации наших типов в JSON.
Мы используем выделенный тип ошибки, возвращаемый, когда что-то не так с запросом или с обработкой запроса.
Что касается базы данных, мы используем простую таблицу Ocsipersist.
Во-первых, давайте определим общие параметры обслуживания:
Следующий шаг — определить наши API службы. Мы определяем четыре из них с одним и тем же путем, используя четыре метода HTTP в нашем распоряжении:
Давайте начнем определение обработчиков с помощью нескольких вспомогательных значений и функций, используемых обработчиками.
Поскольку мы используем функцию низкого уровня Eliom_registration.String.send для отправки нашего ответа, мы переносим его на три специализированные функции: send_json, send_error и send_success (эта отправляет только код состояния 200 OK без какого-либо содержимого).
Другая функция помогает нам проверить, что полученный тип содержимого является ожидаемым, сопоставляя его с MIME-типом. В нашем примере проверим, что мы получаем JSON.
Функция read_raw_content извлекает указанное или стандартное length количество символов из потока Ocsigen raw_content.
Затем мы определяем наши обработчики для выполнения необходимых действий и возврата ответа.
Обработчики POST и PUT будут считывать содержимое исходного контента в JSON и использовать Yojson для преобразования его в наши типы.
В ответах мы используем коды состояния HTTP, с значениями:
Обработчик GET либо возвращает одно местоположение, если предоставлен идентификатор, иначе список всех существующих местоположений.
Затем давайте создадим общую функцию для обработчиков POST и PUT, которые имеют очень похожее поведение. Единственное различие заключается в том, что запрос PUT с несуществующим идентификатором будет возвращать ошибку(таким образом, он будет только принимать запросы на обновление и отклонять запросы на создание), тогда как тот же запрос с методом POST будет успешным (будет создано новое местоположение, связанное с идентификатором).
Для удаления местоположений нужен четвертый обработчик:
Наконец, мы регистрируем службы с помощью модуля Eliom_registration.Any, чтобы иметь полный контроль над отправляемым ответом. Таким образом, мы сможем отправить соответствующий код статуса HTTP в зависимости от того, что происходит во время обработки запроса (ошибка синтаксического анализа, ресурс не найден ...), как это показано выше при определении обработчиков.
Хотелось что бы сообщество OCaml было больше и росло, а сам язык развивался быстрее, язык хороший, а местами даже лучше мэйнстримовых языков, вот несколько его плюсов: он собирается в натив, синтаксис у него довольно лаконичен и понятен(не сразу, но как по мне он легче дается, чем Haskell, но вообще это вкусовщина), также довольно удобная система типов и хорошее ООП конечно. Если этот перевод кому-то пригодился или заставил взглянуть на OCaml и его экосистему, попробовать его, то я могу делать еще переводы или авторские статьи. Об ошибках прошу сообщать в личку.
P.S.:
Вводные статьи про OCaml и Ocsigen на Хабре, с которыми возможно стоит ознакомиться новичкам:
но конечно лучше ознакомиться с официальными мануалами, потому что статьям выше по 6-7 лет, какие-то основы из них извлечь конечно можно(а с учетом вялого развития языка вероятность извлечь базовые знания и не подорваться, стремится к 100%), но я не ручаюсь, что на данный момент там все правильно, особенно в статье про Oscigen. Всем добра и приятного пути в развитии.
В этом руководстве рассказывается, как создать простой, но полный 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; ()
Полный исходник
Всё что у нас получилось в итоге
Источник: RESTful JSON API using Eliom
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. Всем добра и приятного пути в развитии.