Skip to content

Commit b954e3c

Browse files
canonbrothernagarajm22
authored andcommitted
Ocean list pagination + json req/res (#2737)
* add ApiPageResponse + impl json on list_blocks * refine * body -> query
1 parent ab68f87 commit b954e3c

7 files changed

Lines changed: 249 additions & 6 deletions

File tree

lib/Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jsonrpsee-core = "0.16"
7676
jsonrpsee-server = "0.16"
7777
jsonrpsee-types = "0.16"
7878

79-
axum = "0.7.1"
79+
axum = { version = "0.7.1", features = ["macros"] }
8080

8181
tempdir = "0.3"
8282

lib/ain-ocean/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ edition = "2021"
77

88
[dependencies]
99
axum.workspace = true
10-
serde.workspace = true
1110
hyper.workspace = true
11+
keccak-hash.workspace = true
12+
log.workspace = true
13+
serde.workspace = true
14+
thiserror.workspace = true

lib/ain-ocean/src/api/block.rs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use axum::{extract::Path, routing::get, Router};
2-
use serde::Deserialize;
1+
use axum::{debug_handler, extract::{Path, Query}, Json, routing::get, Router};
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::api_paged_response::ApiPagedResponse;
35

46
#[derive(Deserialize)]
57
struct BlockId {
@@ -11,8 +13,29 @@ struct BlockHash {
1113
hash: String,
1214
}
1315

14-
async fn list_blocks() -> String {
15-
"List of blocks".to_string()
16+
#[derive(Deserialize)]
17+
pub struct ListBlocksQuery {
18+
pub size: usize,
19+
pub next: Option<String>
20+
}
21+
22+
#[debug_handler]
23+
async fn list_blocks(Query(query): Query<ListBlocksQuery>) -> Json<ApiPagedResponse<Block>> {
24+
// TODO(): query from db
25+
// db::block::list(req).await...
26+
let blocks = vec![
27+
Block { id: "0".into() },
28+
Block { id: "1".into() },
29+
Block { id: "2".into() },
30+
];
31+
32+
Json(ApiPagedResponse
33+
::of(
34+
blocks,
35+
query.size,
36+
|block| block.clone().id
37+
)
38+
)
1639
}
1740

1841
async fn get_block(Path(BlockId { id }): Path<BlockId>) -> String {
@@ -29,3 +52,33 @@ pub fn router() -> Router {
2952
.route("/:id", get(get_block))
3053
.route("/:hash/transactions", get(get_transactions))
3154
}
55+
56+
#[derive(Clone, Debug, Serialize)]
57+
#[serde(default)]
58+
pub struct Block {
59+
id: String,
60+
// TODO(): type mapping
61+
// hash: H256,
62+
// previous_hash: H256,
63+
64+
// height: u64,
65+
// version: u64,
66+
// time: u64, // ---------------| block time in seconds since epoch
67+
// median_time: u64, // --------| meidan time of the past 11 block timestamps
68+
69+
// transaction_count: u64,
70+
71+
// difficulty: u64,
72+
73+
// masternode: H256,
74+
// minter: H256,
75+
// minter_block_count: u64,
76+
// reward: f64
77+
78+
// state_modifier: H256,
79+
// merkle_root: H256,
80+
81+
// size: u64,
82+
// size_stripped: u64,
83+
// weight: u64,
84+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use serde::Serialize;
2+
3+
/// ApiPagedResponse indicates that this response of data array slice is part of a sorted list of items.
4+
/// Items are part of a larger sorted list and the slice indicates a window within the large sorted list.
5+
/// Each ApiPagedResponse holds the data array and the "token" for next part of the slice.
6+
/// The next token should be passed via query 'next' and only used when getting the next slice.
7+
/// Hence the first request, the next token is always empty and not provided.
8+
///
9+
/// With ascending sorted list and a limit of 3 items per slice will have the behaviour as such.
10+
///
11+
/// SORTED : | [1] [2] [3] | [4] [5] [6] | [7] [8] [9] | [10]
12+
/// Query 1 : Data: [1] [2] [3], Next: 3, Operator: GT (>)
13+
/// Query 2 : Data: [4] [5] [6], Next: 6, Operator: GT (>)
14+
/// Query 3 : Data: [7] [8] [9], Next: 3, Operator: GT (>)
15+
/// Query 4 : Data: [10], Next: undefined
16+
///
17+
/// This design is resilient also mutating sorted list, where pagination is not.
18+
///
19+
/// SORTED : [2] [4] [6] [8] [10] [12] [14]
20+
/// Query 1 : Data: [2] [4] [6], Next: 6, Operator: GT (>)
21+
///
22+
/// Being in a slice window, the larger sorted list can be mutated.
23+
/// You only need the next token to get the next slice.
24+
/// MUTATED : [2] [4] [7] [8] [9] [10] [12] [14]
25+
/// Query 2 : Data: [7] [8] [9], Next: 6, Operator: GT (>)
26+
///
27+
/// Limitations of this requires your dat astructure to always be sorted in one direction and your sort
28+
/// indexes always fixed. Hence the moving down of the slice window, your operator will be greater than (GT).
29+
/// While moving up your operator will be less than (GT).
30+
///
31+
/// ASC : | [1] [2] [3] | [4] [5] [6] | [7] [8] [9] |
32+
/// >3 >6 >9
33+
/// DESC : | [9] [8] [7] | [6] [5] [4] | [3] [2] [1] |
34+
/// <7 <4 <1
35+
/// For developer quality life it's unwise to allow inclusive operator, it just creates more overhead
36+
/// to understanding our services. No GTE or LTE, always GT and LE. Services must beclean and clear,
37+
/// when the usage narrative is clear and so will the use of ease. LIST query must be dead simple.
38+
/// Image travelling down the path, and getting a "next token" to get the next set of itmes to
39+
/// continue walking.
40+
///
41+
/// Because the limit is not part of the slice window your query mechanism should support varying size windows.
42+
///
43+
/// DATA: | [1] [2] [3] | [4] [5] [6] [7] | [8] [9] | ...
44+
/// | limit 3, >3 | limit 4, >7 | limit 2, >9
45+
/// For simplicity your API should not attempt to allow access to different sort indexes, be cognizant of
46+
/// how our APIs are consumed. If we create a GET /blocks operation to list blocks what would the correct indexes
47+
/// be 99% of the time?
48+
///
49+
/// Answer: Blocks sorted by height in descending order, that's your sorted list and your slice window.
50+
/// : <- Latest | [100] [99] [98] [97] [...] | Oldest ->
51+
///
52+
#[derive(Debug, Serialize, PartialEq)]
53+
pub struct ApiPagedResponse<T> {
54+
data: Vec<T>,
55+
page: ApiPage,
56+
}
57+
58+
#[derive(Debug, Serialize, PartialEq)]
59+
struct ApiPage {
60+
next: Option<String>,
61+
}
62+
63+
impl<T> ApiPagedResponse<T> {
64+
pub fn new(data: Vec<T>, next: Option<&str>) -> Self {
65+
Self { data, page: ApiPage{ next: next.map(Into::into) } } // Option<&str> -> Option<String>
66+
}
67+
68+
pub fn next(data: Vec<T>, next: Option<&str>) -> Self {
69+
Self::new(data, next)
70+
}
71+
72+
pub fn of(data: Vec<T>, limit: usize, next_provider: impl Fn(&T) -> String) -> Self {
73+
if data.len() == limit && data.len() > 0 && limit > 0 {
74+
let next = next_provider(&data[limit - 1]);
75+
Self::next(data, Some(next.as_str()))
76+
} else {
77+
Self::next(data, None)
78+
}
79+
}
80+
81+
pub fn empty() -> Self {
82+
Self::new(Vec::new(), None)
83+
}
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use super::{ApiPagedResponse, ApiPage};
89+
90+
#[derive(Clone, Debug)]
91+
struct Item {
92+
id: String,
93+
sort: String,
94+
}
95+
96+
impl Item {
97+
fn new(id: &str, sort: &str) -> Self {
98+
Self {
99+
id: id.into(),
100+
sort: sort.into(),
101+
}
102+
}
103+
}
104+
105+
#[test]
106+
fn should_next_with_none() {
107+
let items: Vec<Item> = vec![
108+
Item::new("0", "a"),
109+
Item::new("1", "b"),
110+
];
111+
112+
let next = ApiPagedResponse::next(items, None).page.next;
113+
assert_eq!(next, None);
114+
}
115+
116+
#[test]
117+
fn should_next_with_value() {
118+
let items: Vec<Item> = vec![
119+
Item::new("0", "a"),
120+
Item::new("1", "b"),
121+
];
122+
123+
let next = ApiPagedResponse::next(items, Some("b")).page.next;
124+
assert_eq!(next, Some("b".into()));
125+
}
126+
127+
#[test]
128+
fn should_of_with_limit_3() {
129+
let items: Vec<Item> = vec![
130+
Item::new("0", "a"),
131+
Item::new("1", "b"),
132+
Item::new("2", "c"),
133+
];
134+
135+
let next = ApiPagedResponse::of(items, 3, |item| item.clone().sort).page.next;
136+
assert_eq!(next, Some("c".into()))
137+
}
138+
139+
#[test]
140+
fn should_not_create_with_limit_3_while_size_2() {
141+
let items: Vec<Item> = vec![
142+
Item::new("0", "a"),
143+
Item::new("1", "b"),
144+
];
145+
146+
let page = ApiPagedResponse::of(items, 3, |item| item.clone().sort).page;
147+
assert_eq!(page, ApiPage{next: None})
148+
}
149+
150+
}

lib/ain-ocean/src/error.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use axum::{http::StatusCode, response::{IntoResponse, Response}};
2+
use thiserror::Error;
3+
4+
pub type OceanResult<T> = Result<T, OceanError>;
5+
6+
#[derive(Error, Debug)]
7+
pub enum OceanError {
8+
}
9+
10+
impl IntoResponse for OceanError {
11+
fn into_response(self) -> Response {
12+
let code: StatusCode = match self {
13+
// OceanError::SomeError => StatusCode::SomeCode,
14+
_ => StatusCode::INTERNAL_SERVER_ERROR,
15+
};
16+
let reason = self.to_string();
17+
(code, reason).into_response()
18+
}
19+
}

lib/ain-ocean/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
mod api;
2+
pub mod api_paged_response;
3+
pub mod error;
24
mod model;
35

46
pub use api::ocean_router;

0 commit comments

Comments
 (0)