пятница, 24 января 2014 г.

REST под Android. Часть 2: разрабатываем API

Первая часть статьи была посвящена вопросу реализации клиентского REST-приложения под Android. Корректная реализация требует использования одного из паттернов, предложенных Virgil Dobjanschi. Существует ряд подходящих библиотек, способных облегчить эту задачу. Наиболее интересной из них мне показалась RoboSpice.

Прежде чем переходить к разработке тестового проекта на RoboSpice, необходимо определиться с API для тестового проекта. Разумеется, можно взять одно из готовых API - благо их сейчас сотни. Однако гораздо интереснее написать свое, тем более, что это нетривиальный процесс. Рассмотрим, как пишутся REST API, какие инструменты для этого существуют, и попробуем реализовать простейшее тестовое API.

Принципы разработки REST API

Apigee

Если вы собираетесь разрабатывать REST API самостоятельно, то первым делом загляните на сайт apigee.com. Здесь собрана масса полезной информации на эту тему. В первую очередь - книги:

Там же на отдельной странице собраны примеры готовых REST API (github, twitter и т.д.). Специальная консоль позволяет поработать с выбранным API прямо из браузера.

Другие ресурсы

Тестовый пример: набор книг

Рассмотрим простую задачу - нужно разработать REST API для набора книг. Ресурс, для простоты, будет только один - книга (book). Что нам требуется от API:
  • [1] получить полный список книг (с пагинацией).
  • [2] получить информацию о книге с заданной степенью детализации;
  • [3] добавить книгу в набор;
  • [4] удалить книгу из набора;
  • [5] редактировать информацию о книге, например, уточнить ФИО автора;
  • [6] провести поиск книг по автору или названию (с пагинацией);

Первая попытка описания API: список URL в текстовом файле

В первом приближении, наше API можно записать в виде простого списка HTTP-запросов, которые клиент может посылать на сервер. Учитывая рекомендации из книги "Web API Design. Crafting Interfaces that Developers Love", получаем такие запросы:
//[1] получить полный список книг
GET /v1/books
GET /v1/books?limit=10&offset=30 //поиск с пагинацией; получаем максимум 10 результатов, начиная с 30-ой позиции

//[2] получить информацию о книге 
GET /v1/books/[book_id]
GET /v1/books/[book_id]?fields=title,author,description //явно указываем, какие поля нам нужны

//[3] добавить книгу в набор
POST /v1/books

//[4] удалить книгу из набора 
DELETE /v1/books/[book_id]

//[5] редактировать информацию о книге
PATCH /v1/books/[book_id] //заменяем только выбранные поля переданными данными

//[6] провести поиск книг по автору, по названию
GET /v1/books?[book_id]?author=John //поиск по автору; по умолчанию выводится первые 10 результатов
GET /v1/books?[book_id]?author=John&limit=25&offset=50  //поиск по автору с пагинацией
GET /v1/books?[book_id]?title=hero&?fields=title,author //поиск по названию, явно указываем, какие поля нам нужны
Для простоты считаем, что наши клиенты поддерживают все необходимые HTTP-методы. На практике, некоторые клиенты могут не поддрживать PUT, DELETE и, тем более, PATCH, так что могут потребоваться альтернативные варианты API, например, вместо PUT /v1/books/[book_id] - POST /v1/books/[book_id].patch. Тег v1 задает версию нашего API.

Договоримся, что для формат представления ресурсов можно задавать следующим образом:

GET /v1/books //json по умолчанию
GET /v1/books.xml //получить список книг в xml
GET /v1/books/[book_id].proto //получить информацию о книге в формате protobuffer

Теперь зафиксируем способ обработки ошибок. Будем считать, что в случае успеха сервер возвращает код 200 - OK, в случае ошибки один из следующих кодов: 400 - Bad Request, 500 - Internal Server Error. Для наших целей вполне достаточно, хотя можно задействовать и другие коды.

В случае ошибки сервер обязан вернуть сообщение с расшифровкой ошибки в формате:

{"code" : 401, "message": "book wasn't found"}

