Правильный способ реализации версий API с помощью active_model_serializers

Я знаю, что уже есть некоторые вопросы, а также это открытая проблема, связанная с тем, что AMS не слишком эффективно обрабатывает пространства имен. (который используется при таком подходе к управлению версиями), но я хотел быть уверенным, что нахожусь на правильном пути в рамках текущих ограничений.

Сейчас я использую Rails 5 и AMS 0.10.1, поэтому я сделал следующее:

# config/initializers/active_model_serializer.rb
ActiveModelSerializers.config.serializer_lookup_enabled = false

отключить поиск сериализатора по умолчанию (который все равно не работал); и

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  def get_serializer(resource, options = {})
    unless options[:each_serializer] || options[:serializer] then
      serializer = (self.class.name.gsub("Controller","").singularize + "Serializer").constantize
      resource.respond_to?(:to_ary) ? options[:each_serializer] = serializer : options[:serializer] = serializer
    end
    super(resource, options)
  end
end

переопределить способ поиска сериализаторов по умолчанию; мои контроллеры и сериализатор такие:

# app/controllers/api/v2/api_controller.rb
module Api::V2
  class ApiController < ApplicationController
    ...

# app/controllers/api/v2/users_controller.rb
module Api::V2
  class UsersController < ApiController
    ...

и

# app/serializers/api/v2/user_serializer.rb
module Api::V2
  class UserSerializer < ActiveModel::Serializer
    ...    

Теперь такие вещи, как ActiveModel::Serializer.serializer_for(object), не будут работать, поэтому мне также пришлось исправлять мои спецификации запроса, используя example.metadata[:api_version], чтобы установить версию API перед каждым тестом, а также поднять и выдать ошибку, если в примере это не установлено.

So:

  1. Есть ли лучший способ задокументировать?
  2. Это близко к правильному?
  3. Буду ли я сталкиваться с проблемой при таком подходе?
  4. как это может быть улучшено?

person chrisandrew.cl    schedule 23.06.2016    source источник


Ответы (2)


Я думаю, что то, что у вас есть здесь, в порядке. Я использую тот же подход, и он отлично работает для моего приложения. Я взял оригинальную идею здесь от Райана Бейтса, где он объясняет очень похожий подход.

http://railscasts.com/episodes/350-rest-api-versioning

Это то, что я использую для указания разных сериализаторов для каждого ресурса:

module API
  module V3
    class AssetController < API::V3::ApiController
      def index
        render json: assets, status: :ok, each_serializer: API::V3::Serializers::AssetSerializer
      end
  end
end

В моей реализации я использую сериализаторы внутри api/controllers/api/v3/serializers. Итак, вы управляете версиями классов сериализаторов и классов контроллеров.

Не уверен, что вам действительно нужно иметь get_serializer, так как это более явно, но не имеет большого значения.

Если у вас много конечных точек API, попробуйте организовать их в ресурсах. В моем config/routes.rb у меня около 700 ресурсов, поэтому я разделил их на отдельные файлы config/api/v1/routes.rb...

namespace :api, defaults: {format: 'json'} do
  namespace :v1
    resources :assets
  end
end

Также это удобно делать внутри инициализатора inflections.rb

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym 'API'
end

Для меня я бы сказал, что самая важная проблема — это хорошее покрытие тестами. Я предпочитаю спецификацию и проверяю правильные коды состояния 200, 201,... и т. д., а также правильный вывод сына с помощью json_schema.

Если вам нужно выполнить аутентификацию, я бы посоветовал вам использовать аутентификацию на основе токенов и JWT - JSON Web Token. В моей реализации я использую два токена. Один токен для чтения и другой токен при выполнении POST и PATCH (возможно, не уверен, что это нужно). так что внутри контроллера API что-то вроде этого

class ApiController < ActionController::Base
  skip_before_action :verify_authenticity_token, if: :json_request?
  before_action :authenticate

  protected
  def json_request?
    request.format.json?
  end
  if request.headers['X-Authorization']
    token = request.headers['X-Authorization']
    payload = JWT.decode(token, 'my_custom_key_to_check_if_key_has_been_tempered d_on_client_side')[0]
  end
end
person Mirza Memic    schedule 23.06.2016
comment
Спасибо за ваш вклад! Я сделал это, чтобы немного высушить вещи. get_serializer всегда вызывается AMS, и мне нужно использовать render json: @object, serializer: Namespaced::Unconventional::ObjectSerializer только в том случае, если это не соответствует определенному соглашению. - person chrisandrew.cl; 24.06.2016

Поскольку я не нашел лучшего способа, ни документированного, ни нигде, он также кажется правильным, и я не сталкивался с проблемами через некоторое время, используя его, это кажется хорошим подходом для управления версиями API.

В любом случае, я советую соблюдать осторожность при использовании этого подхода, чтобы не изменить поведение старых поддерживаемых версий вашего API. Тщательно протестируйте и уведомите своих клиентов об устаревании и удалении поддержки для старых версий.

person chrisandrew.cl    schedule 01.12.2016