From 30a52ac1972cf61041727090f2d3a7f7a0093629 Mon Sep 17 00:00:00 2001 From: canonbrother Date: Mon, 4 Dec 2023 17:37:56 +0800 Subject: [PATCH 1/3] add ApiPageResponse + impl json on list_blocks --- lib/Cargo.lock | 16 +++ lib/Cargo.toml | 2 +- lib/ain-ocean/Cargo.toml | 5 +- lib/ain-ocean/src/api/block.rs | 63 ++++++++++- lib/ain-ocean/src/api_paged_response.rs | 140 ++++++++++++++++++++++++ lib/ain-ocean/src/error.rs | 19 ++++ lib/ain-ocean/src/lib.rs | 2 + 7 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 lib/ain-ocean/src/api_paged_response.rs create mode 100644 lib/ain-ocean/src/error.rs diff --git a/lib/Cargo.lock b/lib/Cargo.lock index eba2ebc6b5..d4fa4c6d06 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -214,7 +214,10 @@ version = "0.1.0" dependencies = [ "axum 0.7.1", "hyper 0.14.27", + "keccak-hash", + "log", "serde", + "thiserror", ] [[package]] @@ -423,6 +426,7 @@ checksum = "810a80b128d70e6ed2bdf3fe8ed72c0ae56f5f5948d01c2753282dd92a84fce8" dependencies = [ "async-trait", "axum-core 0.4.0", + "axum-macros", "bytes", "futures-util", "http 1.0.0", @@ -485,6 +489,18 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2edad600410b905404c594e2523549f1bcd4bded1e252c8f74524ccce0b867" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "backtrace" version = "0.3.69" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 8a2f5d2687..aaf1ed6cd8 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -76,7 +76,7 @@ jsonrpsee-core = "0.16" jsonrpsee-server = "0.16" jsonrpsee-types = "0.16" -axum = "0.7.1" +axum = { version = "0.7.1", features = ["macros"] } tempdir = "0.3" diff --git a/lib/ain-ocean/Cargo.toml b/lib/ain-ocean/Cargo.toml index 4ebf0ea35d..bc5204d243 100644 --- a/lib/ain-ocean/Cargo.toml +++ b/lib/ain-ocean/Cargo.toml @@ -7,5 +7,8 @@ edition = "2021" [dependencies] axum.workspace = true -serde.workspace = true hyper.workspace = true +keccak-hash.workspace = true +log.workspace = true +serde.workspace = true +thiserror.workspace = true diff --git a/lib/ain-ocean/src/api/block.rs b/lib/ain-ocean/src/api/block.rs index 19834e1eda..1d487d40f2 100644 --- a/lib/ain-ocean/src/api/block.rs +++ b/lib/ain-ocean/src/api/block.rs @@ -1,5 +1,7 @@ -use axum::{extract::Path, routing::get, Router}; -use serde::Deserialize; +use axum::{debug_handler, extract::Path, Json, routing::get, Router}; +use serde::{Deserialize, Serialize}; + +use crate::api_paged_response::ApiPagedResponse; #[derive(Deserialize)] struct BlockId { @@ -11,8 +13,29 @@ struct BlockHash { hash: String, } -async fn list_blocks() -> String { - "List of blocks".to_string() +#[derive(Deserialize)] +struct ListBlocksRequest { + size: usize, + next: Option +} + +#[debug_handler] +async fn list_blocks(Json(req): Json) -> Json> { + // TODO(): query from db + // db::block::list(req).await... + let blocks = vec![ + Block { id: "0".into() }, + Block { id: "1".into() }, + Block { id: "2".into() }, + ]; + + Json(ApiPagedResponse + ::of( + blocks, + req.size, + |block| block.clone().id + ) + ) } async fn get_block(Path(BlockId { id }): Path) -> String { @@ -29,3 +52,35 @@ pub fn router() -> Router { .route("/:id", get(get_block)) .route("/:hash/transactions", get(get_transactions)) } + +#[derive(Clone, Debug, Serialize)] +#[serde(default)] +pub struct Block { + id: String, + // TODO(): type mapping + // hash: H256, + // previous_hash: H256, + + // height: u64, + // version: u64, + // time: u64, // ---------------| block time in seconds since epoch + // median_time: u64, // --------| meidan time of the past 11 block timestamps + + // transaction_count: u64, + + // difficulty: u64, + + // masternode: H256, + // minter: H256, + // minter_block_count: u64, + // reward: f64 + + // state_modifier: H256, + // merkle_root: H256, + + // size: u64, + // size_stripped: u64, + // weight: u64, +} + + diff --git a/lib/ain-ocean/src/api_paged_response.rs b/lib/ain-ocean/src/api_paged_response.rs new file mode 100644 index 0000000000..4d3b7aab2c --- /dev/null +++ b/lib/ain-ocean/src/api_paged_response.rs @@ -0,0 +1,140 @@ +use serde::Serialize; + +/// ApiPagedResponse indicates that this response of data array slice is part of a sorted list of items. +/// Items are part of a larger sorted list and the slice indicates a window within the large sorted list. +/// Each ApiPagedResponse holds the data array and the "token" for next part of the slice. +/// The next token should be passed via query 'next' and only used when getting the next slice. +/// Hence the first request, the next token is always empty and not provided. +/// +/// With ascending sorted list and a limit of 3 items per slice will have the behaviour as such. +/// +/// SORTED : | [1] [2] [3] | [4] [5] [6] | [7] [8] [9] | [10] +/// Query 1 : Data: [1] [2] [3], Next: 3, Operator: GT (>) +/// Query 2 : Data: [4] [5] [6], Next: 6, Operator: GT (>) +/// Query 3 : Data: [7] [8] [9], Next: 3, Operator: GT (>) +/// Query 4 : Data: [10], Next: undefined +/// +/// This design is resilient also mutating sorted list, where pagination is not. +/// +/// SORTED : [2] [4] [6] [8] [10] [12] [14] +/// Query 1 : Data: [2] [4] [6], Next: 6, Operator: GT (>) +/// +/// Being in a slice window, the larger sorted list can be mutated. +/// You only need the next token to get the next slice. +/// MUTATED : [2] [4] [7] [8] [9] [10] [12] [14] +/// Query 2 : Data: [7] [8] [9], Next: 6, Operator: GT (>) +/// +/// Limitations of this requires your dat astructure to always be sorted in one direction and your sort +/// indexes always fixed. Hence the moving down of the slice window, your operator will be greater than (GT). +/// While moving up your operator will be less than (GT). +/// +/// ASC : | [1] [2] [3] | [4] [5] [6] | [7] [8] [9] | +/// >3 >6 >9 +/// DESC : | [9] [8] [7] | [6] [5] [4] | [3] [2] [1] | +/// <7 <4 <1 +/// For developer quality life it's unwise to allow inclusive operator, it just creates more overhead +/// to understanding our services. No GTE or LTE, always GT and LE. Services must beclean and clear, +/// when the usage narrative is clear and so will the use of ease. LIST query must be dead simple. +/// Image travelling down the path, and getting a "next token" to get the next set of itmes to +/// continue walking. +/// +/// Because the limit is not part of the slice window your query mechanism should support varying size windows. +/// +/// DATA: | [1] [2] [3] | [4] [5] [6] [7] | [8] [9] | ... +/// | limit 3, >3 | limit 4, >7 | limit 2, >9 +/// For simplicity your API should not attempt to allow access to different sort indexes, be cognizant of +/// how our APIs are consumed. If we create a GET /blocks operation to list blocks what would the correct indexes +/// be 99% of the time? +/// +/// Answer: Blocks sorted by height in descending order, that's your sorted list and your slice window. +/// : <- Latest | [100] [99] [98] [97] [...] | Oldest -> +/// +#[derive(Debug, Serialize, PartialEq)] +pub struct ApiPagedResponse { + data: Vec, + page: ApiPage, +} + +#[derive(Debug, Serialize, PartialEq)] +struct ApiPage { + next: Option, +} + +impl ApiPagedResponse { + pub fn new(data: Vec, next: Option) -> Self { + Self { data, page: ApiPage{ next } } + } + + pub fn next(data: Vec, next: Option) -> Self { + Self::new(data, next) + } + + pub fn of(data: Vec, limit: usize, next_provider: impl Fn(&T) -> String) -> Self { + if data.len() == limit && data.len() > 0 && limit > 0 { + let next = next_provider(&data[limit - 1]); + Self::next(data, Some(next)) + } else { + Self::next(data, None) + } + } + + pub fn empty() -> Self { + Self::new(Vec::new(), None) + } +} + +#[cfg(test)] +mod tests { + use super::{ApiPagedResponse, ApiPage}; + + struct Item { + id: String, + sort: String, + } + + #[test] + fn should_next_with_none() { + let items: Vec = vec![ + Item{id: "1".into(), sort: "a".into()}, + Item{id: "2".into(), sort: "b".into()}, + ]; + + let next = ApiPagedResponse::next(items, None).page.next; + assert_eq!(next, None); + } + + #[test] + fn should_next_with_value() { + let items: Vec = vec![ + Item{id: "1".into(), sort: "a".into()}, + Item{id: "2".into(), sort: "b".into()}, + ]; + + let next = ApiPagedResponse::next(items, Some("b".into())).page.next; + assert_eq!(next, Some("b".into())); + } + + #[test] + fn should_of_with_limit_3() { + let items: Vec = vec![ + Item{id: "1".into(), sort: "a".into()}, + Item{id: "2".into(), sort: "b".into()}, + Item{id: "3".into(), sort: "c".into()}, + ]; + + let next = ApiPagedResponse::of(items, 3, |item| item.sort.to_owned()).page.next; + assert_eq!(next, Some("c".into())) + } + + #[test] + fn should_not_create_with_limit_3_while_size_2() { + let items: Vec = vec![ + Item{id: "1".into(), sort: "a".into()}, + Item{id: "2".into(), sort: "b".into()}, + ]; + + let page = ApiPagedResponse::of(items, 3, |item| item.sort.to_owned()).page; + assert_eq!(page, ApiPage{next: None}) + } + +} diff --git a/lib/ain-ocean/src/error.rs b/lib/ain-ocean/src/error.rs new file mode 100644 index 0000000000..7064679280 --- /dev/null +++ b/lib/ain-ocean/src/error.rs @@ -0,0 +1,19 @@ +use axum::{http::StatusCode, response::{IntoResponse, Response}}; +use thiserror::Error; + +pub type OceanResult = Result; + +#[derive(Error, Debug)] +pub enum OceanError { +} + +impl IntoResponse for OceanError { + fn into_response(self) -> Response { + let code: StatusCode = match self { + // OceanError::SomeError => StatusCode::SomeCode, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + let reason = self.to_string(); + (code, reason).into_response() + } +} diff --git a/lib/ain-ocean/src/lib.rs b/lib/ain-ocean/src/lib.rs index 2dae6056be..7ee6252f32 100644 --- a/lib/ain-ocean/src/lib.rs +++ b/lib/ain-ocean/src/lib.rs @@ -1,4 +1,6 @@ mod api; +pub mod api_paged_response; +pub mod error; mod model; pub use api::ocean_router; From 3989b71f44ee04d35aa4a2b816e9ad2bad8f9f90 Mon Sep 17 00:00:00 2001 From: canonbrother Date: Wed, 6 Dec 2023 15:45:46 +0800 Subject: [PATCH 2/3] refine --- lib/ain-ocean/src/api/block.rs | 10 +++--- lib/ain-ocean/src/api_paged_response.rs | 42 +++++++++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/ain-ocean/src/api/block.rs b/lib/ain-ocean/src/api/block.rs index 1d487d40f2..619f3d1e23 100644 --- a/lib/ain-ocean/src/api/block.rs +++ b/lib/ain-ocean/src/api/block.rs @@ -14,9 +14,9 @@ struct BlockHash { } #[derive(Deserialize)] -struct ListBlocksRequest { - size: usize, - next: Option +pub struct ListBlocksRequest { + pub size: usize, + pub next: Option } #[debug_handler] @@ -33,7 +33,7 @@ async fn list_blocks(Json(req): Json) -> Json ApiPagedResponse { - pub fn new(data: Vec, next: Option) -> Self { - Self { data, page: ApiPage{ next } } + pub fn new(data: Vec, next: Option<&str>) -> Self { + Self { data, page: ApiPage{ next: next.map(Into::into) } } // Option<&str> -> Option } - pub fn next(data: Vec, next: Option) -> Self { + pub fn next(data: Vec, next: Option<&str>) -> Self { Self::new(data, next) } pub fn of(data: Vec, limit: usize, next_provider: impl Fn(&T) -> String) -> Self { if data.len() == limit && data.len() > 0 && limit > 0 { let next = next_provider(&data[limit - 1]); - Self::next(data, Some(next)) + Self::next(data, Some(next.as_str())) } else { Self::next(data, None) } @@ -87,16 +87,26 @@ impl ApiPagedResponse { mod tests { use super::{ApiPagedResponse, ApiPage}; + #[derive(Clone, Debug)] struct Item { id: String, sort: String, } + impl Item { + fn new(id: &str, sort: &str) -> Self { + Self { + id: id.into(), + sort: sort.into(), + } + } + } + #[test] fn should_next_with_none() { let items: Vec = vec![ - Item{id: "1".into(), sort: "a".into()}, - Item{id: "2".into(), sort: "b".into()}, + Item::new("0", "a"), + Item::new("1", "b"), ]; let next = ApiPagedResponse::next(items, None).page.next; @@ -106,34 +116,34 @@ mod tests { #[test] fn should_next_with_value() { let items: Vec = vec![ - Item{id: "1".into(), sort: "a".into()}, - Item{id: "2".into(), sort: "b".into()}, + Item::new("0", "a"), + Item::new("1", "b"), ]; - let next = ApiPagedResponse::next(items, Some("b".into())).page.next; + let next = ApiPagedResponse::next(items, Some("b")).page.next; assert_eq!(next, Some("b".into())); } #[test] fn should_of_with_limit_3() { let items: Vec = vec![ - Item{id: "1".into(), sort: "a".into()}, - Item{id: "2".into(), sort: "b".into()}, - Item{id: "3".into(), sort: "c".into()}, + Item::new("0", "a"), + Item::new("1", "b"), + Item::new("2", "c"), ]; - let next = ApiPagedResponse::of(items, 3, |item| item.sort.to_owned()).page.next; + let next = ApiPagedResponse::of(items, 3, |item| item.clone().sort).page.next; assert_eq!(next, Some("c".into())) } #[test] fn should_not_create_with_limit_3_while_size_2() { let items: Vec = vec![ - Item{id: "1".into(), sort: "a".into()}, - Item{id: "2".into(), sort: "b".into()}, + Item::new("0", "a"), + Item::new("1", "b"), ]; - let page = ApiPagedResponse::of(items, 3, |item| item.sort.to_owned()).page; + let page = ApiPagedResponse::of(items, 3, |item| item.clone().sort).page; assert_eq!(page, ApiPage{next: None}) } From 38f7754cd8945feeb83b9a84ca8d9ac8cf80abe4 Mon Sep 17 00:00:00 2001 From: canonbrother Date: Wed, 6 Dec 2023 17:01:51 +0800 Subject: [PATCH 3/3] body -> query --- lib/ain-ocean/src/api/block.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ain-ocean/src/api/block.rs b/lib/ain-ocean/src/api/block.rs index 619f3d1e23..142d379dce 100644 --- a/lib/ain-ocean/src/api/block.rs +++ b/lib/ain-ocean/src/api/block.rs @@ -1,4 +1,4 @@ -use axum::{debug_handler, extract::Path, Json, routing::get, Router}; +use axum::{debug_handler, extract::{Path, Query}, Json, routing::get, Router}; use serde::{Deserialize, Serialize}; use crate::api_paged_response::ApiPagedResponse; @@ -14,13 +14,13 @@ struct BlockHash { } #[derive(Deserialize)] -pub struct ListBlocksRequest { +pub struct ListBlocksQuery { pub size: usize, pub next: Option } #[debug_handler] -async fn list_blocks(Json(req): Json) -> Json> { +async fn list_blocks(Query(query): Query) -> Json> { // TODO(): query from db // db::block::list(req).await... let blocks = vec![ @@ -32,7 +32,7 @@ async fn list_blocks(Json(req): Json) -> Json