Управление версиями API для маршрутов Rails


142

Я пытаюсь изменить версию своего API, как у Stripe. Ниже приведена последняя версия API - 2.

/api/users возвращает 301 в /api/v2/users

/api/v1/users возвращает индекс 200 пользователей в версии 1

/api/v3/users возвращает 301 в /api/v2/users

/api/asdf/users возвращает 301 в /api/v2/users

Так что в основном все, что не указывает версию, ссылается на последнюю, если указанная версия не существует, затем перенаправляется на нее.

Вот что у меня есть на данный момент:

scope 'api', :format => :json do
  scope 'v:api_version', :api_version => /[12]/ do
    resources :users
  end

  match '/*path', :to => redirect { |params| "/api/v2/#{params[:path]}" }
end

Ответы:


281

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

С тех пор я обновил ответ, чтобы использовать пространства имен и редирект 301, а не 302 по умолчанию. Спасибо pixeltrix и Bo Jeanes за подсказки по этим вопросам.


Возможно, вы захотите надеть действительно прочный шлем, потому что он взорвет ваш мозг .

API маршрутизации Rails 3 супер злой. Чтобы написать маршруты для вашего API в соответствии с вашими требованиями выше, вам понадобится только это:

namespace :api do
  namespace :v1 do
    resources :users
  end

  namespace :v2 do
    resources :users
  end
  match 'v:api/*path', :to => redirect("/api/v2/%{path}")
  match '*path', :to => redirect("/api/v2/%{path}")
end

Если после этого момента ваш разум еще не пострадал, позвольте мне объяснить.

Во-первых, мы вызываем namespaceэто очень удобно, когда вы хотите, чтобы группа маршрутов была привязана к определенному пути и модулю с одинаковыми именами. В этом случае мы хотим, чтобы все маршруты внутри нашего блока namespaceбыли привязаны к контроллерам внутри Apiмодуля, и все запросы к путям внутри этого маршрута будут иметь префикс api. Такие запросы, как /api/v2/users, знаете?

Внутри пространства имен мы определяем еще два пространства имен (воах!). На этот раз мы определяем в «v1» пространство имена, поэтому все маршруты для контроллеров здесь будут внутри V1модуля внутри Apiмодуля: Api::V1. Определив resources :usersвнутри этого маршрута, контроллер будет расположен по адресу Api::V1::UsersController. Это версия 1, и вы попадаете туда, выполняя запросы вроде /api/v1/users.

Версия 2 является лишь крошечной немного по- другому. Вместо того, чтобы обслуживающий его контроллер Api::V1::UsersController, теперь он находится в Api::V2::UsersController. Вы попадаете туда, делая запросы вроде /api/v2/users.

Далее используется a match. Это будет соответствовать всем маршрутам API, которые относятся к таким вещам, как /api/v3/users.

Это та часть, которую я должен был найти. Эта :to =>опция позволяет вам указать, что конкретный запрос должен быть перенаправлен куда-то еще - я это знал, но я не знал, как заставить его перенаправить куда-то еще и передать вместе с ним часть исходного запроса. .

Для этого мы вызываем redirectметод и передаем ему строку со специально-интерполированным %{path}параметром. Когда приходит запрос, соответствующий этому финалу match, он интерполирует pathпараметр в местоположение %{path}внутри строки и перенаправляет пользователя туда, куда ему нужно перейти.

Наконец, мы используем другой matchдля маршрутизации всех оставшихся путей с префиксом /apiи перенаправления на них /api/v2/%{path}. Это означает, что такие запросы /api/usersбудут отправлены в /api/v2/users.

Я не мог понять, как добиться /api/asdf/usersсовпадения, потому что как определить, должен ли это быть запрос к /api/<resource>/<identifier>или /api/<version>/<resource>?

В любом случае, это было интересно исследовать, и я надеюсь, что это поможет вам!


24
Дорогой Райан Бигг. Вы молодец.
maletor 09

18
Репутация Рубинового героя не измеряется просто.
Waseem

1
Райан ... Я не думаю, что это правда. В этом случае / api и / api / v2 будут обслуживать одно и то же содержимое вместо одного канонического URL. / api должен перенаправлять на / api / v2 (как указал исходный автор). Я ожидал, что правильные маршруты будут выглядеть примерно так, как gist.github.com/2044335 ( конечно , я этого не тестировал). Только / api / v [12] должен возвращать 200, / api и / api / <bad version> должны возвращать 301 секунду в / api / v2
Bo Jeanes

2
Стоит отметить, что в файле маршрутов 301 было сделано перенаправление по умолчанию и не зря. От гидов: Please note that this redirection is a 301 “Moved Permanently” redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible.
maletor

3
Разве он не создает бесконечные перенаправления, если путь неверен? Например, запрос / api / v3 / path_that_dont_match_the_routes создаст бесконечное перенаправление, верно?
Робин

38

Еще пара вещей:

Ваше совпадение перенаправления не будет работать для определенных маршрутов - *apiпараметр жадный и поглотит все, например /api/asdf/users/1, перенаправит на /api/v2/1. Вам лучше использовать обычный параметр, например :api. По общему признанию, это не будет соответствовать таким случаям, как, /api/asdf/asdf/users/1но если у вас есть вложенные ресурсы в вашем api, это лучшее решение.

