Skip to content

Ajouter une route ​

Cette page suit l'ajout d'une route de bout en bout, dans l'ordre où on l'écrit. Chaque étape a sa raison : en sauter une laisse un trou (une règle non vérifiée, une documentation fausse, un test absent).

Un exemple fictif

On ajoute GET /api/users/:id/last-clock : le dernier pointage d'une personne. Cette route n'existe pas dans l'API, elle sert d'exemple. Elle s'appuie sur une fonction qui existe déjà, Clocks.get_last_clock/1.

Le plan ​

1. Le métier : le contexte ​

Toute règle vit dans un contexte (lib/timemanager/). Ici, la fonction existe déjà :

elixir
# lib/timemanager/clocks.ex
def get_last_clock(user_id) do
  Clock
  |> where(user_id: ^user_id)
  |> order_by(desc: :time, desc: :id)
  |> limit(1)
  |> Repo.one()
end

Si la route avait besoin d'une nouvelle colonne : d'abord une migration (mix ecto.gen.migration), le champ dans le schéma, et docs/database.mmd (voir Base de données).

2. Les droits ​

Qui peut voir le dernier pointage de quelqu'un ? Les mêmes que pour ses heures : la règle existe déjà, :view_data_of (soi, ses gens, tous pour le GM). On la réutilise.

Pour une action vraiment nouvelle, on ajoute une clause dans lib/timemanager/authorization.ex, au bon endroit (l'ordre compte, voir Droits par rôle), et ses tests dans authorization_test.exs. On n'écrit jamais une vérification de rôle dans un contrôleur.

3. Le web : contrôleur, vue, route ​

Le contrôleur enchaîne les questions avec with, sans aucune règle à lui :

elixir
# lib/timemanager_web/controllers/clock_controller.ex
def last(conn, %{"id" => id}) do
  with {:ok, user_id} <- parse_id(id),
       :ok <- Authorization.authorize(conn.assigns.current_user, :view_data_of, user_id) do
    render(conn, :last, clock: Clocks.get_last_clock(user_id))
  end
end

Les erreurs ({:error, :forbidden}…) partent seules vers le FallbackController : rien à écrire.

La vue JSON choisit les champs envoyés. Elle réutilise data/1 :

elixir
# lib/timemanager_web/controllers/clock_json.ex
def last(%{clock: nil}), do: %{data: nil}
def last(%{clock: clock}), do: %{data: data(clock)}

La route, dans le groupe des routes authentifiées :

elixir
# lib/timemanager_web/router.ex
get "/users/:id/last-clock", ClockController, :last

4. Swagger ​

La description vit à part des contrôleurs, dans lib/timemanager_web/api_docs/. Les routes d'une ressource sont dans un module de paths/ :

elixir
# lib/timemanager_web/api_docs/paths/working_times.ex
"/api/users/{id}/last-clock" => %PathItem{
  get:
    operation("lastClock", "Clocks", "A person's last clock",
      description: "null if they never clocked.",
      parameters: [path_id(:id, "The person's number")],
      responses: %{200 => json("The last clock, or null", data(Clock))},
      errors: [401, 403]
    )
}

Un nouvel objet ? On ajoute son schéma dans api_docs/schemas/. Ici, Clock existe déjà.

5. Les tests ​

Le métier, si on y a touché (test/timemanager/). La route, dans test/timemanager_web/ : le succès, et chaque refus.

elixir
test "a manager sees the last clock of their people, not of others", %{conn: conn} do
  manager = active_user_fixture(%{}, "manager")
  employee = active_user_fixture()
  team_with_members_fixture(manager, [employee])
  {:ok, _} = Clocks.toggle_clock(employee.id)

  conn = log_in(conn, manager)
  assert %{"status" => true} = json_response(get(conn, ~p"/api/users/#{employee.id}/last-clock"), 200)["data"]

  stranger = active_user_fixture()
  assert json_response(get(conn, ~p"/api/users/#{stranger.id}/last-clock"), 403)
end

Puis toute la suite : mix test. Si la route n'est pas décrite dans Swagger, api_docs_test.exs échoue et le dit.

6. La documentation ​

QuoiOù
la route dans l'indexEndpoints
une nouvelle table ou colonnedocs/database.mmd, avec un commentaire pour chaque champ
un nouveau code d'erreurErreurs
une nouvelle règle du métierla page de la section Fonctionnel qui la concerne

La liste de vérification ​

  • [ ] La règle est dans un contexte, pas dans le contrôleur
  • [ ] Le droit passe par Authorization.authorize
  • [ ] La vue JSON choisit ses champs
  • [ ] Chaque refus a son code stable
  • [ ] La route est décrite dans Swagger
  • [ ] Les tests couvrent le succès et les refus
  • [ ] mix test passe en entier
  • [ ] Les fichiers touchés sont formatés (voir Conventions)
  • [ ] La documentation est à jour