full with cli options and config support and all endpoints for crud
operations
This commit is contained in:
1 parent
c5d44c6466
commit
6d250c9c1a
10 files changed
+483
-126
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user