Нельзя просто так взять и сделать версионирование API
Transcript of Нельзя просто так взять и сделать версионирование API
НЕЛЬЗЯ ПРОСТО ТАК ВЗЯТЬ И СДЕЛАТЬВЕРСИОНИРОВАНИЕ API
github.com/ikalnytskyi
МОТИВАЦИЯЗабота о пользователях
Tweetbot
Gwibber
TweetDeck
Twitter API
IFTTT.com
RememberTheMilk.com
МОТИВАЦИЯЗабота о пользователяхОбнаружение поддерживаемого функционала
{ "owner": "John Doe", "product": { ... } }
{ "owner": { "firstName": "John", "lastName": "Doe" }, "product": { ... } }
{ "owner": { "firstName": "John", "lastName": "Doe", "phone": "+1 234 567 89" }, "product": { ... } }
import requests
response = requests.get('https://api.not-ebay.com/sales') entity = response.json()
if isinstance(entity['owner'], dict): if 'phone' in entity['owner']: # Последняя версия API, предоставляем весь # полный набор функционала. else: # Предпоследняя версия API, предоставляем # ограниченный набор функционала. else: # Боль. :'( Самая старая версия API, предоставляем # только базовый набор функционала.
МОТИВАЦИЯЗабота о пользователяхОбнаружение поддерживаемого функционалаОбновление с минимальным временем простоя
Сервер #1
nova-api
nova-conductor
nova-scheduler
python-novaclient
nova-compute
Сервер #2
nova-api
nova-conductor
nova-scheduler
python-novaclient
nova-compute
Сервер #3
nova-api
nova-conductor
nova-scheduler
python-novaclient
nova-compute
Коммуникатор(Load Balancer, AMQP, etc)
СПОСОБЫ ВЕРСИОНИРОВАНИЯ HTTP APIВерсионирование при помощи URI
GET /v1.42/resource HTTP/1.1 Host: api.example.com
СПОСОБЫ ВЕРСИОНИРОВАНИЯ HTTP APIВерсионирование при помощи URIВерсионирование при помощи параметра запроса
GET /resource?version=1.42 HTTP/1.1 Host: api.example.com
СПОСОБЫ ВЕРСИОНИРОВАНИЯ HTTP APIВерсионирование при помощи URIВерсионирование при помощи параметра запросаВерсионирование при помощи HTTP заголовка
GET /resource HTTP/1.1 Host: api.example.com Api-Version: 1.42 Vary: Api-Version
СПОСОБЫ ВЕРСИОНИРОВАНИЯ HTTP APIВерсионирование при помощи URIВерсионирование при помощи параметра запросаВерсионирование при помощи HTTP заголовкаВерсионирование при помощи Content Negotiation
GET /resource HTTP/1.1 Host: api.example.com Accept: application/vnd.project+json; version=1.42
ВЕРСИОНИРОВАНИЕ ПРИ ПОМОЩИ CONTENTNEGOTIATION
Заголовок Accept и RFC 7231
GET /resource HTTP/1.1 Host: api.example.com Accept: application/xml; q=0.2, application/json, application/* Accept: application/vnd.project+json; version=1; q=0.7 Accept: application/vnd.project+json; q=0.9; version=2
ВЕРСИОНИРОВАНИЕ ПРИ ПОМОЩИ CONTENT NEGOTIATION
Content-Type дилемма
POST /resource HTTP/1.1 Host: api.example.com Accept: application/vnd.project+json; version=1 Content-Type: ???
{ "owner": "John Doe", "product": { ... } }
POST /resource HTTP/1.1 Host: api.example.com Accept: application/vnd.project+json; version=1 Content-Type: application/vnd.project+json; version=1
{ "owner": "John Doe", "product": { ... } }
POST /resource HTTP/1.1 Host: api.example.com Accept: application/vnd.project+json; version=1 Content-Type: application/vnd.project+json; version=2
{ "owner": { "firstName": "John", "lastName": "Doe" }, "product": { ... } }
POST /resource HTTP/1.1 Host: api.example.com Accept: application/vnd.project+json; version=1 Content-Type: application/json
{ "owner": "John Doe", "product": { ... } }
Согласно :RFC 7231
Заголовок Accept определяет предпочтения по типупредставления ответа от сервера, включаязапрашиваемый ресурс и возможные ошибки.
Заголовок Content-Type определяет типпредставления передаваемого в теле сообщенияданных.
ОХ.. МОЖЕТ ПОПРОБОВАТЬ HTTP ЗАГОЛОВОК?
ВЕРСИОНИРОВАНИЕ ПРИ ПОМОЩИ HTTPЗАГОЛОВКА
Какой выбрать код ответа если запрашиваемаяверсия API не найдена?
400 Bad Request404 Not Found406 Not Acceptable412 Precondition Failed
Убедиться, что выбранный заголовок не фильтруетсяиспользуемым стеком технологий.
ХМ.. А ЧТО С ПАРАМЕТРОМ ЗАПРОСА?
ВЕРСИОНИРОВАНИЕ ПРИ ПОМОЩИПАРАМЕТРА ЗАПРОСА
Традиционно используются совместно с методомGET.
Некоторые веб-фреймворки могут быть не готовы ктакому порядку вещей.
django 1.10
@require_http_methods(['POST']) def create_user(request): version = request.GET.get('version', LATEST_VERSION)
ОК, ПОПРОБУЕМ URI
ВЕРСИОНИРОВАНИЕ ПРИ ПОМОЩИ URI
Не требует никаких специальных возможностейфреймворка. В простом случае, каждая версия -отдельный обработчик.
Самый популярный способ версионирования HTTPAPI.
ВЕРСИОНИРОВАНИЕ И ВЕБ-ФРЕЙМВОРКИ
Фреймворки не решают проблему версионированияAPI.
В лучшем случае существуют расширения кфреймворкам, которые позволяют автоматическиизвлечь версию и передать ее в обработчик.
djangorestframework 3.5.3
from rest_framework import views, versioning
class CreateUser(views.APIView):
versioning_class = versioning.QueryParameterVersioning
def post(self, request, format=None): if request.version == '42': pass
DIY: ВЕРСИОНИРОВАНИЕ И ВЕБ-ФРЕЙМВОРКИ
Подход к версионированию тесно связан с подходомк организации кода.
Один обработчик принимающий версию API?Много обработчиков вызываемых в зависимости отверсии?
В сочетании со способами передачи версииклиентом, имеем немалое количество вариантов.
Решение задачи обычно сводится к написаниюсобственного middleware или роутера.
ЭВОЛЮЦИЯ ДАННЫХ
Эволюция API - это следствие эволюции данных.
Расширение функциональности зачастую ведет кэволюции данных.
database = [ {'owner': 'John Doe', 'product': some_product_data}, ]
@app.route('/v1/sales') def get_sales_v1(): return jsonify(database)
database = [ {'owner': {'firstName': 'John', 'lastName': 'Doe'}, 'product': some_product_data}, ]
@app.route('/v1/sales') def get_sales_v1(): conv_owner = lambda o: '{firstName} {lastName}'.format(**o) return jsonify([ {'owner': conv_owner(record['owner']), 'product': record['product']} for record in database ])
@app.route('/v2/sales') def get_sales_v2(): return jsonify(database)
ЭВОЛЮЦИЯ ТЕСТОВВместе с эволюцией данных, следует эволюция тестов.
Каждая версия API должна быть покрыта тестамидабы гарантировать что все работает так как иработало, а формат запроса/выдачи не поменялся.
Изменение схемы данных требует адаптациифейковых данных в тестах.
По возможности не использовать mock притестировании версий API.
# Актуальный формат: # # {'owner': # {'firstName': 'John', # 'lastName': 'Doe'}, # 'product': some_product_data}, # _database = [ {'owner': 'John Doe', 'product': some_product_data}, ]
@mock.patch('project.database', _database) def test_get_sales_v1(app): with app.test_client() as c: assert c.get('/v1/sales').json() == { 'owner': 'John Doe', 'product': some_product_data, }
ЭВОЛЮЦИЯ БИЗНЕС ЛОГИКИ
Разные версии API могут требовать разной версиибизнес логики.
Необходимо быть готовым к наличию несколькихальтернативных реализаций одной и той же функции.
Следуя принципу DRY, очень легко оказаться в адунаследований, стратегий и сложных интерфейсов.Альтернатива – copy-paste подход.
def assign_ips_lt_5_0(...): pass
def assign_ips_eq_5_0(...): pass
def assign_ips_gt_6_1(...): pass
ВЫВОДЫИх нет. Каждый их должен сделать сам. :)
МОИ ВЫВОДЫ
Наличие нескольких версий API существенноувеличивает цену поддержки.
Не поддерживать все множество существующихверсий. Выбрать стратегию. Например, поддерживатьпоследние N версий.
Подумать об отказе от версионирования в случаезакрытого продукта, если нет требований кобновлению с минимальным временем простоя.
Использовать HTTP заголовок для версионированиякак самый простой и гибкий способ.
Хранить тесты на каждую поддерживаемую версиюAPI. Не использовать mock.
СПАСИБО ЗА ВНИМАНИЕ!ВОПРОСЫ?