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/.