Осталось описать формат представления ресурса и списков ресурсов.

  • [1] получить полный список книг
    GET /books
    Response 200 OK 
    {"books": [{"book":{ "id":"1234", "title": "Война и мир", "author": "Л.Н. Толстой", "released": "1868"}}
     , {"book":{ "id":"1235", "title":"Господин Ау", "author", "Э. Успенский",  "released" : "1980"}}]
    "_metadata": [{"totalCount":250,"limit":10,"offset":0}]
    "_links" : [
    {"rel": "next", "href": "/v1/books?offset=30&limit=10}, 
    {"rel": "prev", "href": "/v1/books?offset=10}, 
    {"rel": "first","href": "/v1/books?offset=0}, 
    {"rel": "last", "href": "/v1/books?offset=240}
    ]}
    
  • [2] получить информацию о книге
    GET /books/1234
    
    Response 200 OK 
    
    {"book":{ "id":"1234", "title": "Война и мир", "author": "Л.Н. Толстой", "released": "1868" }}
    
  • [3] добавить книгу в набор
    POST /v1/books
    title="Желтый туман"&author="Александр Волков"
    
    Response 200 OK 
    {"book":{ "id":"1236", "title": "Желтый туман", "author": "Александр Волков", "released" : ""}}
    
  • [4] удалить книгу из набора
    DELETE /v1/books/[book_id]
    
    Response 200 OK 
    
  • [5] редактировать информацию о книге
    PATCH /v1/books/1236 
    released="1970"
    
    Response 200 OK 
    {"book":{ "id":"1236", "title": "Желтый туман", "author": "Александр Волков", "released": "1970"}}
    
    
  • [6] провести поиск книг по автору, по названию - аналогично [1]

Инструменты для описания API

Вариант описания API в виде текстового файла вполне жизнеспособен, но далеко не идеален. Основная проблема в том, что в этом случае реализация и документация API неминуемо разойдутся. Решений несколько:
  • [1] Описать API, воспользовавшись одним из специальных языков. Далее, на основе описания, автоматически генерировать документацию и код реализации, проверять корректность и полноту реализацию API и т.д.
  • [2] Описать API в коде сервера, генерировать актуальную документацию на основе кода.
  • [3] Воспользоваться подходом Hypermedia as the Engine of Application State (HATEOAS) и вообще обойтись без описания API. Такой подход подразумевает, что клиент изначально знает лишь адрес сервера, и узнает все ссылки для доступа к конкретным ресурсам в процессе навигации. Для его реализации можно использовать обобщенные языки описания API типа HAL, Siren. В данной статье подход HATEOAS рассматриваться не будет.
Подходящий инструментарий для [1] и [2]:
  • apiary.io. Бесплатный сервис, который позволяет разрабатывать REST API на языке API Blueprint Language. На основе такого API можно:
    • генерировать документацию;
    • проверить реализацию на соответствие API; проверка выполняется следующим образом: сервер Apiari используется как отладочный прокси-сервер, который пропускает через себя весь трафик к серверу и от сервера, сравнивает запросы и результаты с API и сообщает о найденных ошибках.
    Приятные "фишки" сервиса:
    • сервис может работать как сервер-заглушка (Server Mock), так что с API можно будет попробовать работать сразу, не реализуя реальную серверную часть;
    • сервис поддерживает отладку запросов (в разделе "Documentation" возле каждого запроса есть кнопочка Try it).
    • сервис плотно интегрирован с GitHub.
  • http://www.apihub.com/. Альтернативный вариант, использующий для описания REST API язык RESTful API Modeling Language (RAML). Сервис платный, с возможностью бесплатно разработать одно API.
  • http://enunciate.codehaus.org - Java + JAX-RS аннотации - для тех, кто разрабатывает сервер на основе JAX-RS.
  • Самописные велосипеды;
  • и т.д.

Вторая попытка описания API: Apiary.io

Воспользуемся сервисом apiari.io, и перепишем наше API, используя документацию по API Blueprint Language и образцы готовых API (1, 2).

Результат можно посмотреть на GitHub и на Apiary.

А вот результаты работы Mock-сервера Apiary:

В целом, работа с API Blueprint Language не вызвала каких-то особых проблем. Смутил только один момент, с которым так и не удалось разобраться. Я создал ресурс Book:

## Book [/v1/books/{id}{?fields}]
и действия к нему
### Retrieve a Single Book [GET]
...
### Edit a Book [PATCH]
...
### Delete a Book [DELETE]
...
Параметр fields может использоваться только с GET, а в PATCN и DELETE его быть не может. Набор параметров я задаю отдельно для каждого действия. Тем не менее, в документации для Edit и Delete параметр fields присутствует в URL, что несколько сбивает с толку. Причем ниже, в списке возможных параметров, он отсутствует.
Несколько неудобно, хотя не слишком критично.

Выводы

  • Прежде чем разрабатывать собственное REST API, имеет смысл ознакомиться как минимум с Web API Design. Crafting Interfaces that Developers Love (pdf), чтобы не наделать грубых ошибок.
  • Сервис apiary.io - весьма функционален. Разрабатывать REST API на нем гораздо удобнее, чем в просто фиксировать список URL в текстовом файле. Возможность проверки реализации на соответствие спецификации и Mock-сервер стоят того, чтобы потратить время на изучение API Blueprint Language.
Цель достигнута - BooksSet REST API для тестового проекта создано. Переходим к разработке проекта. Об этом - в следующий раз.

Mindmap, созданный в процессе работы над этой статьей, можно скачать здесь, в формате Freemind 1.0 (портативная версия: freemind-bin-1.0.0.zip).

Update Mock-сервер Apiary не поддерживает компрессию gzip. Может когда-нибудь сделают, особенно если за эту функцию проголосует достаточное количество пользователей.

Комментариев нет:

Отправить комментарий