Платформа/Rust интеграция
Это руководство сочетает Топаз с экосистемой Rust/Actix и показывает, как встроить логику Топаз в существующие Rust‑сервисы.
Создайте ваш первый полный проект на Топазе! Мы создадим API для управления задачами, изучая все основные возможности Топаза. 🚀
🎯 Обзор проекта
Что мы создаем
REST API для управления личными задачами
Используемые технологии
Топаз веб-сервер, обработка JSON, файловое хранилище, обработка ошибок
Что вы изучите
Структура проекта, маршрутизация, моделирование данных, тестирование
📁 Настройка проекта
1. Создание нового проекта
# Создание нового проекта Топаз
topaz new ЗадачиAPI --template=web-api
cd ЗадачиAPI
# Проверка структуры проекта
tree .
Созданная структура:
src/
├─ main.tpz # Главная точка входа
├─ models/ # Модели данных
├─ handlers/ # Обработчики запросов
└─ utils/ # Утилиты
tests/ # Тестовые файлы
data/ # Хранилище данных
topaz.toml # Конфигурация проекта
README.md
2. Добавление зависимостей
# topaz.toml
[package]
name = "ЗадачиAPI"
version = "0.1.0"
description = "REST API для управления личными задачами"
[dependencies]
web = "2.0" # Веб-сервер фреймворк
json = "1.5" # Обработка JSON
uuid = "1.0" # Генерация уникальных ID
chrono = "0.9" # Работа с датой/временем
serde = "2.1" # Сериализация/десериализация
[dev-dependencies]
test-framework = "1.0"
http-client = "0.8"
🏗️ Моделирование данных
Определение модели задачи
// src/models/todo.tpz
use chrono::{DateTime, Utc}
use uuid::Uuid
use serde::{Serialize, Deserialize}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct Задача {
id: Uuid,
заголовок: string,
описание: Option<string>,
выполнена: bool,
приоритет: Приоритет,
датаСоздания: DateTime<Utc>,
датаВыполнения: Option<DateTime<Utc>>,
теги: Array<string>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
enum Приоритет {
Низкий,
Обычный,
Высокий,
Срочный
}
impl Задача {
// Создание новой задачи
function новая(
заголовок: string,
описание: Option<string> = None,
приоритет: Приоритет = Приоритет::Обычный
) -> Задача {
return Задача {
id: Uuid::new_v4(),
заголовок,
описание,
выполнена: false,
приоритет,
датаСоздания: Utc::now(),
датаВыполнения: None,
теги: []
}
}
// Отметить задачу как выполненную
function выполнить(&mut self) {
self.выполнена = true
self.датаВыполнения = Some(Utc::now())
}
// Добавить тег
function добавитьТег(&mut self, новыйТег: string) {
if !self.теги.contains(&новыйТег) {
self.теги.push(новыйТег)
}
}
// Получить балл приоритета для сортировки
function баллПриоритета(&self) -> int {
match self.приоритет {
case Приоритет::Срочный => 4
case Приоритет::Высокий => 3
case Приоритет::Обычный => 2
case Приоритет::Низкий => 1
}
}
} test {
// Тест создания задачи
let задача = Задача::новая("Изучить Топаз".to_string(), None, Приоритет::Высокий)
assert задача.заголовок == "Изучить Топаз"
assert задача.выполнена == false
assert задача.приоритет == Приоритет::Высокий
// Тест выполнения
let mut задача = Задача::новая("Тест".to_string(), None, Приоритет::Обычный)
задача.выполнить()
assert задача.выполнена == true
assert задача.датаВыполнения.is_some() == true
// Тест добавления тегов
let mut задача = Задача::новая("Проект".to_string(), None, Приоритет::Обычный)
задача.добавитьТег("разработка".to_string())
задача.добавитьТег("изучение".to_string())
задача.добавитьТег("разработка".to_string()) // Попытка дублирования
assert задача.теги.len() == 2
assert задача.теги.contains(&"разработка".to_string())
assert задача.теги.contains(&"изучение".to_string())
}
Модели запросов/ответов
// src/models/dto.tpz
use serde::{Serialize, Deserialize}
use uuid::Uuid
#[derive(Deserialize)]
struct ЗапросСозданияЗадачи {
заголовок: string,
описание: Option<string>,
приоритет: Option<Приоритет>,
теги: Option<Array<string>>
}
#[derive(Deserialize)]
struct ЗапросОбновленияЗадачи {
заголовок: Option<string>,
описание: Option<string>,
приоритет: Option<Приоритет>,
теги: Option<Array<string>>
}
#[derive(Serialize)]
struct ОтветЗадача {
id: Uuid,
заголовок: string,
описание: Option<string>,
выполнена: bool,
приоритет: Приоритет,
датаСоздания: string,
датаВыполнения: Option<string>,
теги: Array<string>
}
impl From<Задача> for ОтветЗадача {
function from(задача: Задача) -> ОтветЗадача {
return ОтветЗадача {
id: задача.id,
заголовок: задача.заголовок,
описание: задача.описание,
выполнена: задача.выполнена,
приоритет: задача.приоритет,
датаСоздания: задача.датаСоздания.format("%Y-%m-%d %H:%M:%S").to_string(),
датаВыполнения: задача.датаВыполнения.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()),
теги: задача.теги
}
}
}
#[derive(Serialize)]
struct ОтветAPI<T> {
успех: bool,
данные: Option<T>,
сообщение: string
}
impl<T> ОтветAPI<T> {
function успех(данные: T) -> ОтветAPI<T> {
return ОтветAPI {
успех: true,
данные: Some(данные),
сообщение: "Успех".to_string()
}
}
function ошибка(сообщение: string) -> ОтветAPI<T> {
return ОтветAPI {
успех: false,
данные: None,
сообщение
}
}
}
💾 Хранилище данных
Простое файловое хранилище
// src/storage/file_store.tpz
use std::fs
use std::path::Path
use uuid::Uuid
use crate::models::Задача
struct ФайловоеХранилище {
путьДанных: string
}
impl ФайловоеХранилище {
function new(путьДанных: string) -> Result<ФайловоеХранилище, string> {
// Создание директории данных
if !Path::new(&путьДанных).exists() {
fs::create_dir_all(&путьДанных)
.map_err(|e| "Не удалось создать директорию данных: {e}".to_string())?
}
return Ok(ФайловоеХранилище { путьДанных })
}
function путьФайла(&self) -> string {
return format!("{}/todos.json", self.путьДанных)
}
// Загрузить все задачи
function получитьВсе(&self) -> Result<Array<Задача>, string> {
let путь = self.путьФайла()
if !Path::new(&путь).exists() {
return Ok([])
}
let содержимое = fs::read_to_string(&путь)
.map_err(|e| "Не удалось прочитать файл: {e}".to_string())?
let задачи: Array<Задача> = serde_json::from_str(&содержимое)
.map_err(|e| "Не удалось разобрать JSON: {e}".to_string())?
return Ok(задачи)
}
// Сохранить все задачи
function сохранитьВсе(&self, задачи: &Array<Задача>) -> Result<(), string> {
let jsonСтрока = serde_json::to_string_pretty(задачи)
.map_err(|e| "Не удалось сериализовать JSON: {e}".to_string())?
fs::write(self.путьФайла(), jsonСтрока)
.map_err(|e| "Не удалось записать файл: {e}".to_string())?
return Ok(())
}
// Добавить задачу
function добавить(&self, задача: Задача) -> Result<Задача, string> {
let mut задачи = self.получитьВсе()?
задачи.push(задача.clone())
self.сохранитьВсе(&задачи)?
return Ok(задача)
}
// Найти задачу по ID
function найтиПоID(&self, id: Uuid) -> Result<Option<Задача>, string> {
let задачи = self.получитьВсе()?
let найденнаяЗадача = задачи.iter().find(|задача| задача.id == id).cloned()
return Ok(найденнаяЗадача)
}
// Обновить задачу
function обновить(&self, id: Uuid, обновленнаяЗадача: Задача) -> Result<Option<Задача>, string> {
let mut задачи = self.получитьВсе()?
return match задачи.iter().position(|задача| задача.id == id) {
case Some(индекс) => {
задачи[индекс] = обновленнаяЗадача.clone()
self.сохранитьВсе(&задачи)?
Ok(Some(обновленнаяЗадача))
}
case None => Ok(None)
}
}
// Удалить задачу
function удалить(&self, id: Uuid) -> Result<bool, string> {
let mut задачи = self.получитьВсе()?
return match задачи.iter().position(|задача| задача.id == id) {
case Some(индекс) => {
задачи.remove(индекс)
self.сохранитьВсе(&задачи)?
Ok(true)
}
case None => Ok(false)
}
}
// Поиск задач с фильтром
function поиск<F>(&self, фильтр: F) -> Result<Array<Задача>, string>
where
F: Fn(&Задача) -> bool
{
let задачи = self.получитьВсе()?
let результат = задачи.into_iter().filter(фильтр).collect()
return Ok(результат)
}
} test {
use tempfile::tempdir
// Тест во временной директории
let временнаяДиректория = tempdir().unwrap()
let хранилище = ФайловоеХранилище::new(временнаяДиректория.path().to_str().unwrap().to_string()).unwrap()
// Тест добавления задачи
let новаяЗадача = Задача::новая("Тестовая задача".to_string(), None, Приоритет::Обычный)
let добавленнаяЗадача = хранилище.добавить(новаяЗадача.clone()).unwrap()
assert добавленнаяЗадача.id == новаяЗадача.id
// Тест поиска задачи
let найденнаяЗадача = хранилище.найтиПоID(новаяЗадача.id).unwrap()
assert найденнаяЗадача.is_some()
assert найденнаяЗадача.unwrap().заголовок == "Тестовая задача"
// Тест получения всех задач
let всеЗадачи = хранилище.получитьВсе().unwrap()
assert всеЗадачи.len() == 1
}
🌐 Веб-сервер и маршрутизация
Настройка главного сервера
// src/main.tpz
mod models
mod storage
mod handlers
use web::{App, HttpServer, middleware}
use storage::ФайловоеХранилище
use handlers::*
#[tokio::main]
function main() -> Result<(), Box<dyn std::error::Error>> {
// Настройка логирования
env_logger::init()
// Инициализация хранилища данных
let хранилище = ФайловоеХранилище::new("./data".to_string())?
println!("🚀 Запуск сервера API управления задачами...")
println!("📍 Адрес сервера: http://localhost:8080")
println!("📋 Документация API: http://localhost:8080/docs")
// Запуск HTTP сервера
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(хранилище.clone()))
.wrap(middleware::Logger::default())
.wrap(middleware::DefaultHeaders::new()
.header("Content-Type", "application/json; charset=utf-8"))
.service(
web::scope("/api/v1")
.route("/todos", web::get().to(получитьВсеЗадачи))
.route("/todos", web::post().to(создатьЗадачу))
.route("/todos/{id}", web::get().to(получитьЗадачу))
.route("/todos/{id}", web::put().to(обновитьЗадачу))
.route("/todos/{id}", web::delete().to(удалитьЗадачу))
.route("/todos/{id}/complete", web::patch().to(выполнитьЗадачу))
.route("/todos/search", web::get().to(поискЗадач))
)
.route("/health", web::get().to(проверкаЗдоровья))
.route("/docs", web::get().to(документацияAPI))
})
.bind("127.0.0.1:8080")?
.run()?
Ok(())
}
Обработчики запросов
// src/handlers/todo_handlers.tpz
use web::{web, HttpRequest, HttpResponse, Result as WebResult}
use uuid::Uuid
use crate::models::{Задача, ЗапросСозданияЗадачи, ЗапросОбновленияЗадачи, ОтветЗадача, ОтветAPI}
use crate::storage::ФайловоеХранилище
// Получить все задачи
function получитьВсеЗадачи(
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
match хранилище.получитьВсе() {
case Ok(задачи) => {
let ответыЗадач: Array<ОтветЗадача> = задачи
.into_iter()
.map(|задача| ОтветЗадача::from(задача))
.collect()
return Ok(HttpResponse::Ok()
.json(ОтветAPI::успех(ответыЗадач)))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
// Создать новую задачу
function создатьЗадачу(
запрос: web::Json<ЗапросСозданияЗадачи>,
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
let mut новаяЗадача = Задача::новая(
запрос.заголовок.clone(),
запрос.описание.clone(),
запрос.приоритет.unwrap_or(Приоритет::Обычный)
)
// Добавить теги если предоставлены
match &запрос.теги {
case Some(списокТегов) => {
for тег in списокТегов {
новаяЗадача.добавитьТег(тег.clone())
}
}
case None => {}
}
match хранилище.добавить(новаяЗадача) {
case Ok(созданнаяЗадача) => {
return Ok(HttpResponse::Created()
.json(ОтветAPI::успех(ОтветЗадача::from(созданнаяЗадача))))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
// Получить конкретную задачу
function получитьЗадачу(
путь: web::Path<Uuid>,
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
let id = путь.into_inner()
match хранилище.найтиПоID(id) {
case Ok(Some(задача)) => {
return Ok(HttpResponse::Ok()
.json(ОтветAPI::успех(ОтветЗадача::from(задача))))
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ОтветAPI::<()>::ошибка("Задача не найдена".to_string())))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
// Обновить задачу
function обновитьЗадачу(
путь: web::Path<Uuid>,
запрос: web::Json<ЗапросОбновленияЗадачи>,
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
let id = путь.into_inner()
match хранилище.найтиПоID(id) {
case Ok(Some(mut существующаяЗадача)) => {
// Обновить только запрашиваемые поля
match &запрос.заголовок {
case Some(заголовок) => {
существующаяЗадача.заголовок = заголовок.clone()
}
case None => {}
}
match &запрос.описание {
case Some(описание) => {
существующаяЗадача.описание = Some(описание.clone())
}
case None => {}
}
match &запрос.приоритет {
case Some(приоритет) => {
существующаяЗадача.приоритет = приоритет.clone()
}
case None => {}
}
match &запрос.теги {
case Some(списокТегов) => {
существующаяЗадача.теги = списокТегов.clone()
}
case None => {}
}
match хранилище.обновить(id, существующаяЗадача) {
case Ok(Some(обновленнаяЗадача)) => {
return Ok(HttpResponse::Ok()
.json(ОтветAPI::успех(ОтветЗадача::from(обновленнаяЗадача))))
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ОтветAPI::<()>::ошибка("Задача не найдена".to_string())))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ОтветAPI::<()>::ошибка("Задача не найдена".to_string())))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
// Удалить задачу
function удалитьЗадачу(
путь: web::Path<Uuid>,
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
let id = путь.into_inner()
match хранилище.удалить(id) {
case Ok(true) => {
return Ok(HttpResponse::Ok()
.json(ОтветAPI::успех("Задача успешно удалена".to_string())))
}
case Ok(false) => {
return Ok(HttpResponse::NotFound()
.json(ОтветAPI::<()>::ошибка("Задача не найдена".to_string())))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
// Выполнить задачу
function выполнитьЗадачу(
путь: web::Path<Uuid>,
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
let id = путь.into_inner()
match хранилище.найтиПоID(id) {
case Ok(Some(mut задача)) => {
задача.выполнить()
match хранилище.обновить(id, задача) {
case Ok(Some(выполненнаяЗадача)) => {
return Ok(HttpResponse::Ok()
.json(ОтветAPI::успех(ОтветЗадача::from(выполненнаяЗадача))))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
case _ => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка("Не удалось выполнить задачу".to_string())))
}
}
}
case Ok(None) => {
return Ok(HttpResponse::NotFound()
.json(ОтветAPI::<()>::ошибка("Задача не найдена".to_string())))
}
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
}
// Поиск задач (используя параметры запроса)
function поискЗадач(
запрос: HttpRequest,
хранилище: web::Data<ФайловоеХранилище>
) -> WebResult<HttpResponse> {
let запрос = web::Query::<HashMap<String, String>>::from_query(запрос.query_string())
.map_err(|_| HttpResponse::BadRequest().json(ОтветAPI::<()>::ошибка("Неверные параметры запроса".to_string())))?
let задачи = match хранилище.получитьВсе() {
case Ok(список) => список
case Err(ошибка) => {
return Ok(HttpResponse::InternalServerError()
.json(ОтветAPI::<()>::ошибка(ошибка)))
}
}
let mut отфильтрованныеЗадачи = задачи
// Фильтр по статусу выполнения
match запрос.get("completed") {
case Some(выполненаСтрока) => {
let статусВыполнения = выполненаСтрока.parse::<bool>().unwrap_or(false)
отфильтрованныеЗадачи = отфильтрованныеЗадачи.into_iter()
.filter(|задача| задача.выполнена == статусВыполнения)
.collect()
}
case None => {}
}
// Фильтр по приоритету
match запрос.get("priority") {
case Some(приоритетСтрока) => {
match приоритетСтрока.parse::<Приоритет>() {
case Ok(приоритет) => {
отфильтрованныеЗадачи = отфильтрованныеЗадачи.into_iter()
.filter(|задача| задача.приоритет == приоритет)
.collect()
}
case Err(_) => {}
}
}
case None => {}
}
// Фильтр по тегу
match запрос.get("tag") {
case Some(тег) => {
отфильтрованныеЗадачи = отфильтрованныеЗадачи.into_iter()
.filter(|задача| задача.теги.contains(тег))
.collect()
}
case None => {}
}
// Поиск в заголовке
match запрос.get("search") {
case Some(поисковыйТермин) => {
let поискВНижнемРегистре = поисковыйТермин.to_lowercase()
отфильтрованныеЗадачи = отфильтрованныеЗадачи.into_iter()
.filter(|задача| задача.заголовок.to_lowercase().contains(&поискВНижнемРегистре))
.collect()
}
case None => {}
}
// Сортировка по приоритету
отфильтрованныеЗадачи.sort_by(|a, b| b.баллПриоритета().cmp(&a.баллПриоритета()))
let ответыЗадач: Array<ОтветЗадача> = отфильтрованныеЗадачи
.into_iter()
.map(|задача| ОтветЗадача::from(задача))
.collect()
return Ok(HttpResponse::Ok()
.json(ОтветAPI::успех(ответыЗадач)))
}
// Проверка здоровья
function проверкаЗдоровья() -> WebResult<HttpResponse> {
return Ok(HttpResponse::Ok()
.json(json!({
"статус": "здоров",
"время": chrono::Utc::now().to_rfc3339(),
"версия": "0.1.0"
})))
}
// Документация API
function документацияAPI() -> WebResult<HttpResponse> {
let документы = r#"
<!DOCTYPE html>
<html>
<head>
<title>Документация API управления задачами</title>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
.endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 5px; }
.method { font-weight: bold; padding: 3px 8px; border-radius: 3px; color: white; }
.get { background: #28a745; }
.post { background: #007bff; }
.put { background: #ffc107; color: black; }
.delete { background: #dc3545; }
.patch { background: #6f42c1; }
</style>
</head>
<body>
<h1>🚀 Документация API управления задачами</h1>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/v1/todos</strong>
<p>Получить все задачи.</p>
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/v1/todos</strong>
<p>Создать новую задачу.</p>
<pre>{ "заголовок": "string", "описание": "string?", "приоритет": "enum?", "теги": "string[]?" }</pre>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/v1/todos/{id}</strong>
<p>Получить конкретную задачу.</p>
</div>
<div class="endpoint">
<span class="method put">PUT</span>
<strong>/api/v1/todos/{id}</strong>
<p>Обновить задачу.</p>
</div>
<div class="endpoint">
<span class="method delete">DELETE</span>
<strong>/api/v1/todos/{id}</strong>
<p>Удалить задачу.</p>
</div>
<div class="endpoint">
<span class="method patch">PATCH</span>
<strong>/api/v1/todos/{id}/complete</strong>
<p>Отметить задачу как выполненную.</p>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/v1/todos/search</strong>
<p>Поиск задач. Параметры запроса: completed, priority, tag, search</p>
</div>
<h2>🏥 Проверка здоровья</h2>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/health</strong>
<p>Проверить состояние сервера.</p>
</div>
</body>
</html>
"#;
return Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(документы))
}
🧪 Написание тестов
Интеграционные тесты
// tests/integration_test.tpz
use actix_web::{test, App}
use serde_json::json
use tempfile::tempdir
use ЗадачиAPI::*
#[actix_rt::test]
function тест_crud_задач() -> Result<(), Box<dyn std::error::Error>> {
// Настройка временного хранилища
let временнаяДиректория = tempdir().unwrap()
let хранилище = ФайловоеХранилище::new(временнаяДиректория.path().to_str().unwrap().to_string()).unwrap()
let mut приложение = test::init_service(
App::new()
.app_data(web::Data::new(хранилище))
.service(
web::scope("/api/v1")
.route("/todos", web::get().to(получитьВсеЗадачи))
.route("/todos", web::post().to(создатьЗадачу))
.route("/todos/{id}", web::get().to(получитьЗадачу))
.route("/todos/{id}", web::put().to(обновитьЗадачу))
.route("/todos/{id}", web::delete().to(удалитьЗадачу))
)
)?
// 1. Тест создания задачи
let запросНовойЗадачи = json!({
"заголовок": "Завершить проект Топаз",
"описание": "Закончить первый проект на Топазе",
"приоритет": "Высокий",
"теги": ["разработка", "изучение"]
})
let запрос = test::TestRequest::post()
.uri("/api/v1/todos")
.set_json(&запросНовойЗадачи)
.to_request()
let ответ = test::call_service(&mut приложение, запрос)?
assert!(ответ.status().is_success())
let телоОтвета: serde_json::Value = test::read_body_json(ответ)?
assert!(телоОтвета["успех"].as_bool().unwrap())
let созданнаяЗадача = &телоОтвета["данные"]
let idЗадачи = созданнаяЗадача["id"].as_str().unwrap()
// 2. Тест получения задачи
let запрос = test::TestRequest::get()
.uri(&format!("/api/v1/todos/{}", idЗадачи))
.to_request()
let ответ = test::call_service(&mut приложение, запрос)?
assert!(ответ.status().is_success())
let телоОтвета: serde_json::Value = test::read_body_json(ответ)?
let полученнаяЗадача = &телоОтвета["данные"]
assert_eq!(полученнаяЗадача["заголовок"], "Завершить проект Топаз")
// 3. Тест получения всех задач
let запрос = test::TestRequest::get()
.uri("/api/v1/todos")
.to_request()
let ответ = test::call_service(&mut приложение, запрос)?
assert!(ответ.status().is_success())
let телоОтвета: serde_json::Value = test::read_body_json(ответ)?
let списокЗадач = телоОтвета["данные"].as_array().unwrap()
assert_eq!(списокЗадач.len(), 1)
// 4. Тест обновления задачи
let запросОбновления = json!({
"заголовок": "Завершить проект Топаз (Обновлено)",
"приоритет": "Срочный"
})
let запрос = test::TestRequest::put()
.uri(&format!("/api/v1/todos/{}", idЗадачи))
.set_json(&запросОбновления)
.to_request()
let ответ = test::call_service(&mut приложение, запрос)?
assert!(ответ.status().is_success())
// 5. Тест удаления задачи
let запрос = test::TestRequest::delete()
.uri(&format!("/api/v1/todos/{}", idЗадачи))
.to_request()
let ответ = test::call_service(&mut приложение, запрос)?
assert!(ответ.status().is_success())
// 6. Проверка удаления
let запрос = test::TestRequest::get()
.uri(&format!("/api/v1/todos/{}", idЗадачи))
.to_request()
let ответ = test::call_service(&mut приложение, запрос)?
assert_eq!(ответ.status(), 404)
Ok(())
}
#[actix_rt::test]
function тест_поиска_задач() -> Result<(), Box<dyn std::error::Error>> {
// Настройка тестовых данных и тестирование функции поиска
// ... (код тестов поиска)
Ok(())
}
🚀 Запуск и тестирование
Запуск проекта
# Установка зависимостей
topaz build
# Запуск сервера разработки
topaz run
# Или запуск в режиме релиза
topaz run --release
Тестирование API
# Проверка здоровья
curl http://localhost:8080/health
# Создание новой задачи
curl -X POST http://localhost:8080/api/v1/todos \
-H "Content-Type: application/json" \
-d '{
"заголовок": "Изучить Топаз",
"описание": "Изучить все возможности языка Топаза",
"приоритет": "Высокий",
"теги": ["изучение", "программирование"]
}'
# Получение всех задач
curl http://localhost:8080/api/v1/todos
# Поиск незавершенных задач
curl "http://localhost:8080/api/v1/todos/search?completed=false"
# Поиск задач с высоким приоритетом
curl "http://localhost:8080/api/v1/todos/search?priority=Высокий"
Запуск тестов
# Запуск всех тестов
topaz test
# Запуск конкретного теста
topaz test integration_test
# Проверка покрытия тестами
topaz test --coverage
🎯 Следующие шаги
Поздравляем! Вы завершили свой первый полный проект на Топазе! 🎉
Дополнительные функции для реализации:
- Интеграция с базой данных: PostgreSQL, MongoDB и др.
- Аутентификация пользователей: Аутентификация на основе JWT токенов
- Обновления в реальном времени: Синхронизация задач в реальном времени через WebSocket
- Загрузка файлов: Функция прикрепления файлов к задачам
- Email уведомления: Уведомления о приближении дедлайнов
- Docker развертывание: Контейнеризация и облачное развертывание
Основные концепции Топаз, которые вы изучили:
- ✅ Проектирование структуры проекта
- ✅ Моделирование данных и система типов
- ✅ Обработка ошибок (типы Result/Option)
- ✅ Построение веб-сервера и REST API
- ✅ Файловое хранение данных
- ✅ Использование сопоставления с образцом
- ✅ Разработка через тестирование
- ✅ Многоязычные идентификаторы и естественное написание кода
Теперь вы готовы браться за более сложные проекты на Топазе! 🚀