Skip to content

Commit 2dc30ac

Browse files
authored
feat(sv-api): Added macros for receipts, inputs and outputs endpoints… (#447)
feat(sv-api): Added macros for receipts, inputs and outputs endpoints generation with docs
1 parent 4830ae5 commit 2dc30ac

File tree

6 files changed

+365
-256
lines changed

6 files changed

+365
-256
lines changed

Cargo.lock

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

services/api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ fuel-tx.workspace = true
3030
fuel-vm.workspace = true
3131
fuel-web-utils.workspace = true
3232
num_cpus.workspace = true
33+
paste = "1.0.15"
3334
prometheus = { version = "0.13", features = ["process"] }
3435
serde.workspace = true
3536
serde_json.workspace = true
Lines changed: 110 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use axum::{
2-
extract::{FromRequest, FromRequestParts, State},
3-
http::{request::Parts, Request},
2+
extract::{FromRequest, State},
3+
http::Request,
44
response::IntoResponse,
55
Json,
66
};
@@ -15,6 +15,7 @@ use fuel_streams_domains::{
1515
inputs::{queryable::InputsQuery, InputType},
1616
queryable::{Queryable, ValidatedQuery},
1717
};
18+
use paste::paste;
1819

1920
use super::open_api::TAG_INPUTS;
2021
use crate::server::{
@@ -23,71 +24,113 @@ use crate::server::{
2324
state::ServerState,
2425
};
2526

26-
pub struct InputTypeVariant(Option<InputType>);
27+
#[macro_export]
28+
macro_rules! generate_input_endpoints {
29+
(
30+
$tag:expr,
31+
$base_path:expr,
32+
$base_name:ident,
33+
$(
34+
$variant:ident => $path_suffix:literal
35+
),*
36+
) => {
37+
paste! {
38+
$(
39+
#[utoipa::path(
40+
get,
41+
path = concat!($base_path, $path_suffix),
42+
tag = $tag,
43+
params(
44+
("txId" = Option<TxId>, Query, description = "Filter by transaction ID"),
45+
("txIndex" = Option<u32>, Query, description = "Filter by transaction index"),
46+
("inputIndex" = Option<i32>, Query, description = "Filter by input index"),
47+
("blockHeight" = Option<BlockHeight>, Query, description = "Filter by block height"),
48+
("ownerId" = Option<Address>, Query, description = "Filter by owner ID (for coin inputs)"),
49+
("assetId" = Option<AssetId>, Query, description = "Filter by asset ID (for coin inputs)"),
50+
("contractId" = Option<ContractId>, Query, description = "Filter by contract ID (for contract inputs)"),
51+
("senderAddress" = Option<Address>, Query, description = "Filter by sender address (for message inputs)"),
52+
("recipientAddress" = Option<Address>, Query, description = "Filter by recipient address (for message inputs)"),
53+
("address" = Option<Address>, Query, description = "Filter by address"),
54+
("after" = Option<i32>, Query, description = "Return inputs after this height"),
55+
("before" = Option<i32>, Query, description = "Return inputs before this height"),
56+
("first" = Option<i32>, Query, description = "Limit results, sorted by ascending block height", maximum = 100),
57+
("last" = Option<i32>, Query, description = "Limit results, sorted by descending block height", maximum = 100)
58+
),
59+
responses(
60+
(status = 200, description = "Successfully retrieved inputs", body = GetDataResponse),
61+
(status = 400, description = "Invalid query parameters", body = String),
62+
(status = 500, description = "Internal server error", body = String)
63+
),
64+
security(
65+
("api_key" = [])
66+
)
67+
)]
68+
pub async fn [<$base_name _ $variant:snake>](
69+
State(state): State<ServerState>,
70+
req: Request<axum::body::Body>,
71+
) -> Result<impl IntoResponse, ApiError> {
72+
let mut query = ValidatedQuery::<InputsQuery>::from_request(req, &state)
73+
.await?
74+
.into_inner();
75+
query.set_input_type(Some(InputType::$variant));
76+
let response: GetDataResponse =
77+
query.execute(&state.db.pool).await?.try_into()?;
78+
Ok(Json(response))
79+
}
80+
)*
2781

28-
impl<S> FromRequestParts<S> for InputTypeVariant
29-
where
30-
S: Send + Sync,
31-
{
32-
type Rejection = ApiError;
33-
34-
async fn from_request_parts(
35-
parts: &mut Parts,
36-
_state: &S,
37-
) -> Result<Self, Self::Rejection> {
38-
let path = parts.uri.path();
39-
let variant = match path {
40-
p if p.ends_with("/message") => Some(InputType::Message),
41-
p if p.ends_with("/contract") => Some(InputType::Contract),
42-
p if p.ends_with("/coin") => Some(InputType::Coin),
43-
_ => None,
44-
};
45-
Ok(InputTypeVariant(variant))
46-
}
82+
// Generic handler for the base path
83+
#[utoipa::path(
84+
get,
85+
path = $base_path,
86+
tag = $tag,
87+
params(
88+
("txId" = Option<TxId>, Query, description = "Filter by transaction ID"),
89+
("txIndex" = Option<u32>, Query, description = "Filter by transaction index"),
90+
("inputIndex" = Option<i32>, Query, description = "Filter by input index"),
91+
("inputType" = Option<InputType>, Query, description = "Filter by input type"),
92+
("blockHeight" = Option<BlockHeight>, Query, description = "Filter by block height"),
93+
("ownerId" = Option<Address>, Query, description = "Filter by owner ID (for coin inputs)"),
94+
("assetId" = Option<AssetId>, Query, description = "Filter by asset ID (for coin inputs)"),
95+
("contractId" = Option<ContractId>, Query, description = "Filter by contract ID (for contract inputs)"),
96+
("senderAddress" = Option<Address>, Query, description = "Filter by sender address (for message inputs)"),
97+
("recipientAddress" = Option<Address>, Query, description = "Filter by recipient address (for message inputs)"),
98+
("address" = Option<Address>, Query, description = "Filter by address"),
99+
("after" = Option<i32>, Query, description = "Return inputs after this height"),
100+
("before" = Option<i32>, Query, description = "Return inputs before this height"),
101+
("first" = Option<i32>, Query, description = "Limit results, sorted by ascending block height", maximum = 100),
102+
("last" = Option<i32>, Query, description = "Limit results, sorted by descending block height", maximum = 100)
103+
),
104+
responses(
105+
(status = 200, description = "Successfully retrieved inputs", body = GetDataResponse),
106+
(status = 400, description = "Invalid query parameters", body = String),
107+
(status = 500, description = "Internal server error", body = String)
108+
),
109+
security(
110+
("api_key" = [])
111+
)
112+
)]
113+
pub async fn $base_name(
114+
State(state): State<ServerState>,
115+
req: Request<axum::body::Body>,
116+
) -> Result<impl IntoResponse, ApiError> {
117+
let mut query = ValidatedQuery::<InputsQuery>::from_request(req, &state)
118+
.await?
119+
.into_inner();
120+
query.set_input_type(None);
121+
let response: GetDataResponse =
122+
query.execute(&state.db.pool).await?.try_into()?;
123+
Ok(Json(response))
124+
}
125+
}
126+
};
47127
}
48128

