commit 255706974fdde455c22158170cda96a0e163e04c Author: Popov Aleksandr Date: Thu Oct 9 18:02:17 2025 +0300 Initial commit: CPU management API diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c5c098 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +target/ +Cargo.lock +*.db +*.db-shm +*.db-wal +.env +.DS_Store +.idea/ +.vscode/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ec72e64 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cpu-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7" +tokio = { version = "1.0", features = ["full"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "macros"] } +config = "0.14" +serde = { version = "1.0", features = ["derive"] } +utoipa = "5.3" +env_logger = "0.10" +log = "0.4" diff --git a/settings.toml b/settings.toml new file mode 100644 index 0000000..5c21f3d --- /dev/null +++ b/settings.toml @@ -0,0 +1,3 @@ +host = "127.0.0.1" +port = 3000 +database_url = "sqlite:/home/popov_av/cpu-server/cpu.db" diff --git a/src/dtos.rs b/src/dtos.rs new file mode 100644 index 0000000..3dfdd92 --- /dev/null +++ b/src/dtos.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct CpuDto { + pub id: i64, + pub brand: String, + pub model: String, + pub frequency_mhz: i32, + pub cores: i32, + pub threads: i32, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..165c2e5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,23 @@ +use axum::Router; +use sqlx::SqlitePool; +use std::sync::Arc; + +mod dtos; +mod models; +mod queries; +mod routes; +mod schemas; +mod services; +mod settings; +mod openapi; + +pub use settings::Settings; + +#[derive(Clone)] +pub struct AppState { + pub pool: SqlitePool, +} + +pub fn router(state: AppState) -> Router { + routes::create_router(Arc::new(state)) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..395c516 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,49 @@ +use cpu_server::{router, Settings}; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + // Инициализация логгера + env_logger::init(); + + // Загрузка настроек + let settings = Settings::new().expect("Failed to load settings"); + + // Создание пула подключений к базе данных + let pool = sqlx::sqlite::SqlitePoolOptions::new() + .connect(&settings.database_url) + .await + .expect("Failed to connect to database"); + + // Создание таблицы если не существует + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS cpus ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + brand TEXT NOT NULL, + model TEXT NOT NULL, + frequency_mhz INTEGER NOT NULL, + cores INTEGER NOT NULL, + threads INTEGER NOT NULL + ) + "#, + ) + .execute(&pool) + .await + .expect("Failed to create table"); + + let app_state = cpu_server::AppState { pool }; + + let app = router(app_state); + + let addr = SocketAddr::from(([127, 0, 0, 1], settings.port)); + println!("Server running on http://{}", addr); + + let listener = tokio::net::TcpListener::bind(addr) + .await + .unwrap(); + + axum::serve(listener, app) + .await + .unwrap(); +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..b9ff3c2 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Cpu { + pub id: i64, + pub brand: String, + pub model: String, + pub frequency_mhz: i32, + pub cores: i32, + pub threads: i32, +} diff --git a/src/openapi.rs b/src/openapi.rs new file mode 100644 index 0000000..115b5b9 --- /dev/null +++ b/src/openapi.rs @@ -0,0 +1,20 @@ +use utoipa::OpenApi; +use crate::schemas::{CreateCpuRequest, CpuResponse, UpdateCpuRequest}; + +#[derive(OpenApi)] +#[openapi( + paths( + crate::routes::get_all_cpus, + crate::routes::create_cpu, + crate::routes::update_cpu, + crate::routes::delete_cpu, + crate::routes::openapi_json + ), + components( + schemas(CreateCpuRequest, UpdateCpuRequest, CpuResponse) + ), + tags( + (name = "CPU API", description = "CPU management API") + ) +)] +pub struct ApiDoc; diff --git a/src/queries.rs b/src/queries.rs new file mode 100644 index 0000000..7deaaca --- /dev/null +++ b/src/queries.rs @@ -0,0 +1,87 @@ +use sqlx::SqlitePool; +use crate::dtos::CpuDto; + +pub async fn get_all_cpus(pool: &SqlitePool) -> Result, sqlx::Error> { + sqlx::query_as::<_, CpuDto>("SELECT * FROM cpus") + .fetch_all(pool) + .await +} + +pub async fn get_cpu_by_id(pool: &SqlitePool, id: i64) -> Result, sqlx::Error> { + sqlx::query_as::<_, CpuDto>("SELECT * FROM cpus WHERE id = ?") + .bind(id) + .fetch_optional(pool) + .await +} + +pub async fn create_cpu( + pool: &SqlitePool, + brand: &str, + model: &str, + frequency_mhz: i32, + cores: i32, + threads: i32, +) -> Result { + let id = sqlx::query( + "INSERT INTO cpus (brand, model, frequency_mhz, cores, threads) VALUES (?, ?, ?, ?, ?)" + ) + .bind(brand) + .bind(model) + .bind(frequency_mhz) + .bind(cores) + .bind(threads) + .execute(pool) + .await? + .last_insert_rowid(); + + get_cpu_by_id(pool, id).await.map(|cpu| cpu.unwrap()) +} + +pub async fn update_cpu( + pool: &SqlitePool, + id: i64, + brand: Option<&str>, + model: Option<&str>, + frequency_mhz: Option, + cores: Option, + threads: Option, +) -> Result, sqlx::Error> { + // Получаем текущие данные процессора + let current_cpu = get_cpu_by_id(pool, id).await?; + if current_cpu.is_none() { + return Ok(None); + } + let current_cpu = current_cpu.unwrap(); + + // Используем переданные значения или текущие + let brand = brand.unwrap_or(¤t_cpu.brand); + let model = model.unwrap_or(¤t_cpu.model); + let frequency_mhz = frequency_mhz.unwrap_or(current_cpu.frequency_mhz); + let cores = cores.unwrap_or(current_cpu.cores); + let threads = threads.unwrap_or(current_cpu.threads); + + // Выполняем обновление + sqlx::query( + "UPDATE cpus SET brand = ?, model = ?, frequency_mhz = ?, cores = ?, threads = ? WHERE id = ?" + ) + .bind(brand) + .bind(model) + .bind(frequency_mhz) + .bind(cores) + .bind(threads) + .bind(id) + .execute(pool) + .await?; + + // Возвращаем обновленные данные + get_cpu_by_id(pool, id).await +} + +pub async fn delete_cpu(pool: &SqlitePool, id: i64) -> Result { + let result = sqlx::query("DELETE FROM cpus WHERE id = ?") + .bind(id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..104b743 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,172 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::Json, + routing::{get, post}, + Router, +}; +use std::sync::Arc; +use utoipa::OpenApi; + +use crate::{ + schemas::{CreateCpuRequest, CpuResponse, UpdateCpuRequest}, + services::CpuService, + AppState, openapi::ApiDoc, +}; + +pub type AppStateType = Arc; + +#[utoipa::path( + get, + path = "/api/cpu", + responses( + (status = 200, description = "List all CPUs", body = [CpuResponse]) + ) +)] +pub async fn get_all_cpus( + State(state): State, +) -> Result>, StatusCode> { + let cpus = CpuService::get_all_cpus(&state.pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let responses: Vec = cpus + .into_iter() + .map(|cpu| CpuResponse { + id: cpu.id, + brand: cpu.brand, + model: cpu.model, + frequency_mhz: cpu.frequency_mhz, + cores: cpu.cores, + threads: cpu.threads, + }) + .collect(); + + Ok(Json(responses)) +} + +#[utoipa::path( + post, + path = "/api/cpu", + request_body = CreateCpuRequest, + responses( + (status = 201, description = "CPU created successfully", body = CpuResponse), + (status = 400, description = "Invalid input") + ) +)] +pub async fn create_cpu( + State(state): State, + Json(payload): Json, +) -> Result, StatusCode> { + let cpu = CpuService::create_cpu( + &state.pool, + payload.brand, + payload.model, + payload.frequency_mhz, + payload.cores, + payload.threads, + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let response = CpuResponse { + id: cpu.id, + brand: cpu.brand, + model: cpu.model, + frequency_mhz: cpu.frequency_mhz, + cores: cpu.cores, + threads: cpu.threads, + }; + + Ok(Json(response)) +} + +#[utoipa::path( + post, + path = "/api/cpu/{id}", + params( + ("id" = i64, Path, description = "CPU ID") + ), + request_body = UpdateCpuRequest, + responses( + (status = 200, description = "CPU updated successfully", body = CpuResponse), + (status = 404, description = "CPU not found") + ) +)] +pub async fn update_cpu( + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result, StatusCode> { + let cpu = CpuService::update_cpu( + &state.pool, + id, + payload.brand, + payload.model, + payload.frequency_mhz, + payload.cores, + payload.threads, + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + match cpu { + Some(cpu) => { + let response = CpuResponse { + id: cpu.id, + brand: cpu.brand, + model: cpu.model, + frequency_mhz: cpu.frequency_mhz, + cores: cpu.cores, + threads: cpu.threads, + }; + Ok(Json(response)) + } + None => Err(StatusCode::NOT_FOUND), + } +} + +#[utoipa::path( + delete, + path = "/api/cpu/{id}", + params( + ("id" = i64, Path, description = "CPU ID") + ), + responses( + (status = 200, description = "CPU deleted successfully"), + (status = 404, description = "CPU not found") + ) +)] +pub async fn delete_cpu( + State(state): State, + Path(id): Path, +) -> Result { + let deleted = CpuService::delete_cpu(&state.pool, id) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if deleted { + Ok(StatusCode::OK) + } else { + Err(StatusCode::NOT_FOUND) + } +} + +#[utoipa::path( + get, + path = "/api/openapi.json", + responses( + (status = 200, description = "OpenAPI specification") + ) +)] +pub async fn openapi_json() -> Json { + Json(ApiDoc::openapi()) +} + +pub fn create_router(state: AppStateType) -> Router { + Router::new() + .route("/api/cpu", get(get_all_cpus).post(create_cpu)) + .route("/api/cpu/:id", post(update_cpu).delete(delete_cpu)) + .route("/api/openapi.json", get(openapi_json)) + .with_state(state) +} diff --git a/src/schemas.rs b/src/schemas.rs new file mode 100644 index 0000000..d27faa1 --- /dev/null +++ b/src/schemas.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct CreateCpuRequest { + pub brand: String, + pub model: String, + pub frequency_mhz: i32, + pub cores: i32, + pub threads: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct UpdateCpuRequest { + pub brand: Option, + pub model: Option, + pub frequency_mhz: Option, + pub cores: Option, + pub threads: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct CpuResponse { + pub id: i64, + pub brand: String, + pub model: String, + pub frequency_mhz: i32, + pub cores: i32, + pub threads: i32, +} diff --git a/src/services.rs b/src/services.rs new file mode 100644 index 0000000..02aa07b --- /dev/null +++ b/src/services.rs @@ -0,0 +1,61 @@ +use crate::{dtos::CpuDto, models::Cpu, queries}; +use sqlx::SqlitePool; + +pub struct CpuService; + +impl CpuService { + pub async fn get_all_cpus(pool: &SqlitePool) -> Result, sqlx::Error> { + let dtos = queries::get_all_cpus(pool).await?; + Ok(dtos.into_iter().map(|dto| dto.into()).collect()) + } + pub async fn create_cpu( + pool: &SqlitePool, + brand: String, + model: String, + frequency_mhz: i32, + cores: i32, + threads: i32, + ) -> Result { + let dto = queries::create_cpu(pool, &brand, &model, frequency_mhz, cores, threads).await?; + Ok(dto.into()) + } + + pub async fn update_cpu( + pool: &SqlitePool, + id: i64, + brand: Option, + model: Option, + frequency_mhz: Option, + cores: Option, + threads: Option, + ) -> Result, sqlx::Error> { + let dto = queries::update_cpu( + pool, + id, + brand.as_deref(), + model.as_deref(), + frequency_mhz, + cores, + threads, + ) + .await?; + Ok(dto.map(|dto| dto.into())) + } + + pub async fn delete_cpu(pool: &SqlitePool, id: i64) -> Result { + queries::delete_cpu(pool, id).await + } +} + +impl From for Cpu { + fn from(dto: CpuDto) -> Self { + Cpu { + id: dto.id, + brand: dto.brand, + model: dto.model, + frequency_mhz: dto.frequency_mhz, + cores: dto.cores, + threads: dto.threads, + } + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..edaf8e7 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,19 @@ +use config::Config; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct Settings { + pub host: String, + pub port: u16, + pub database_url: String, +} + +impl Settings { + pub fn new() -> Result { + let settings = Config::builder() + .add_source(config::File::with_name("settings")) + .build()?; + + settings.try_deserialize() + } +}