Райан, ПОЧЕМУ МЫ НЕ НРАВИТСЯ namespace? :-), например:

current_api_routes = lambda do
  resources :users
end

namespace :api do
  scope :module => :v2, &current_api_routes
  namespace :v2, &current_api_routes
  namespace :v1, &current_api_routes
  match ":api/*path", :to => redirect("/api/v2/%{path}")
end

Который имеет дополнительное преимущество версионных и универсальных именованных маршрутов. Еще одно замечание - соглашение при использовании :moduleзаключается в использовании обозначения подчеркивания, например: api/v1не «Api :: V1». В какой-то момент последнее не сработало, но я считаю, что это было исправлено в Rails 3.1.

Кроме того, когда вы выпускаете v3 своего API, маршруты будут обновлены следующим образом:

current_api_routes = lambda do
  resources :users
end

namespace :api do
  scope :module => :v3, &current_api_routes
  namespace :v3, &current_api_routes
  namespace :v2, &current_api_routes
  namespace :v1, &current_api_routes
  match ":api/*path", :to => redirect("/api/v3/%{path}")
end

Конечно, вполне вероятно, что ваш API имеет разные маршруты между версиями, и в этом случае вы можете сделать это:

current_api_routes = lambda do
  # Define latest API
end

namespace :api do
  scope :module => :v3, &current_api_routes
  namespace :v3, &current_api_routes

  namespace :v2 do
    # Define API v2 routes
  end

  namespace :v1 do
    # Define API v1 routes
  end

  match ":api/*path", :to => redirect("/api/v3/%{path}")
end

Как бы вы поступили с последним делом? т.е. /api/asdf/users?как и /api/users/1? Я не мог понять этого в моем обновленном ответе, поэтому решил, что вы можете знать способ
Райан Бигг

Нет простого способа сделать это - вам нужно будет определить все перенаправления до того, как перехватить все, но вам нужно будет сделать каждое только для каждого родительского ресурса, например / api / users / * path => / api / v2 / users /% {path}
pixeltrix

13

Если это вообще возможно, я бы предложил переосмыслить ваши URL-адреса, чтобы версия не была в URL-адресе, а была помещена в заголовок accept. Этот ответ о переполнении стека хорошо подходит:

Лучшие практики для управления версиями API?

и эта ссылка показывает, как именно это сделать с маршрутизацией рельсов:

http://freelancing-gods.com/posts/versioning_your_ap_is


Это также отличный способ сделать это, и он, вероятно, также будет обслуживать запрос "/ api / asdf / users".
Райан Бигг

9

Я не большой поклонник версионирования по маршрутам. Мы создали VersionCake для поддержки более простой формы управления версиями API.

Включая номер версии API в имя файла каждого из наших соответствующих представлений (jbuilder, RABL и т. Д.), Мы сохраняем ненавязчивое управление версиями и допускаем легкую деградацию для поддержки обратной совместимости (например, если v5 представления не существует, мы рендер v4 вида).


8

Я не уверен, почему вы хотите перенаправить на конкретную версию, если версия не запрашивается явно. Похоже, вы просто хотите определить версию по умолчанию, которая будет обслуживаться, если версия не запрашивается явно. Я также согласен с Дэвидом Боком в том, что сохранение версий вне структуры URL-адресов - более чистый способ поддержки управления версиями.

Бесстыдный плагин: Versionist поддерживает эти (и другие) варианты использования.

https://github.com/bploetz/versionist


2

Ответ Райана Бигга сработал для меня.

Если вы также хотите сохранить параметры запроса через перенаправление, вы можете сделать это следующим образом:

match "*path", to: redirect{ |params, request| "/api/v2/#{params[:path]}?#{request.query_string}" }

2

Реализовал это сегодня и нашел то, что я считаю «правильным» на RailsCasts - управление версиями REST API . Так просто. Так что ремонтопригодный. Так эффективно.

Добавить lib/api_constraints.rb(даже не нужно изменять vnd.example.)

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options[:default]
  end

  def matches?(req)
    @default || req.headers['Accept'].include?("application/vnd.example.v#{@version}")
  end
end

Настроить config/routes.rbтак

require 'api_constraints'

Rails.application.routes.draw do

  # Squads API
  namespace :api do
    # ApiConstaints is a lib file to allow default API versions,
    # this will help prevent having to change link names from /api/v1/squads to /api/squads, better maintainability
    scope module: :v1, constraints: ApiConstraints.new(version:1, default: true) do
      resources :squads do
        # my stuff was here
      end
    end
  end

  resources :squads
  root to: 'site#index'

Отредактируйте свой контроллер (т.е. /controllers/api/v1/squads_controller.rb)

module Api
  module V1
    class SquadsController < BaseController
      # my stuff was here
    end
  end
end

Затем вы можете изменить все ссылки в своем приложении с /api/v1/squadsна, /api/squadsи вы сможете ЛЕГКО реализовать новые версии API, даже не меняя ссылки.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.