49-
#[utoipa::path(
50-
get,
51-
path = "/inputs",
52-
tag = TAG_INPUTS,
53-
params(
54-
("txId" = Option<TxId>, Query, description = "Filter by transaction ID"),
55-
("txIndex" = Option<u32>, Query, description = "Filter by transaction index"),
56-
("inputIndex" = Option<i32>, Query, description = "Filter by input index"),
57-
("inputType" = Option<InputType>, Query, description = "Filter by input type"),
58-
("blockHeight" = Option<BlockHeight>, Query, description = "Filter by block height"),
59-
("ownerId" = Option<Address>, Query, description = "Filter by owner ID (for coin inputs)"),
60-
("assetId" = Option<AssetId>, Query, description = "Filter by asset ID (for coin inputs)"),
61-
("contractId" = Option<ContractId>, Query, description = "Filter by contract ID (for contract inputs)"),
62-
("senderAddress" = Option<Address>, Query, description = "Filter by sender address (for message inputs)"),
63-
("recipientAddress" = Option<Address>, Query, description = "Filter by recipient address (for message inputs)"),
64-
("address" = Option<Address>, Query, description = "Filter by address"),
65-
("after" = Option<i32>, Query, description = "Return inputs after this height"),
66-
("before" = Option<i32>, Query, description = "Return inputs before this height"),
67-
("first" = Option<i32>, Query, description = "Limit results, sorted by ascending block height", maximum = 100),
68-
("last" = Option<i32>, Query, description = "Limit results, sorted by descending block height", maximum = 100)
69-
),
70-
responses(
71-
(status = 200, description = "Successfully retrieved inputs", body = GetDataResponse),
72-
(status = 400, description = "Invalid query parameters", body = String),
73-
(status = 500, description = "Internal server error", body = String)
74-
),
75-
security(
76-
("api_key" = [])
77-
)
78-
)]
79-
pub async fn get_inputs(
80-
State(state): State<ServerState>,
81-
variant: InputTypeVariant,
82-
req: Request<axum::body::Body>,
83-
) -> Result<impl IntoResponse, ApiError> {
84-
let mut query = ValidatedQuery::<InputsQuery>::from_request(req, &state)
85-
.await?
86-
.into_inner();
87-
if let Some(input_type) = variant.0 {
88-
query.set_input_type(Some(input_type));
89-
}
90-
let response: GetDataResponse =
91-
query.execute(&state.db.pool).await?.try_into()?;
92-
Ok(Json(response))
93-
}
129+
generate_input_endpoints!(
130+
TAG_INPUTS,
131+
"/inputs",
132+
get_inputs,
133+
Message => "/message",
134+
Contract => "/contract",
135+
Coin => "/coin"
136+
);

0 commit comments

Comments
 (0)