Initial commit: CPU management API

This commit is contained in:
Popov Aleksandr
2025-10-09 18:02:17 +03:00
commit 255706974f
13 changed files with 509 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
target/
Cargo.lock
*.db
*.db-shm
*.db-wal
.env
.DS_Store
.idea/
.vscode/

14
Cargo.toml Normal file
View File

@ -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"

3
settings.toml Normal file
View File

@ -0,0 +1,3 @@
host = "127.0.0.1"
port = 3000
database_url = "sqlite:/home/popov_av/cpu-server/cpu.db"

11
src/dtos.rs Normal file
View File

@ -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,
}

23
src/lib.rs Normal file
View File

@ -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))
}

49
src/main.rs Normal file
View File

@ -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();
}

11
src/models.rs Normal file
View File

@ -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,
}

20
src/openapi.rs Normal file
View File

@ -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;

87
src/queries.rs Normal file
View File

@ -0,0 +1,87 @@
use sqlx::SqlitePool;
use crate::dtos::CpuDto;
pub async fn get_all_cpus(pool: &SqlitePool) -> Result<Vec<CpuDto>, 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<Option<CpuDto>, 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<CpuDto, sqlx::Error> {
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<i32>,
cores: Option<i32>,
threads: Option<i32>,
) -> Result<Option<CpuDto>, 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(&current_cpu.brand);
let model = model.unwrap_or(&current_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<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM cpus WHERE id = ?")
.bind(id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}

172
src/routes.rs Normal file
View File

@ -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<AppState>;
#[utoipa::path(
get,
path = "/api/cpu",
responses(
(status = 200, description = "List all CPUs", body = [CpuResponse])
)
)]
pub async fn get_all_cpus(
State(state): State<AppStateType>,
) -> Result<Json<Vec<CpuResponse>>, StatusCode> {
let cpus = CpuService::get_all_cpus(&state.pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let responses: Vec<CpuResponse> = 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<AppStateType>,
Json(payload): Json<CreateCpuRequest>,
) -> Result<Json<CpuResponse>, 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<AppStateType>,
Path(id): Path<i64>,
Json(payload): Json<UpdateCpuRequest>,
) -> Result<Json<CpuResponse>, 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<AppStateType>,
Path(id): Path<i64>,
) -> Result<StatusCode, StatusCode> {
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<utoipa::openapi::OpenApi> {
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)
}

30
src/schemas.rs Normal file
View File

@ -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<String>,
pub model: Option<String>,
pub frequency_mhz: Option<i32>,
pub cores: Option<i32>,
pub threads: Option<i32>,
}
#[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,
}

61
src/services.rs Normal file
View File

@ -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<Vec<Cpu>, 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<Cpu, sqlx::Error> {
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<String>,
model: Option<String>,
frequency_mhz: Option<i32>,
cores: Option<i32>,
threads: Option<i32>,
) -> Result<Option<Cpu>, 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<bool, sqlx::Error> {
queries::delete_cpu(pool, id).await
}
}
impl From<CpuDto> 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,
}
}
}

19
src/settings.rs Normal file
View File

@ -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<Self, config::ConfigError> {
let settings = Config::builder()
.add_source(config::File::with_name("settings"))
.build()?;
settings.try_deserialize()
}
}