diff --git a/Cargo.lock b/Cargo.lock index 4eba214..e79677c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,7 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", + "headers", "http", "http-body", "hyper", @@ -202,6 +203,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.20.0" @@ -220,6 +227,15 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "brotli" version = "3.3.4" @@ -337,6 +353,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -346,6 +371,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctrlc" version = "3.4.0" @@ -356,6 +391,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -455,6 +500,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -472,6 +527,31 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -623,7 +703,7 @@ dependencies = [ [[package]] name = "jserver" -version = "0.1.0" +version = "0.1.1" dependencies = [ "axum", "chrono", @@ -960,6 +1040,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1110,7 +1201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8bd22a874a2d0b70452d5597b12c537331d49060824a95f49f108994f94aa4c" dependencies = [ "async-compression", - "base64", + "base64 0.20.0", "bitflags 2.3.3", "bytes", "futures-core", @@ -1172,6 +1263,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicase" version = "2.6.0" diff --git a/Cargo.toml b/Cargo.toml index bd36d33..f80d838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,13 @@ name = "jserver" authors = ["JupiterGao "] description = "A json api and static files server" -version = "0.1.0" +version = "0.1.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = { version = "0.6.18" } +axum = { version = "0.6.18", features = ["headers"] } chrono = { version = "0.4.26", features = ["serde"] } clap = { version = "4.3.10", features = ["derive"] } ctrlc = "3.4.0" diff --git a/README.md b/README.md index 074297a..af52061 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,43 @@ PUT /api/profile PATCH /api/profile ``` +### Paginate + +Use `_page` and optionally `_size` to paginate returned data. + +``` +GET /posts?_page=7 +GET /posts?_page=7&_size=20 +``` + +_20 items are returned by default, page is 1 based(0 is treated as 1)_ + +### Sort + +Add `_sort` and `_order` (ascending order by default) + +``` +GET /posts?_sort=views&_order=asc +GET /posts/1/comments?_sort=votes&_order=asc +``` + +For multiple fields, use the following format: + +``` +GET /posts?_sort=user,views&_order=desc,asc +``` + +### Slice + +Add `_start` and (`_end` or `_limit`) + +``` +GET /posts?_start=20&_end=30 +GET /posts?_start=20&_limit=10 +``` + +An `X-Total-Count` header is included in the array response + ### Database ``` diff --git a/src/handler/array.rs b/src/handler/array.rs index 7c70036..d35b5d5 100644 --- a/src/handler/array.rs +++ b/src/handler/array.rs @@ -1,21 +1,156 @@ +use std::cmp::Ordering; + use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, http::{StatusCode, Uri}, + response::{IntoResponse, Response}, Json, }; -use serde_json::Value; +use serde::Deserialize; +use serde_json::{json, Value}; use super::{get_name, AppState}; -pub async fn list(uri: Uri, State(app_state): State) -> String { +pub async fn list( + uri: Uri, + paginate: Option>, + sort: Option>, + slice: Option>, + State(app_state): State, +) -> impl IntoResponse { let name = get_name(uri); - app_state - .db_value - .read() - .await - .get(&name) + let db_value = app_state.db_value.read().await; + let values = db_value.get(&name).unwrap(); + if !values.is_array() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body::("key is not array".into()) + .unwrap(); + } + let (sorts, orders) = if let Some(sort) = sort { + let mut sorts = sort + .sort + .split(',') + .map(|i| i.to_string()) + .collect::>(); + let mut orders = sort + .order + .split(',') + .map(|i| i.to_string()) + .collect::>(); + if sorts.len() != orders.len() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("sort and order length not match".into()) + .unwrap(); + } + sorts.reverse(); + orders.reverse(); + (sorts, orders) + } else { + (Vec::new(), Vec::new()) + }; + + if paginate.is_some() && slice.is_some() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("paginate and slice can not use together".into()) + .unwrap(); + } + + if let Some(slice) = slice.clone() { + if slice.end.is_none() && slice.limit.is_none() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("slice end and limit can not both be none".into()) + .unwrap(); + } + if let Some(end) = slice.end { + if slice.start > end { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("slice start must less than end".into()) + .unwrap(); + } + } + if let Some(limit) = slice.limit { + if limit == 0 { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body("slice limit can not be zero".into()) + .unwrap(); + } + } + } + + let mut values_clone = values.clone(); + let values = values_clone.as_array_mut().unwrap(); + + //1、filter + //2、sort + for (sort, order) in sorts.iter().zip(orders.iter()) { + values.sort_by(|a, b| { + let a = a.get(sort).unwrap(); + let b = b.get(sort).unwrap(); + if a.is_number() && b.is_number() { + let a = a.as_f64().unwrap(); + let b = b.as_f64().unwrap(); + if order == "asc" { + a.partial_cmp(&b).unwrap() + } else { + b.partial_cmp(&a).unwrap() + } + } else if a.is_string() && b.is_string() { + let a = a.as_str().unwrap(); + let b = b.as_str().unwrap(); + if order == "asc" { + a.partial_cmp(b).unwrap() + } else { + b.partial_cmp(a).unwrap() + } + } else { + Ordering::Equal + } + }); + } + //3、page or slice + let (start, end) = if let Some(paginate) = paginate { + ( + (if paginate.page > 0 { + paginate.page - 1 + } else { + 0 + }) * paginate.size.unwrap_or(20), + (if paginate.page > 0 { + paginate.page - 1 + } else { + 0 + }) * paginate.size.unwrap_or(20) + + paginate.size.unwrap_or(20), + ) + } else if let Some(slice) = slice { + if let Some(end) = slice.end { + (slice.start, end) + } else { + (slice.start, slice.start + slice.limit.unwrap()) + } + } else { + (0, 20) + }; + + let body = if start >= values.len() { + json!(Vec::::new()).to_string() + } else if end > values.len() { + json!(values[start..].to_vec()).to_string() + } else { + json!(values[start..end].to_vec()).to_string() + }; + + Response::builder() + .status(StatusCode::OK) + .header("X-Total-Count", values.len().to_string()) + .body(body) .unwrap() - .to_string() } pub async fn get_item_by_id( @@ -170,3 +305,29 @@ pub async fn delete_item_by_id( )) } } + +#[derive(Deserialize, Clone)] +pub struct Paginate { + #[serde(rename = "_page")] + pub page: usize, + #[serde(rename = "_size")] + pub size: Option, +} + +#[derive(Deserialize)] +pub struct Sort { + #[serde(rename = "_sort")] + pub sort: String, + #[serde(rename = "_order")] + pub order: String, +} + +#[derive(Deserialize, Clone)] +pub struct Slice { + #[serde(rename = "_start")] + pub start: usize, + #[serde(rename = "_end")] + pub end: Option, + #[serde(rename = "_limit")] + pub limit: Option, +} diff --git a/src/handler/mod.rs b/src/handler/mod.rs index d7f7713..4a5e19f 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -4,7 +4,10 @@ use axum::{ routing::{delete, get, patch, post, put}, Router, }; -use tower_http::services::ServeDir; +use tower_http::{ + cors::{Any, CorsLayer}, + services::ServeDir, +}; use crate::AppState; @@ -45,6 +48,7 @@ pub async fn build_router(app_state: AppState, public_path: &str) -> Router { .route("/db", get(db)) .nest("/api", api_routers) .fallback_service(ServeDir::new(public_path)) + .layer(CorsLayer::new().allow_methods(Any).allow_origin(Any)) .with_state(app_state.clone()) }