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à :
# 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()
endSi 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 :
# 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
endLes erreurs ({:error, :forbidden}…) partent seules vers le FallbackController : rien à écrire.
La vue JSON choisit les champs envoyés. Elle réutilise data/1 :
# 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 :
# lib/timemanager_web/router.ex
get "/users/:id/last-clock", ClockController, :last4. 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/ :
# 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.
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)
endPuis 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
| Quoi | Où |
|---|---|
| la route dans l'index | Endpoints |
| une nouvelle table ou colonne | docs/database.mmd, avec un commentaire pour chaque champ |
| un nouveau code d'erreur | Erreurs |
| une nouvelle règle du métier | la 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 testpasse en entier - [ ] Les fichiers touchés sont formatés (voir Conventions)
- [ ] La documentation est à jour