diff --git a/Cargo.toml b/Cargo.toml index 100ae95..cc45d1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ hex = "0.4" [dev-dependencies] tempfile = "3" +surf = { version = "2", default-features = false, features = ["h1-client"] } diff --git a/docs/README.md b/docs/README.md index 088c2aa..67eb733 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,6 +24,7 @@ Voir aussi: - `depannage.md` - `performance.md` - `api_json_spec.md` +- `api_contrats.md` ## REX technique diff --git a/docs/api_contrats.md b/docs/api_contrats.md new file mode 100644 index 0000000..6a7f3bd --- /dev/null +++ b/docs/api_contrats.md @@ -0,0 +1,21 @@ +# Contrats API + +## Garanties de Contrat +- Content-Type JSON, réponses structurées. +- Clé: 64 hex (validation stricte), sinon 400. +- Valeur: hex valide, sinon 400. +- Conflit de clé: 409 si la clé existe déjà. +- TTL: min 60, max 31 536 000; par défaut 86 400 si non `--permanent`. +- Récupération: + - 200 avec `{ key, value }` si trouvée. + - 400 si clé invalide. + - 404 si absente. + +## Couverture de Tests +- Stockage et récupération (succès). +- Conflit de clé. +- Suppression des expirés via nettoyage. +- HTTP `/store`: succès, conflit, clé invalide, valeur invalide. +- HTTP `/retrieve`: succès, clé invalide, clé absente. + +Voir `api_json_spec.md` pour les schémas et contraintes détaillés. diff --git a/src/lib.rs b/src/lib.rs index 9d1a5e0..a415121 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ use async_std::path::Path; use async_std::stream::StreamExt; use serde::{Deserialize, Serialize}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tide::StatusCode; +use tide::{StatusCode, Request, Response}; #[derive(Clone, Debug)] pub struct StorageService { @@ -142,3 +142,111 @@ pub fn system_time_to_unix(system_time: SystemTime) -> u64 { pub fn unix_to_system_time(unix_timestamp: u64) -> SystemTime { UNIX_EPOCH + Duration::from_secs(unix_timestamp) } + +#[derive(Deserialize)] +pub struct StoreRequest { + pub key: String, + pub value: String, + pub ttl: Option, +} + +#[derive(Serialize)] +pub struct ApiResponse { pub message: String } + +#[derive(Serialize)] +pub struct RetrieveResponse { pub key: String, pub value: String } + +pub async fn handle_store(mut req: Request, no_ttl_permanent: bool) -> tide::Result { + let data: StoreRequest = match req.body_json().await { + Ok(data) => data, + Err(e) => { + return Ok(Response::builder(StatusCode::BadRequest) + .body(format!("Invalid request: {}", e)) + .build()); + } + }; + + if data.key.len() != 64 || !data.key.chars().all(|c| c.is_ascii_hexdigit()) { + return Ok(Response::builder(StatusCode::BadRequest) + .body("Invalid key: must be a 32 bytes hex string.".to_string()) + .build()); + } + + let live_for: Option = if let Some(ttl) = data.ttl { + if ttl < 60 { + return Ok(Response::builder(StatusCode::BadRequest) + .body(format!("Invalid ttl: must be at least {} seconds.", 60)) + .build()); + } else if ttl > 31_536_000 { + return Ok(Response::builder(StatusCode::BadRequest) + .body(format!("Invalid ttl: must be at most {} seconds.", 31_536_000)) + .build()); + } + Some(Duration::from_secs(ttl)) + } else if no_ttl_permanent { + None + } else { + Some(Duration::from_secs(86_400)) + }; + + let expires_at: Option = match live_for { + Some(lf) => Some( + SystemTime::now() + .checked_add(lf) + .ok_or(tide::Error::from_str(StatusCode::BadRequest, "Invalid ttl"))? + ), + None => None, + }; + + let value_bytes = match hex::decode(&data.value) { + Ok(value) => value, + Err(e) => { + return Ok(Response::builder(StatusCode::BadRequest) + .body(format!("Invalid request: {}", e)) + .build()); + } + }; + + let svc = req.state(); + match svc.store_data(&data.key, &value_bytes, expires_at).await { + Ok(()) => Ok(Response::builder(StatusCode::Ok) + .body(serde_json::to_value(&ApiResponse { + message: "Data stored successfully.".to_string(), + })?) + .build()), + Err(e) => Ok(Response::builder(e.status()) + .body(serde_json::to_value(&ApiResponse { + message: e.to_string(), + })?) + .build()), + } +} + +pub async fn handle_retrieve(req: Request) -> tide::Result { + let key: String = req.param("key")?.to_string(); + + if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) { + return Ok(Response::builder(StatusCode::BadRequest) + .body("Invalid key: must be a 32 bytes hex string.".to_string()) + .build()); + } + + let svc = req.state(); + match svc.retrieve_data(&key).await { + Ok(value) => { + let encoded_value = hex::encode(value); + Ok(Response::builder(StatusCode::Ok) + .body(serde_json::to_value(&RetrieveResponse { key, value: encoded_value })?) + .build()) + } + Err(e) => Ok(Response::builder(StatusCode::NotFound).body(e).build()), + } +} + +pub fn create_app(no_ttl_permanent: bool, storage_dir: impl Into) -> tide::Server { + let svc = StorageService::new(storage_dir); + let mut app = tide::with_state(svc); + app.at("/store").post(move |req| handle_store(req, no_ttl_permanent)); + app.at("/retrieve/:key").get(handle_retrieve); + app +} diff --git a/src/main.rs b/src/main.rs index b8b052c..94bf171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,128 +1,12 @@ -use serde::{Deserialize, Serialize}; -use std::time::{Duration, SystemTime}; use std::env; use async_std::task; use async_std::fs::create_dir_all; -use tide::{Request, Response, StatusCode}; -use sdk_storage::StorageService; +use sdk_storage::{StorageService, create_app}; const STORAGE_DIR: &str = "./storage"; const PORT: u16 = 8081; -const MIN_TTL: u64 = 60; const DEFAULT_TTL: u64 = 86400; -const MAX_TTL: u64 = 31_536_000; -#[derive(Deserialize)] -struct StoreRequest { - key: String, - value: String, - ttl: Option, -} - -#[derive(Serialize)] -struct ApiResponse { message: String } - -#[derive(Serialize)] -struct RetrieveResponse { key: String, value: String } - -async fn handle_store(mut req: Request, no_ttl_permanent: bool) -> tide::Result { - // Parse the JSON body - let data: StoreRequest = match req.body_json().await { - Ok(data) => data, - Err(e) => { - return Ok(Response::builder(StatusCode::BadRequest) - .body(format!("Invalid request: {}", e)) - .build()); - } - }; - - // Validate the key - if data.key.len() != 64 || !data.key.chars().all(|c| c.is_ascii_hexdigit()) { - return Ok(Response::builder(StatusCode::BadRequest) - .body("Invalid key: must be a 32 bytes hex string.".to_string()) - .build()); - } - - // Validate the ttl - let live_for: Option = if let Some(ttl) = data.ttl { - if ttl < MIN_TTL { - return Ok(Response::builder(StatusCode::BadRequest) - .body(format!( - "Invalid ttl: must be at least {} seconds.", - MIN_TTL - )) - .build()); - } else if ttl > MAX_TTL { - return Ok(Response::builder(StatusCode::BadRequest) - .body(format!("Invalid ttl: must be at most {} seconds.", MAX_TTL)) - .build()); - } - Some(Duration::from_secs(ttl)) - } else if no_ttl_permanent { - // When no_ttl_permanent is true, requests without TTL are permanent - None - } else { - Some(Duration::from_secs(DEFAULT_TTL)) - }; - - let expires_at: Option = match live_for { - Some(lf) => Some( - SystemTime::now() - .checked_add(lf) - .ok_or(tide::Error::from_str(StatusCode::BadRequest, "Invalid ttl"))? - ), - None => None, - }; - - // Decode hex value - let value_bytes = match hex::decode(&data.value) { - Ok(value) => value, - Err(e) => { - return Ok(Response::builder(StatusCode::BadRequest) - .body(format!("Invalid request: {}", e)) - .build()); - } - }; - - // Store the data - let svc = req.state(); - match svc.store_data(&data.key, &value_bytes, expires_at).await { - Ok(()) => Ok(Response::builder(StatusCode::Ok) - .body(serde_json::to_value(&ApiResponse { - message: "Data stored successfully.".to_string(), - })?) - .build()), - Err(e) => Ok(Response::builder(e.status()) - .body(serde_json::to_value(&ApiResponse { - message: e.to_string(), - })?) - .build()), - } -} - -async fn handle_retrieve(req: Request) -> tide::Result { - let key: String = req.param("key")?.to_string(); - - if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) { - return Ok(Response::builder(StatusCode::BadRequest) - .body("Invalid key: must be a 32 bytes hex string.".to_string()) - .build()); - } - - let svc = req.state(); - match svc.retrieve_data(&key).await { - Ok(value) => { - let encoded_value = hex::encode(value); - Ok(Response::builder(StatusCode::Ok) - .body(serde_json::to_value(&RetrieveResponse { - key, - value: encoded_value, - })?) - .build()) - } - Err(e) => Ok(Response::builder(StatusCode::NotFound).body(e).build()), - } -} #[async_std::main] async fn main() -> tide::Result<()> { @@ -150,9 +34,7 @@ async fn main() -> tide::Result<()> { } }); - let mut app = tide::with_state(svc); - app.at("/store").post(move |req| handle_store(req, no_ttl_permanent)); - app.at("/retrieve/:key").get(handle_retrieve); + let mut app = create_app(no_ttl_permanent, STORAGE_DIR); app.listen(format!("0.0.0.0:{}", PORT)).await?; println!("Server running at http://0.0.0.0:{}", PORT);