full with cli options and config support and all endpoints for crud

operations
This commit is contained in:
wt committed 2025-11-11 03:33:28 +07:00
1 parent c5d44c6466
commit 6d250c9c1a
10 files changed
+483 -126

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
use clap::Parser;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Deserialize, Debug)]
struct FileConfig {
host: Option<String>,
port: Option<u16>,
log_level: Option<String>,
}
#[derive(Parser, Debug)]
#[command(version, about)]
struct Cli {
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
addr: Option<String>,
#[arg(short, long)]
port: Option<u16>,
#[arg(short, long)]
log_level: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Config {
pub host: String,
pub port: u16,
pub log_level: String,
}
impl Config {
pub async fn new() -> anyhow::Result<Self> {
let cli: Cli = Cli::parse();
let file_config: FileConfig = if let Some(config_path) = &cli.config {
println!("Loading config from file");
Self::load_file_config(config_path).await?
} else {
FileConfig {
host: None,
port: None,
log_level: None,
}
};
let host: String = cli
.addr
.or(file_config.host)
.unwrap_or_else(|| "0.0.0.0".to_string());
let port: u16 = cli.port.or(file_config.port).unwrap_or(8888);
let log_level: String = cli
.log_level
.or(file_config.log_level)
.unwrap_or_else(|| "info".to_string());
Ok(Config {
host,
port,
log_level,
})
}
async fn load_file_config(path: &PathBuf) -> anyhow::Result<FileConfig> {
let content = tokio::fs::read_to_string(path).await?;
let config: FileConfig = serde_json::from_str(&content)?;
Ok(config)
}
}
+26 -39
View File
@@ -1,47 +1,34 @@
use std::fs;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
// mod model;
use crate::model;
pub fn get_all_users() -> Vec<model::User> {
let data = fs::read_to_string("users.json").expect("Unable to read file");
serde_json::from_str(&data).expect("Unable to parse JSON")
pub type UserStorage = Arc<Mutex<HashMap<String, model::User>>>;
// storage initialization
pub async fn init_user_storage() -> UserStorage {
let users = read_users_from_file().await.unwrap_or_default();
let map: HashMap<String, model::User> = users
.into_iter()
.map(|user| (user.id.clone(), user))
.collect();
Arc::new(Mutex::new(map))
}
pub fn get_user_by_id(id: String) -> Option<model::User> {
let users = get_all_users();
users.into_iter().find(|user| user.id == id)
// async read file
async fn read_users_from_file() -> Result<Vec<model::User>, Box<dyn std::error::Error>> {
let content = tokio::fs::read_to_string("users.json").await?;
let users: Vec<model::User> = serde_json::from_str(&content)?;
Ok(users)
}
pub fn get_user_by_username(username: &str) -> Option<model::User> {
let users = get_all_users();
users.into_iter().find(|user| user.username == username)
}
pub fn insert_user(user: model::User) -> model::GetUserResponse {
let mut users = get_all_users();
let respuser = model::GetUserResponse {
id: user.id.clone(),
username: user.username.clone(),
email: user.email.clone(),
group: user.group.clone(),
is_superuser: user.is_superuser.clone(),
};
users.push(user);
let data = serde_json::to_string(&users).expect("Unable to serialize JSON");
fs::write("users.json", data).expect("Unable to write file");
respuser
}
pub fn delete_user(id: String) -> bool {
let mut users = get_all_users();
let user_exists = users.iter().any(|user| user.id == id);
if user_exists {
users.retain(|user| user.id != id);
let data = serde_json::to_string_pretty(&users).expect("Unable to serialize JSON");
fs::write("users.json", data).expect("Unable to write file");
true
} else {
false
}
// async write file
pub async fn write_users_to_file(
users: &HashMap<String, model::User>,
) -> Result<(), Box<dyn std::error::Error>> {
let users_vec: Vec<model::User> = users.values().cloned().collect();
let content = serde_json::to_string_pretty(&users_vec)?;
tokio::fs::write("users.json", content).await?;
Ok(())
}
+162
View File
@@ -0,0 +1,162 @@
use crate::db;
use crate::model;
use crate::state;
use axum::Json;
use axum::extract::State;
pub async fn get_all_users_handler(
State(state): State<state::AppState>,
) -> Json<Vec<model::GetUserResponse>> {
let storage_guard = state.storage.lock().await;
let users: Vec<model::User> = storage_guard.values().cloned().collect();
let mapped_users = users
.into_iter()
.map(|user| model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
})
.collect();
Json(mapped_users)
}
pub async fn get_user_by_id_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::GetUserByIdRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
let storage_guard = state.storage.lock().await;
storage_guard
.get(&payload.id)
.cloned()
.map(|user| {
Json(model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
})
})
.ok_or_else(|| {
Json(model::ErrorResponse {
error: "User not found".to_string(),
})
})
}
pub async fn get_user_by_username_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::GetUserByUsernameRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
let storage_guard = state.storage.lock().await;
storage_guard
.get(&payload.username)
.cloned()
.map(|user| {
Json(model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
})
})
.ok_or_else(|| {
Json(model::ErrorResponse {
error: "User not found".to_string(),
})
})
}
pub async fn add_user_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::AddUserRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
let user = model::User {
id: payload.id.clone(),
username: payload.username.clone(),
email: payload.email.clone(),
password: payload.password.clone(),
group: payload.group.clone(),
is_superuser: payload.is_superuser,
};
let mut storage_guard = state.storage.lock().await;
if storage_guard.contains_key(&user.id) {
return Err(Json(model::ErrorResponse {
error: "User already exists".to_string(),
}));
}
storage_guard.insert(user.id.clone(), user.clone());
let storage_clone = storage_guard.clone();
tokio::spawn(async move {
if let Err(e) = db::write_users_to_file(&storage_clone).await {
eprintln!("Failed to write users to file: {}", e);
}
});
Ok(Json(model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
}))
}
pub async fn delete_user_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::DeleteUserRequest>,
) -> Result<Json<model::DeleteUserResponse>, Json<model::ErrorResponse>> {
let mut storage_guard = state.storage.lock().await;
if storage_guard.remove(&payload.id).is_none() {
return Err(Json(model::ErrorResponse {
error: "User not found".to_string(),
}));
}
let storage_clone = storage_guard.clone();
tokio::spawn(async move {
if let Err(e) = db::write_users_to_file(&storage_clone).await {
eprintln!("Failed to write users to file: {}", e);
}
});
Ok(Json(model::DeleteUserResponse { success: true }))
}
pub async fn update_user_handler(
State(state): State<state::AppState>,
Json(payload): Json<model::UpdateUserRequest>,
) -> Result<Json<model::GetUserResponse>, Json<model::ErrorResponse>> {
let mut storage_guard = state.storage.lock().await;
if !storage_guard.contains_key(&payload.id) {
return Err(Json(model::ErrorResponse {
error: "User not found".to_string(),
}));
}
let old_user = storage_guard.get(&payload.id).unwrap().clone();
storage_guard.insert(
payload.id.clone(),
model::User {
id: payload.id.clone(),
username: payload.username.clone(),
email: payload.email.clone(),
password: old_user.password.clone(),
group: payload.group.clone(),
is_superuser: payload.is_superuser,
},
);
let storage_clone = storage_guard.clone(); // write to file in background
tokio::spawn(async move {
if let Err(e) = db::write_users_to_file(&storage_clone).await {
eprintln!("Failed to write users to file: {}", e);
}
});
Ok(Json(model::GetUserResponse {
id: payload.id.clone(),
username: payload.username.clone(),
email: payload.email.clone(),
group: payload.group.clone(),
is_superuser: payload.is_superuser,
}))
}
+7 -85
View File
@@ -1,91 +1,13 @@
use axum::{Json, Router, routing::delete, routing::post};
use std::net::SocketAddr;
mod config;
mod db;
mod handlers;
mod model;
mod startup;
mod state;
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let app = Router::new()
.route("/api/users", post(get_all_users_handler))
.route("/api/users/user", post(get_user_by_id_handler))
.route(
"/api/users/user/username",
post(get_user_by_username_handler),
)
.route("/api/users/add", post(add_user_handler))
.route("/api/users", delete(delete_user_handler));
let addr = SocketAddr::from(([0, 0, 0, 0], 8888));
println!("Сервер запускается на {}", addr);
axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
.await
.unwrap();
}
async fn get_all_users_handler() -> Json<Vec<model::GetUserResponse>> {
let users = db::get_all_users();
let mapped_users = users
.into_iter()
.map(|user| model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
})
.collect();
Json(mapped_users)
}
async fn get_user_by_id_handler(
Json(payload): Json<model::GetUserByIdRequest>,
) -> Json<Option<model::GetUserResponse>> {
let user = db::get_user_by_id(payload.id);
let mapped_user = user.map(|user| model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
});
Json(mapped_user)
}
async fn get_user_by_username_handler(
Json(payload): Json<model::GetUserByUsernameRequest>,
) -> Json<Option<model::GetUserResponse>> {
let user = db::get_user_by_username(&payload.username);
let mapped_user = user.map(|user| model::GetUserResponse {
id: user.id,
username: user.username,
email: user.email,
group: user.group,
is_superuser: user.is_superuser,
});
Json(mapped_user)
}
async fn add_user_handler(
Json(payload): Json<model::AddUserRequest>,
) -> Json<model::GetUserResponse> {
let user = model::User {
id: payload.id.clone(),
username: payload.username.clone(),
email: payload.email.clone(),
password: payload.password.clone(),
group: payload.group.clone(),
is_superuser: payload.is_superuser,
};
Json(db::insert_user(user))
}
async fn delete_user_handler(
Json(payload): Json<model::DeleteUserRequest>,
) -> Json<model::DeleteUserResponse> {
let deleted: bool = db::delete_user(payload.id);
Json(model::DeleteUserResponse { success: deleted })
startup::start_server().await?;
Ok(())
}
+16 -2
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct User {
pub id: String,
pub username: String,
@@ -20,7 +20,7 @@ pub struct GetUserByUsernameRequest {
pub username: String,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct AddUserRequest {
pub id: String,
pub username: String,
@@ -48,3 +48,17 @@ pub struct DeleteUserRequest {
pub struct DeleteUserResponse {
pub success: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateUserRequest {
pub id: String,
pub username: String,
pub email: String,
pub group: String,
pub is_superuser: bool,
}
#[derive(Serialize, Debug)]
pub struct ErrorResponse {
pub error: String,
}
+40
View File
@@ -0,0 +1,40 @@
use crate::handlers;
use crate::state;
use axum::{Router, routing::delete, routing::post, routing::put};
use std::net::SocketAddr;
pub async fn start_server() -> anyhow::Result<()> {
let state = state::AppState::new().await?;
print_config(&state.config);
let app = create_app(state.clone());
let addr: SocketAddr = format!("{}:{}", state.config.host, state.config.port)
.parse()
.expect("Invalid host or port configuration");
axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
.await
.unwrap();
tokio::signal::ctrl_c().await?;
println!("Server stopped");
Ok(())
}
pub fn create_app(state: state::AppState) -> Router {
Router::new()
.route("/api/users", post(handlers::get_all_users_handler))
.route("/api/users/user", post(handlers::get_user_by_id_handler))
.route(
"/api/users/user/username",
post(handlers::get_user_by_username_handler),
)
.route("/api/users/add", post(handlers::add_user_handler))
.route("/api/users", delete(handlers::delete_user_handler))
.route("/api/users", put(handlers::update_user_handler))
.with_state(state)
}
fn print_config(config: &crate::config::Config) {
println!("Host: {}", config.host.clone());
println!("Port: {}", config.port);
println!("log_level: {}", config.log_level);
}
+20
View File
@@ -0,0 +1,20 @@
use crate::config;
use crate::db;
#[derive(Clone)]
pub struct AppState {
pub storage: db::UserStorage,
pub config: config::Config,
}
impl AppState {
pub async fn new() -> anyhow::Result<Self> {
let config = config::Config::new().await?;
let user_storage = db::init_user_storage().await;
let state = AppState {
storage: user_storage,
config: config.clone(),
};
Ok(state)
}
}