← Оглавление

7.4 Правильный ящик

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

Что нужно

На данный момент обработчики работают с вектором напрямую: add считает идентификатор вручную, done итерирует вектор сам. TaskStore из tq_core умеет и то и другое - и делает это за своей дверью: .add() держит счётчик, .get_mut() ищет по номеру и возвращает ошибку если не нашёл. Как именно ищет - устройство хранилища, не дело обработчика.

Один шаг: заменить Vec<Task> на TaskStore в SharedStore. Обработчики получат готовые методы - ручная логика уйдёт туда, где ей место.

Постройка

В crates/api/src/routes.rs меняются импорты. Store - трейт, в котором объявлены .add(), .get(), .get_mut(), .all(). Без него компилятор не видит методов на TaskStore:

use tq_core::store::{Store, TaskStore};
use std::sync::{Arc, Mutex};           // MutexGuard больше не нужен

SharedStore получает новый тип:

pub type SharedStore = Arc<Mutex<TaskStore>>;

Полный crates/api/src/routes.rs:

// crates/api/src/routes.rs - CHANGED
use axum::{
    Json, Router,
    extract::{Path, State},
    routing::{get, patch, post},
};
use serde::Deserialize;
use std::sync::{Arc, Mutex};
use tq_core::store::{Store, TaskStore};
use tq_core::task::Task;

pub type SharedStore = Arc<Mutex<TaskStore>>;

#[derive(Deserialize)]
struct AddRequest {
    title: String,
}

async fn add(State(store): State<SharedStore>, Json(req): Json<AddRequest>) -> Json<Task> {
    let mut guard = store.lock().unwrap();
    let id = guard.add(req.title.as_str()).unwrap(); // TaskStore assigns the id
    Json(guard.get(id).unwrap().clone())
}

async fn list(State(store): State<SharedStore>) -> Json<Vec<Task>> {
    let guard = store.lock().unwrap();
    Json(guard.all().to_vec()) // Json needs owned Vec<Task>; to_vec() copies out of the guard
}

async fn get_task(State(store): State<SharedStore>, Path(id): Path<u64>) -> Json<Task> {
    let guard = store.lock().unwrap();
    Json(guard.get(id).unwrap().clone())
}

async fn done(State(store): State<SharedStore>, Path(id): Path<u64>) -> Json<Task> {
    let mut guard = store.lock().unwrap();
    let task = guard.get_mut(id).unwrap();
    task.complete();
    Json(task.clone())
}

pub fn router(store: SharedStore) -> Router {
    Router::new()
        .route("/tasks", post(add).get(list))
        .route("/tasks/{id}", get(get_task))
        .route("/tasks/{id}/done", patch(done))
        .with_state(store)
}

В crates/api/src/main.rs - TaskStore вместо пустого вектора:

// crates/api/src/main.rs - CHANGED
mod routes;

use axum::{Router, routing::get};
use routes::SharedStore;
use std::sync::{Arc, Mutex};
use tq_core::store::TaskStore;

async fn health() -> String {
    "tq ok".to_string()
}

#[tokio::main]
async fn main() {
    let store: SharedStore = Arc::new(Mutex::new(TaskStore::new(vec![]))); // CHANGED
    let app = Router::new()
        .route("/", get(health))
        .merge(routes::router(store));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Результат

Запросы работают как прежде:

$ curl -s -X POST localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"buy milk"}'
{"id":1,"title":"buy milk","status":"Todo","created_at":"2026-06-20T10:00:00Z"}

$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk","status":"Todo","created_at":"2026-06-20T10:00:00Z"}]

$ curl -s -X PATCH localhost:3000/tasks/1/done
{"id":1,"title":"buy milk","status":"Done","created_at":"2026-06-20T10:00:00Z"}

Обработчики больше не занимаются устройством хранилища - они просят, и хранилище отвечает. Ящик наконец используется по назначению.

Полный код tq для этой главы - 7-many-hands/04-the-proper-store/.