From 6e3c17c66134c1a6eb3237f07149cad99f2de8eb Mon Sep 17 00:00:00 2001 From: Sild Date: Thu, 25 Jun 2026 14:03:37 +0200 Subject: [PATCH] api: normalize service clients --- AGENTS.md | 9 +-- bidask_api_client/AGENTS.md | 3 +- bidask_api_client/src/api.rs | 71 +++++++++++++++++++ bidask_api_client/src/api_client.rs | 70 +----------------- bidask_api_client/src/api_client/builder.rs | 4 +- bidask_api_client/tests/test_api.rs | 2 +- bidask_api_client/tests/test_construction.rs | 11 ++- dedust_api_client/AGENTS.md | 13 ++-- dedust_api_client/README.md | 8 +-- dedust_api_client/src/api_client.rs | 28 +------- dedust_api_client/src/api_client/builder.rs | 4 +- dedust_api_client/src/api_v2.rs | 7 -- dedust_api_client/src/lib.rs | 2 +- dedust_api_client/src/v2.rs | 39 ++++++++++ .../src/{api_v2 => v2}/request.rs | 0 .../src/{api_v2 => v2}/response.rs | 4 +- dedust_api_client/src/{api_v2 => v2}/types.rs | 0 dedust_api_client/tests/test_api_v2.rs | 12 ++-- dedust_api_client/tests/test_construction.rs | 11 ++- swap_coffee_api_client/AGENTS.md | 13 ++-- swap_coffee_api_client/README.md | 8 +-- swap_coffee_api_client/src/api_client.rs | 20 +----- .../src/api_client/builder.rs | 4 +- swap_coffee_api_client/src/api_v1.rs | 7 -- swap_coffee_api_client/src/lib.rs | 2 +- swap_coffee_api_client/src/v1.rs | 31 ++++++++ .../src/{api_v1 => v1}/request.rs | 0 .../src/{api_v1 => v1}/response.rs | 4 +- .../src/{api_v1 => v1}/types.rs | 0 swap_coffee_api_client/tests/test_api_v1.rs | 8 +-- .../tests/test_construction.rs | 11 ++- tonco_api_client/AGENTS.md | 8 +-- tonco_api_client/README.md | 7 +- tonco_api_client/src/api_client.rs | 33 +-------- tonco_api_client/src/api_client/builder.rs | 4 +- tonco_api_client/src/graphql.rs | 38 ++++++++++ tonco_api_client/src/lib.rs | 1 + tonco_api_client/tests/test_construction.rs | 14 ++++ tonco_api_client/tests/test_graphql.rs | 2 +- 39 files changed, 303 insertions(+), 210 deletions(-) delete mode 100644 dedust_api_client/src/api_v2.rs create mode 100644 dedust_api_client/src/v2.rs rename dedust_api_client/src/{api_v2 => v2}/request.rs (100%) rename dedust_api_client/src/{api_v2 => v2}/response.rs (86%) rename dedust_api_client/src/{api_v2 => v2}/types.rs (100%) delete mode 100644 swap_coffee_api_client/src/api_v1.rs create mode 100644 swap_coffee_api_client/src/v1.rs rename swap_coffee_api_client/src/{api_v1 => v1}/request.rs (100%) rename swap_coffee_api_client/src/{api_v1 => v1}/response.rs (88%) rename swap_coffee_api_client/src/{api_v1 => v1}/types.rs (100%) create mode 100644 tonco_api_client/src/graphql.rs create mode 100644 tonco_api_client/tests/test_construction.rs diff --git a/AGENTS.md b/AGENTS.md index befb7b9..c923ce2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,8 +77,9 @@ change is needed, state why in the final report. - Preserve thin-wrapper behavior. Prefer adding the missing endpoint mapping, request params, response types, and tests over redesigning the client. - Follow the existing crate shape: `api_client.rs`, `api_client/builder.rs`, API module - files such as `api_v1.rs`, `api_v1/request.rs`, `api_v1/response.rs`, and - `api_v1/types.rs`. + files such as `v1.rs`, `v1/request.rs`, `v1/response.rs`, and `v1/types.rs` + for versioned REST APIs. Keep unversioned service groups in a descriptive + module such as `api.rs` or `graphql.rs`. - Use the existing `derive_setters` builder pattern for client configuration. - Prefer typed request parameter structs with serde derives over manual query string construction. @@ -138,8 +139,8 @@ For GraphQL crates: 1. Keep checked-in schema/query files aligned with the generated types used in tests or examples. -2. Preserve the `exec_graphql(operation_name, query)` boundary unless the user - asks for a higher-level wrapper. +2. Preserve the `exec(operation_name, query)` GraphQL boundary on the child + GraphQL client unless the user asks for a higher-level wrapper. 3. Verify operation names and headers against the live endpoint. 4. Document usage through tests or README examples. diff --git a/bidask_api_client/AGENTS.md b/bidask_api_client/AGENTS.md index 3e94555..5a7d384 100644 --- a/bidask_api_client/AGENTS.md +++ b/bidask_api_client/AGENTS.md @@ -14,7 +14,7 @@ changes. The crate currently contains a thin typed client: - `BidaskApiClient::builder().build()?` -- `client.exec_api(&Request::...)` +- `client.api.exec(&Request::...)` - request enum in `api/request.rs` - response enum and models in `api/response.rs` and `api/types.rs` @@ -35,6 +35,7 @@ Treat these as public contracts: - `BidaskApiClient` - `DEFAULT_API_URL` +- `api::ApiClient` - `Request` - `Response` - public response/type structs diff --git a/bidask_api_client/src/api.rs b/bidask_api_client/src/api.rs index 41bacfc..0a33d62 100644 --- a/bidask_api_client/src/api.rs +++ b/bidask_api_client/src/api.rs @@ -2,6 +2,77 @@ mod request; mod response; mod types; +use api_clients_core::{ApiClientsResult, Executor}; +use serde::Serialize; +use std::sync::Arc; + pub use request::*; pub use response::*; pub use types::*; + +#[derive(Clone)] +pub struct ApiClient { + executor: Arc, +} + +impl ApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + pub async fn exec(&self, request: REQUEST) -> ApiClientsResult + where + REQUEST: Into, + { + let response = match request.into() { + Request::Pools => Response::Pools(self.fetch_all_pool_pages().await?), + }; + Ok(response) + } + + async fn fetch_all_pool_pages(&self) -> ApiClientsResult> { + let mut page_number = 1_i64; + let mut total_pages: Option = None; + let mut aggregated_pages = Vec::new(); + + loop { + let params = PaginationParams { page: page_number }; + let PaginatedResponse { + page: page_info, + result: pools, + } = self.executor.exec_get_extra("pools", ¶ms, &[]).await?; + + let page_count = match total_pages { + Some(count) => count, + None => { + let computed = total_pages_from(&page_info); + total_pages = Some(computed); + computed + } + }; + + let is_empty_page = pools.is_empty(); + aggregated_pages.extend_from_slice(&pools); + + if page_number >= page_count || is_empty_page { + break; + } + + page_number += 1; + } + + Ok(aggregated_pages) + } +} + +#[derive(Serialize)] +struct PaginationParams { + page: i64, +} + +fn total_pages_from(page_info: &PageInfo) -> i64 { + if page_info.size <= 0 { + return 1; + } + + let pages = (page_info.total + page_info.size - 1) / page_info.size; + pages.max(1) +} diff --git a/bidask_api_client/src/api_client.rs b/bidask_api_client/src/api_client.rs index 33f2d25..cb53c9b 100644 --- a/bidask_api_client/src/api_client.rs +++ b/bidask_api_client/src/api_client.rs @@ -1,80 +1,16 @@ mod builder; -use crate::api::PageInfo; -use crate::api::PaginatedResponse; -use crate::api::PoolInfo; -use crate::api::Request; -use crate::api::Response; +use crate::api::ApiClient; use crate::api_client::builder::Builder; -use api_clients_core::{ApiClientsResult, Executor}; -use serde::Serialize; -use std::sync::Arc; pub const DEFAULT_API_URL: &str = "https://api.bidask.finance/api"; #[derive(Clone)] +#[non_exhaustive] pub struct BidaskApiClient { - executor: Arc, + pub api: ApiClient, } impl BidaskApiClient { pub fn builder() -> Builder { Builder::new() } - - pub async fn exec_api(&self, request: REQUEST) -> ApiClientsResult - where - REQUEST: Into, - { - let response = match request.into() { - Request::Pools => Response::Pools(self.fetch_all_pool_pages().await?), - }; - Ok(response) - } - - async fn fetch_all_pool_pages(&self) -> ApiClientsResult> { - let mut page_number = 1_i64; - let mut total_pages: Option = None; - let mut aggregated_pages = Vec::new(); - - loop { - let params = PaginationParams { page: page_number }; - let PaginatedResponse { - page: page_info, - result: pools, - } = self.executor.exec_get_extra("pools", ¶ms, &[]).await?; - - let page_count = match total_pages { - Some(count) => count, - None => { - let computed = total_pages_from(&page_info); - total_pages = Some(computed); - computed - } - }; - - let is_empty_page = pools.is_empty(); - aggregated_pages.extend_from_slice(&pools); - - if page_number >= page_count || is_empty_page { - break; - } - - page_number += 1; - } - - Ok(aggregated_pages) - } -} - -#[derive(Serialize)] -struct PaginationParams { - page: i64, -} - -fn total_pages_from(page_info: &PageInfo) -> i64 { - if page_info.size <= 0 { - return 1; - } - - let pages = (page_info.total + page_info.size - 1) / page_info.size; - pages.max(1) } diff --git a/bidask_api_client/src/api_client/builder.rs b/bidask_api_client/src/api_client/builder.rs index 7aa4ddb..77c6d26 100644 --- a/bidask_api_client/src/api_client/builder.rs +++ b/bidask_api_client/src/api_client/builder.rs @@ -1,3 +1,4 @@ +use crate::api::ApiClient; use crate::api_client::{BidaskApiClient, DEFAULT_API_URL}; use api_clients_core::{ApiClientsResult, Executor}; use derive_setters::Setters; @@ -25,6 +26,7 @@ impl Builder { None => Executor::builder(self.api_url).build()?.into(), }; - Ok(BidaskApiClient { executor }) + let api = ApiClient::new(executor); + Ok(BidaskApiClient { api }) } } diff --git a/bidask_api_client/tests/test_api.rs b/bidask_api_client/tests/test_api.rs index c064138..6800722 100644 --- a/bidask_api_client/tests/test_api.rs +++ b/bidask_api_client/tests/test_api.rs @@ -17,7 +17,7 @@ // async fn test_pools() -> Result<()> { // let client = init_env(); // let request = Request::Pools; -// let response = unwrap_response!(Pools, client.exec_api(&request).await?)?; +// let response = unwrap_response!(Pools, client.api.exec(&request).await?)?; // assert_ne!(response, vec![]); // log::debug!("{:?}", response.len()); // Ok(()) diff --git a/bidask_api_client/tests/test_construction.rs b/bidask_api_client/tests/test_construction.rs index bbb06e8..4c9493c 100644 --- a/bidask_api_client/tests/test_construction.rs +++ b/bidask_api_client/tests/test_construction.rs @@ -1,4 +1,13 @@ -use bidask_api_client::api::{PageInfo, PaginatedResponse, PoolInfo, TokenInfo}; +use bidask_api_client::api::{PageInfo, PaginatedResponse, PoolInfo, Request, TokenInfo}; +use bidask_api_client::api_client::BidaskApiClient; + +#[test] +fn test_client_exposes_api_executor() -> anyhow::Result<()> { + let client = BidaskApiClient::builder().build()?; + let future = client.api.exec(Request::Pools); + drop(future); + Ok(()) +} #[test] fn test_response_models_support_default_setter_construction() { diff --git a/dedust_api_client/AGENTS.md b/dedust_api_client/AGENTS.md index 2f8834c..fa1db3c 100644 --- a/dedust_api_client/AGENTS.md +++ b/dedust_api_client/AGENTS.md @@ -14,9 +14,9 @@ changes. The crate exposes a thin typed client: - `DedustApiClient::builder().build()?` -- `client.exec_api_v2(&V2Request::...)` -- request params in `api_v2/request.rs` -- response enums and models in `api_v2/response.rs` and `api_v2/types.rs` +- `client.v2.exec(&V2Request::...)` +- request params in `v2/request.rs` +- response enums and models in `v2/response.rs` and `v2/types.rs` Keep DeDust-specific address formatting and endpoint mapping in this crate. @@ -26,6 +26,7 @@ Treat these as public contracts: - `DedustApiClient` - `DEFAULT_API_V2_URL` +- `V2ApiClient` - `V2Request` - `RoutingPlanParams` - `V2Response` and public response/type structs @@ -34,7 +35,7 @@ Treat these as public contracts: Request parameter and response/model POD structs are `#[non_exhaustive]`; use `Default::default().with_(...)` or request parameter constructors instead of struct literals in downstream examples and integration tests. Pass request -parameters directly to `exec_api_v2` where `Into` is implemented. +parameters directly to `client.v2.exec` where `Into` is implemented. Public enums are `#[non_exhaustive]`; downstream matches need wildcard arms. `RoutingPlanParams::new` maps the zero TON address to `native` and all other @@ -50,8 +51,8 @@ counts, routing amounts, or ordering. ## Downstream Integration Example ```rust -use dedust_api_client::api_v2::{RoutingPlanParams, V2Request, V2Response}; use dedust_api_client::api_client::DedustApiClient; +use dedust_api_client::v2::{RoutingPlanParams, V2Request, V2Response}; # async fn example() -> anyhow::Result<()> { let client = DedustApiClient::builder().build()?; @@ -60,7 +61,7 @@ let params = RoutingPlanParams::new( "0:0000000000000000000000000000000000000000000000000000000000000000", "1000000000", ); -let response = client.exec_api_v2(params).await?; +let response = client.v2.exec(params).await?; match response { V2Response::RoutingPlan(routes) => println!("routes: {}", routes.len()), diff --git a/dedust_api_client/README.md b/dedust_api_client/README.md index 5c00d4b..398d538 100644 --- a/dedust_api_client/README.md +++ b/dedust_api_client/README.md @@ -20,7 +20,7 @@ wildcard arm. ```rust,no_run use dedust_api_client::api_client::DedustApiClient; -use dedust_api_client::api_v2::{RoutingPlanParams, V2Response}; +use dedust_api_client::v2::{RoutingPlanParams, V2Response}; # async fn example() -> Result<(), Box> { let client = DedustApiClient::builder().build()?; @@ -29,7 +29,7 @@ let params = RoutingPlanParams::new( "0:0000000000000000000000000000000000000000000000000000000000000000", "1000000000", ); -let response = client.exec_api_v2(params).await?; +let response = client.v2.exec(params).await?; match response { V2Response::RoutingPlan(routes) => println!("route groups: {}", routes.len()), @@ -70,8 +70,8 @@ addresses to `jetton:
`. Public request and response types are marked `#[non_exhaustive]` for semver headroom. Build public POD structs with `Default::default().with_(...)` or request parameter constructors, pass request parameters directly to -`exec_api_v2` where `Into` is implemented, and include a wildcard arm -when matching response enums. +`client.v2.exec` where `Into` is implemented, and include a wildcard +arm when matching response enums. Live API tests hit DeDust directly. Pool counts, routing amounts, and ordering can drift with upstream state. diff --git a/dedust_api_client/src/api_client.rs b/dedust_api_client/src/api_client.rs index d3bc359..95d9d8e 100644 --- a/dedust_api_client/src/api_client.rs +++ b/dedust_api_client/src/api_client.rs @@ -1,38 +1,16 @@ mod builder; use crate::api_client::builder::Builder; -use crate::api_v2::V2Request; -use crate::api_v2::V2Response; -use api_clients_core::{ApiClientsResult, Executor}; -use std::sync::Arc; +use crate::v2::V2ApiClient; pub const DEFAULT_API_V2_URL: &str = "https://api.dedust.io/v2"; #[derive(Clone)] +#[non_exhaustive] pub struct DedustApiClient { - executor: Arc, + pub v2: V2ApiClient, } impl DedustApiClient { pub fn builder() -> Builder { Builder::new() } - - pub async fn exec_api_v2(&self, request: REQUEST) -> ApiClientsResult - where - REQUEST: Into, - { - let request = request.into(); - let response = match &request { - V2Request::Assets => V2Response::Assets(self.executor.exec_get("assets").await?), - V2Request::Pools => V2Response::Pools(self.executor.exec_get("pools").await?), - V2Request::PoolsLite => V2Response::PoolsLite(self.executor.exec_get("pools-lite").await?), - V2Request::PoolTrades(pool_addr) => { - let path = format!("pools/{}/trades", pool_addr); - V2Response::PoolTrades(self.executor.exec_get(&path).await?) - } - V2Request::RoutingPlan(params) => { - V2Response::RoutingPlan(self.executor.exec_post_body("routing/plan", params, &[]).await?) - } - }; - Ok(response) - } } diff --git a/dedust_api_client/src/api_client/builder.rs b/dedust_api_client/src/api_client/builder.rs index db6b0d3..ef0aee3 100644 --- a/dedust_api_client/src/api_client/builder.rs +++ b/dedust_api_client/src/api_client/builder.rs @@ -1,4 +1,5 @@ use crate::api_client::{DedustApiClient, DEFAULT_API_V2_URL}; +use crate::v2::V2ApiClient; use api_clients_core::{ApiClientsResult, Executor}; use derive_setters::Setters; use std::sync::Arc; @@ -25,6 +26,7 @@ impl Builder { None => Executor::builder(self.api_url).build()?.into(), }; - Ok(DedustApiClient { executor }) + let v2 = V2ApiClient::new(executor); + Ok(DedustApiClient { v2 }) } } diff --git a/dedust_api_client/src/api_v2.rs b/dedust_api_client/src/api_v2.rs deleted file mode 100644 index 41bacfc..0000000 --- a/dedust_api_client/src/api_v2.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod request; -mod response; -mod types; - -pub use request::*; -pub use response::*; -pub use types::*; diff --git a/dedust_api_client/src/lib.rs b/dedust_api_client/src/lib.rs index aa7b4d9..a9c284c 100644 --- a/dedust_api_client/src/lib.rs +++ b/dedust_api_client/src/lib.rs @@ -2,4 +2,4 @@ pub use api_clients_core; // re-export pub mod api_client; -pub mod api_v2; +pub mod v2; diff --git a/dedust_api_client/src/v2.rs b/dedust_api_client/src/v2.rs new file mode 100644 index 0000000..518aea6 --- /dev/null +++ b/dedust_api_client/src/v2.rs @@ -0,0 +1,39 @@ +mod request; +mod response; +mod types; + +use api_clients_core::{ApiClientsResult, Executor}; +use std::sync::Arc; + +pub use request::*; +pub use response::*; +pub use types::*; + +#[derive(Clone)] +pub struct V2ApiClient { + executor: Arc, +} + +impl V2ApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + pub async fn exec(&self, request: REQUEST) -> ApiClientsResult + where + REQUEST: Into, + { + let request = request.into(); + let response = match &request { + V2Request::Assets => V2Response::Assets(self.executor.exec_get("assets").await?), + V2Request::Pools => V2Response::Pools(self.executor.exec_get("pools").await?), + V2Request::PoolsLite => V2Response::PoolsLite(self.executor.exec_get("pools-lite").await?), + V2Request::PoolTrades(pool_addr) => { + let path = format!("pools/{}/trades", pool_addr); + V2Response::PoolTrades(self.executor.exec_get(&path).await?) + } + V2Request::RoutingPlan(params) => { + V2Response::RoutingPlan(self.executor.exec_post_body("routing/plan", params, &[]).await?) + } + }; + Ok(response) + } +} diff --git a/dedust_api_client/src/api_v2/request.rs b/dedust_api_client/src/v2/request.rs similarity index 100% rename from dedust_api_client/src/api_v2/request.rs rename to dedust_api_client/src/v2/request.rs diff --git a/dedust_api_client/src/api_v2/response.rs b/dedust_api_client/src/v2/response.rs similarity index 86% rename from dedust_api_client/src/api_v2/response.rs rename to dedust_api_client/src/v2/response.rs index 5015857..0899e45 100644 --- a/dedust_api_client/src/api_v2/response.rs +++ b/dedust_api_client/src/v2/response.rs @@ -1,4 +1,4 @@ -use crate::api_v2::types::*; +use crate::v2::types::*; use derive_more::From; use serde_derive::Deserialize; @@ -6,7 +6,7 @@ use serde_derive::Deserialize; macro_rules! unwrap_response { ($variant:ident, $result:expr) => { match $result { - $crate::api_v2::V2Response::$variant(inner) => Ok(inner), + $crate::v2::V2Response::$variant(inner) => Ok(inner), other => Err($crate::api_clients_core::ApiClientsError::UnexpectedResponse(format!( "ApiClientError: expected {}, but got {:?}", stringify!($variant), diff --git a/dedust_api_client/src/api_v2/types.rs b/dedust_api_client/src/v2/types.rs similarity index 100% rename from dedust_api_client/src/api_v2/types.rs rename to dedust_api_client/src/v2/types.rs diff --git a/dedust_api_client/tests/test_api_v2.rs b/dedust_api_client/tests/test_api_v2.rs index 4a6fb52..c9139c6 100644 --- a/dedust_api_client/tests/test_api_v2.rs +++ b/dedust_api_client/tests/test_api_v2.rs @@ -2,8 +2,8 @@ use anyhow::Result; use api_clients_core::Executor; use dedust_api_client::api_client::DedustApiClient; use dedust_api_client::api_client::DEFAULT_API_V2_URL; -use dedust_api_client::api_v2::{RoutingPlanParams, V2Request}; use dedust_api_client::unwrap_response; +use dedust_api_client::v2::{RoutingPlanParams, V2Request}; use std::sync::Arc; use std::time::Duration; @@ -17,7 +17,7 @@ fn init_env() -> Result { async fn test_assets() -> Result<()> { let client = init_env()?; let request = V2Request::Assets; - let response = unwrap_response!(Assets, client.exec_api_v2(&request).await?)?; + let response = unwrap_response!(Assets, client.v2.exec(&request).await?)?; assert!(!response.is_empty()); Ok(()) } @@ -26,7 +26,7 @@ async fn test_assets() -> Result<()> { async fn test_pools() -> Result<()> { let client = init_env()?; let request = V2Request::Pools; - let response = unwrap_response!(Pools, client.exec_api_v2(&request).await?)?; + let response = unwrap_response!(Pools, client.v2.exec(&request).await?)?; assert!(!response.is_empty()); Ok(()) } @@ -35,7 +35,7 @@ async fn test_pools() -> Result<()> { async fn test_pools_lite() -> Result<()> { let client = init_env()?; let request = V2Request::PoolsLite; - let response = unwrap_response!(PoolsLite, client.exec_api_v2(&request).await?)?; + let response = unwrap_response!(PoolsLite, client.v2.exec(&request).await?)?; assert!(!response.is_empty()); Ok(()) } @@ -44,7 +44,7 @@ async fn test_pools_lite() -> Result<()> { async fn test_pool_trades() -> Result<()> { let client = init_env()?; let request = V2Request::PoolTrades("EQAADLqcF3lNb1O_GQLowwky8vvUGXuzPRNGvKBwBxjsHR7s".to_string()); - let response = unwrap_response!(PoolTrades, client.exec_api_v2(&request).await?)?; + let response = unwrap_response!(PoolTrades, client.v2.exec(&request).await?)?; assert!(!response.is_empty()); Ok(()) } @@ -55,7 +55,7 @@ async fn test_routing_plan() -> Result<()> { let usdt_addr = "0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe"; let ton_addr = "0:0000000000000000000000000000000000000000000000000000000000000000"; let params = RoutingPlanParams::new(usdt_addr, ton_addr, &10.to_string()); - let response = unwrap_response!(RoutingPlan, client.exec_api_v2(params).await?)?; + let response = unwrap_response!(RoutingPlan, client.v2.exec(params).await?)?; assert!(!response.is_empty()); assert!(!response[0].is_empty()); Ok(()) diff --git a/dedust_api_client/tests/test_construction.rs b/dedust_api_client/tests/test_construction.rs index 6f0dd5c..378b06f 100644 --- a/dedust_api_client/tests/test_construction.rs +++ b/dedust_api_client/tests/test_construction.rs @@ -1,4 +1,13 @@ -use dedust_api_client::api_v2::{Asset, Pool, PoolAsset, RoutingPlanParams}; +use dedust_api_client::api_client::DedustApiClient; +use dedust_api_client::v2::{Asset, Pool, PoolAsset, RoutingPlanParams, V2Request}; + +#[test] +fn test_client_exposes_v2_executor() -> anyhow::Result<()> { + let client = DedustApiClient::builder().build()?; + let future = client.v2.exec(V2Request::Assets); + drop(future); + Ok(()) +} #[test] fn test_request_params_support_default_setter_construction() { diff --git a/swap_coffee_api_client/AGENTS.md b/swap_coffee_api_client/AGENTS.md index b9616dd..40489bf 100644 --- a/swap_coffee_api_client/AGENTS.md +++ b/swap_coffee_api_client/AGENTS.md @@ -14,9 +14,9 @@ changes. The crate exposes a thin typed client: - `SwapCoffeeApiClient::builder().build()?` -- `client.exec_api_v1(&V1Request::...)` -- request params in `api_v1/request.rs` -- response enums and models in `api_v1/response.rs` and `api_v1/types.rs` +- `client.v1.exec(&V1Request::...)` +- request params in `v1/request.rs` +- response enums and models in `v1/response.rs` and `v1/types.rs` Do not add routing decisions or swap execution flows here. Keep this crate to API request/response wrapping. @@ -27,6 +27,7 @@ Treat these as public contracts: - `SwapCoffeeApiClient` - `DEFAULT_API_V1_URL` +- `V1ApiClient` - `V1Request` - `Dexes` - `V1Response` and public response/type structs @@ -35,7 +36,7 @@ Treat these as public contracts: Request parameter and response/model POD structs are `#[non_exhaustive]`; use `Default::default().with_(...)` or request parameter `new()` methods instead of struct literals in downstream examples and integration tests. Pass -request parameters directly to `exec_api_v1` where `Into` is +request parameters directly to `client.v1.exec` where `Into` is implemented. Public enums are `#[non_exhaustive]`; downstream matches need wildcard arms. @@ -50,12 +51,12 @@ assertions on returned pool counts, token ordering, or dynamic metadata. ## Downstream Integration Example ```rust -use swap_coffee_api_client::api_v1::{Dexes, V1Request, V1Response}; use swap_coffee_api_client::api_client::SwapCoffeeApiClient; +use swap_coffee_api_client::v1::{Dexes, V1Request, V1Response}; # async fn example() -> anyhow::Result<()> { let client = SwapCoffeeApiClient::builder().build()?; -let response = client.exec_api_v1(Dexes::new("stonfi_v2")).await?; +let response = client.v1.exec(Dexes::new("stonfi_v2")).await?; match response { V1Response::Pools(pools) => println!("pool pages: {}", pools.len()), diff --git a/swap_coffee_api_client/README.md b/swap_coffee_api_client/README.md index 1ac75cd..eda44ba 100644 --- a/swap_coffee_api_client/README.md +++ b/swap_coffee_api_client/README.md @@ -20,11 +20,11 @@ wildcard arm. ```rust,no_run use swap_coffee_api_client::api_client::SwapCoffeeApiClient; -use swap_coffee_api_client::api_v1::{Dexes, V1Response}; +use swap_coffee_api_client::v1::{Dexes, V1Response}; # async fn example() -> Result<(), Box> { let client = SwapCoffeeApiClient::builder().build()?; -let response = client.exec_api_v1(Dexes::new("coffee")).await?; +let response = client.v1.exec(Dexes::new("coffee")).await?; match response { V1Response::Pools(pages) => println!("pool pages: {}", pages.len()), @@ -44,8 +44,8 @@ match response { Public request and response types are marked `#[non_exhaustive]` for semver headroom. Build public POD structs with `Default::default().with_(...)` or request parameter constructors, pass request parameters directly to -`exec_api_v1` where `Into` is implemented, and include a wildcard arm -when matching response enums. +`client.v1.exec` where `Into` is implemented, and include a wildcard +arm when matching response enums. Live API tests hit Swap Coffee directly. Pool counts, token ordering, metadata, and liquidity fields can drift with upstream state. diff --git a/swap_coffee_api_client/src/api_client.rs b/swap_coffee_api_client/src/api_client.rs index 0b4553b..e493482 100644 --- a/swap_coffee_api_client/src/api_client.rs +++ b/swap_coffee_api_client/src/api_client.rs @@ -1,30 +1,16 @@ mod builder; use crate::api_client::builder::Builder; -use crate::api_v1::V1Request; -use crate::api_v1::V1Response; -use api_clients_core::{ApiClientsResult, Executor}; -use std::sync::Arc; +use crate::v1::V1ApiClient; pub const DEFAULT_API_V1_URL: &str = "https://backend.swap.coffee/v1"; #[derive(Clone)] +#[non_exhaustive] pub struct SwapCoffeeApiClient { - executor: Arc, + pub v1: V1ApiClient, } impl SwapCoffeeApiClient { pub fn builder() -> Builder { Builder::new() } - - pub async fn exec_api_v1(&self, request: REQUEST) -> ApiClientsResult - where - REQUEST: Into, - { - let request = request.into(); - let response = match &request { - V1Request::Assets => V1Response::Assets(self.executor.exec_get("tokens").await?), - V1Request::Pools(dexes) => V1Response::Pools(self.executor.exec_get_extra("pools", dexes, &[]).await?), - }; - Ok(response) - } } diff --git a/swap_coffee_api_client/src/api_client/builder.rs b/swap_coffee_api_client/src/api_client/builder.rs index a1bb303..c650a88 100644 --- a/swap_coffee_api_client/src/api_client/builder.rs +++ b/swap_coffee_api_client/src/api_client/builder.rs @@ -1,4 +1,5 @@ use crate::api_client::{SwapCoffeeApiClient, DEFAULT_API_V1_URL}; +use crate::v1::V1ApiClient; use api_clients_core::{ApiClientsResult, Executor}; use derive_setters::Setters; use std::sync::Arc; @@ -25,6 +26,7 @@ impl Builder { None => Executor::builder(self.api_url).build()?.into(), }; - Ok(SwapCoffeeApiClient { executor }) + let v1 = V1ApiClient::new(executor); + Ok(SwapCoffeeApiClient { v1 }) } } diff --git a/swap_coffee_api_client/src/api_v1.rs b/swap_coffee_api_client/src/api_v1.rs deleted file mode 100644 index 41bacfc..0000000 --- a/swap_coffee_api_client/src/api_v1.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod request; -mod response; -mod types; - -pub use request::*; -pub use response::*; -pub use types::*; diff --git a/swap_coffee_api_client/src/lib.rs b/swap_coffee_api_client/src/lib.rs index ad83b24..65027a5 100644 --- a/swap_coffee_api_client/src/lib.rs +++ b/swap_coffee_api_client/src/lib.rs @@ -2,4 +2,4 @@ pub use api_clients_core; pub mod api_client; -pub mod api_v1; +pub mod v1; diff --git a/swap_coffee_api_client/src/v1.rs b/swap_coffee_api_client/src/v1.rs new file mode 100644 index 0000000..600a7cc --- /dev/null +++ b/swap_coffee_api_client/src/v1.rs @@ -0,0 +1,31 @@ +mod request; +mod response; +mod types; + +use api_clients_core::{ApiClientsResult, Executor}; +use std::sync::Arc; + +pub use request::*; +pub use response::*; +pub use types::*; + +#[derive(Clone)] +pub struct V1ApiClient { + executor: Arc, +} + +impl V1ApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + pub async fn exec(&self, request: REQUEST) -> ApiClientsResult + where + REQUEST: Into, + { + let request = request.into(); + let response = match &request { + V1Request::Assets => V1Response::Assets(self.executor.exec_get("tokens").await?), + V1Request::Pools(dexes) => V1Response::Pools(self.executor.exec_get_extra("pools", dexes, &[]).await?), + }; + Ok(response) + } +} diff --git a/swap_coffee_api_client/src/api_v1/request.rs b/swap_coffee_api_client/src/v1/request.rs similarity index 100% rename from swap_coffee_api_client/src/api_v1/request.rs rename to swap_coffee_api_client/src/v1/request.rs diff --git a/swap_coffee_api_client/src/api_v1/response.rs b/swap_coffee_api_client/src/v1/response.rs similarity index 88% rename from swap_coffee_api_client/src/api_v1/response.rs rename to swap_coffee_api_client/src/v1/response.rs index 5a0d686..752d378 100644 --- a/swap_coffee_api_client/src/api_v1/response.rs +++ b/swap_coffee_api_client/src/v1/response.rs @@ -1,4 +1,4 @@ -use crate::api_v1::types::*; +use crate::v1::types::*; use derive_more::From; use derive_setters::Setters; use serde_derive::Deserialize; @@ -7,7 +7,7 @@ use serde_derive::Deserialize; macro_rules! unwrap_response { ($variant:ident, $result:expr) => { match $result { - $crate::api_v1::V1Response::$variant(inner) => Ok(inner), + $crate::v1::V1Response::$variant(inner) => Ok(inner), other => Err($crate::api_clients_core::ApiClientsError::UnexpectedResponse(format!( "ApiClientError: expected {}, but got {:?}", stringify!($variant), diff --git a/swap_coffee_api_client/src/api_v1/types.rs b/swap_coffee_api_client/src/v1/types.rs similarity index 100% rename from swap_coffee_api_client/src/api_v1/types.rs rename to swap_coffee_api_client/src/v1/types.rs diff --git a/swap_coffee_api_client/tests/test_api_v1.rs b/swap_coffee_api_client/tests/test_api_v1.rs index 037724c..3031b55 100644 --- a/swap_coffee_api_client/tests/test_api_v1.rs +++ b/swap_coffee_api_client/tests/test_api_v1.rs @@ -2,9 +2,9 @@ use std::vec; use anyhow::Result; use swap_coffee_api_client::api_client::SwapCoffeeApiClient; -use swap_coffee_api_client::api_v1::Dexes; -use swap_coffee_api_client::api_v1::V1Request; use swap_coffee_api_client::unwrap_response; +use swap_coffee_api_client::v1::Dexes; +use swap_coffee_api_client::v1::V1Request; fn init_env() -> SwapCoffeeApiClient { let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).try_init(); @@ -15,7 +15,7 @@ fn init_env() -> SwapCoffeeApiClient { async fn test_assets() -> Result<()> { let client = init_env(); let request = V1Request::Assets; - let response = unwrap_response!(Assets, client.exec_api_v1(&request).await?)?; + let response = unwrap_response!(Assets, client.v1.exec(&request).await?)?; assert_ne!(response, vec![]); Ok(()) } @@ -23,7 +23,7 @@ async fn test_assets() -> Result<()> { #[tokio::test] async fn test_pools() -> Result<()> { let client = init_env(); - let response = unwrap_response!(Pools, client.exec_api_v1(Dexes::new("coffee")).await?)?; + let response = unwrap_response!(Pools, client.v1.exec(Dexes::new("coffee")).await?)?; log::debug!("response: {:?}", response[0].pools[0]); assert_ne!(response[0].pools, vec![]); Ok(()) diff --git a/swap_coffee_api_client/tests/test_construction.rs b/swap_coffee_api_client/tests/test_construction.rs index 5fcbe01..d77dca4 100644 --- a/swap_coffee_api_client/tests/test_construction.rs +++ b/swap_coffee_api_client/tests/test_construction.rs @@ -1,4 +1,13 @@ -use swap_coffee_api_client::api_v1::{Address, Dexes, Pool, PoolsResponse, Token}; +use swap_coffee_api_client::api_client::SwapCoffeeApiClient; +use swap_coffee_api_client::v1::{Address, Dexes, Pool, PoolsResponse, Token, V1Request}; + +#[test] +fn test_client_exposes_v1_executor() -> anyhow::Result<()> { + let client = SwapCoffeeApiClient::builder().build()?; + let future = client.v1.exec(V1Request::Assets); + drop(future); + Ok(()) +} #[test] fn test_request_params_support_default_setter_construction() { diff --git a/tonco_api_client/AGENTS.md b/tonco_api_client/AGENTS.md index 14b71a1..05abb20 100644 --- a/tonco_api_client/AGENTS.md +++ b/tonco_api_client/AGENTS.md @@ -14,7 +14,7 @@ changes. The crate exposes a low-level GraphQL execution boundary: - `ToncoApiClient::builder().build()?` -- `client.exec_graphql(operation_name, &query)` +- `client.graphql.exec(operation_name, &query)` - checked-in GraphQL schema at `src/graphql_schema.json` - usage examples in `tests/test_graphql.rs` and `tests/pools.graphql` @@ -27,13 +27,13 @@ Treat these as public contracts: - `ToncoApiClient` - `DEFAULT_GRAPHQL_ENDPOINT` -- `ToncoApiClient::exec_graphql` +- `GraphqlApiClient` Public client types are `#[non_exhaustive]` where applicable; avoid public struct literal construction in examples. The default endpoint intentionally has no trailing slash: -`https://indexer.tonco.io`. `exec_graphql` posts through +`https://indexer.tonco.io`. `client.graphql.exec` posts through `Executor::exec_post_body("", ...)`, so a trailing slash would produce `POST //` against the live service. @@ -63,7 +63,7 @@ pub struct Pools; # async fn example() -> anyhow::Result<()> { let client = ToncoApiClient::builder().build()?; let query = Pools::build_query(pools::Variables); -let data: pools::ResponseData = client.exec_graphql(pools::OPERATION_NAME, &query).await?; +let data: pools::ResponseData = client.graphql.exec(pools::OPERATION_NAME, &query).await?; if let Some(pools) = data.pools { println!("pools: {}", pools.len()); } diff --git a/tonco_api_client/README.md b/tonco_api_client/README.md index 32a869e..222eace 100644 --- a/tonco_api_client/README.md +++ b/tonco_api_client/README.md @@ -5,7 +5,8 @@ Low-level GraphQL wrapper for the [Tonco Indexer API](https://indexer.tonco.io/d Use this crate when an application needs to execute Tonco GraphQL queries while keeping query files, generated query modules, schema refreshes, pagination, and domain mapping in the application layer. The crate intentionally exposes -`exec_graphql(operation_name, query)` instead of higher-level pool or swap APIs. +`client.graphql.exec(operation_name, query)` instead of higher-level pool or +swap APIs. ## Usage @@ -16,7 +17,7 @@ serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` -Run requests inside an async Tokio runtime. `exec_graphql` sends the +Run requests inside an async Tokio runtime. `client.graphql.exec` sends the `x-apollo-operation-name` header, unwraps GraphQL `data`, and maps GraphQL `errors` or missing `data` to `ApiClientsError::UnexpectedResponse`. @@ -53,7 +54,7 @@ let request = GraphqlRequest { variables: EmptyVariables, }; -let data: PoolsData = client.exec_graphql("Pools", &request).await?; +let data: PoolsData = client.graphql.exec("Pools", &request).await?; let pool_count = data.pools.unwrap_or_default().len(); println!("pools: {pool_count}"); # Ok(()) diff --git a/tonco_api_client/src/api_client.rs b/tonco_api_client/src/api_client.rs index 3b2717c..b06f601 100644 --- a/tonco_api_client/src/api_client.rs +++ b/tonco_api_client/src/api_client.rs @@ -1,43 +1,16 @@ mod builder; use crate::api_client::builder::Builder; -use api_clients_core::{ApiClientsError, ApiClientsResult, Executor}; -use graphql_client::Response; -use serde::{de, ser}; -use std::sync::Arc; +use crate::graphql::GraphqlApiClient; pub static DEFAULT_GRAPHQL_ENDPOINT: &str = "https://indexer.tonco.io"; #[derive(Clone)] +#[non_exhaustive] pub struct ToncoApiClient { - executor: Arc, + pub graphql: GraphqlApiClient, } impl ToncoApiClient { pub fn builder() -> Builder { Builder::new() } - - pub async fn exec_graphql(&self, op_name: &str, graphql_query: &PARAMS) -> ApiClientsResult - where - PARAMS: ser::Serialize, - RSP: de::DeserializeOwned, - { - let headers = &[ - (reqwest::header::CONTENT_TYPE.to_string(), "application/json".to_string()), - ("x-apollo-operation-name".to_string(), op_name.to_string()), - ]; - let response = self.executor.exec_post_body("", graphql_query, headers).await?; - handle_graphql_result(response) - } -} - -fn handle_graphql_result(graphql_result: Response) -> ApiClientsResult { - if let Some(errors) = graphql_result.errors { - let msgs: Vec = errors.into_iter().map(|e| e.message).collect(); - let err_msg = msgs.join(", "); - Err(ApiClientsError::UnexpectedResponse(err_msg)) - } else if let Some(data) = graphql_result.data { - Ok(data) - } else { - Err(ApiClientsError::UnexpectedResponse("No data in GraphQL response".to_string())) - } } diff --git a/tonco_api_client/src/api_client/builder.rs b/tonco_api_client/src/api_client/builder.rs index e440d85..0910c26 100644 --- a/tonco_api_client/src/api_client/builder.rs +++ b/tonco_api_client/src/api_client/builder.rs @@ -1,4 +1,5 @@ use crate::api_client::{ToncoApiClient, DEFAULT_GRAPHQL_ENDPOINT}; +use crate::graphql::GraphqlApiClient; use api_clients_core::{ApiClientsResult, Executor}; use derive_setters::Setters; use std::sync::Arc; @@ -24,6 +25,7 @@ impl Builder { Some(executor) => executor, None => Executor::builder(self.graphql_endpoint).build()?.into(), }; - Ok(ToncoApiClient { executor }) + let graphql = GraphqlApiClient::new(executor); + Ok(ToncoApiClient { graphql }) } } diff --git a/tonco_api_client/src/graphql.rs b/tonco_api_client/src/graphql.rs new file mode 100644 index 0000000..0a8a349 --- /dev/null +++ b/tonco_api_client/src/graphql.rs @@ -0,0 +1,38 @@ +use api_clients_core::{ApiClientsError, ApiClientsResult, Executor}; +use graphql_client::Response; +use serde::{de, ser}; +use std::sync::Arc; + +#[derive(Clone)] +pub struct GraphqlApiClient { + executor: Arc, +} + +impl GraphqlApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + pub async fn exec(&self, op_name: &str, graphql_query: &PARAMS) -> ApiClientsResult + where + PARAMS: ser::Serialize, + RSP: de::DeserializeOwned, + { + let headers = &[ + (reqwest::header::CONTENT_TYPE.to_string(), "application/json".to_string()), + ("x-apollo-operation-name".to_string(), op_name.to_string()), + ]; + let response = self.executor.exec_post_body("", graphql_query, headers).await?; + handle_graphql_result(response) + } +} + +fn handle_graphql_result(graphql_result: Response) -> ApiClientsResult { + if let Some(errors) = graphql_result.errors { + let msgs: Vec = errors.into_iter().map(|e| e.message).collect(); + let err_msg = msgs.join(", "); + Err(ApiClientsError::UnexpectedResponse(err_msg)) + } else if let Some(data) = graphql_result.data { + Ok(data) + } else { + Err(ApiClientsError::UnexpectedResponse("No data in GraphQL response".to_string())) + } +} diff --git a/tonco_api_client/src/lib.rs b/tonco_api_client/src/lib.rs index d2f5ee9..a17ea08 100644 --- a/tonco_api_client/src/lib.rs +++ b/tonco_api_client/src/lib.rs @@ -1,3 +1,4 @@ #![doc = include_str!("../README.md")] pub mod api_client; +pub mod graphql; diff --git a/tonco_api_client/tests/test_construction.rs b/tonco_api_client/tests/test_construction.rs new file mode 100644 index 0000000..86f99e3 --- /dev/null +++ b/tonco_api_client/tests/test_construction.rs @@ -0,0 +1,14 @@ +use tonco_api_client::api_client::ToncoApiClient; + +#[test] +fn test_client_exposes_graphql_executor() -> anyhow::Result<()> { + let client = ToncoApiClient::builder().build()?; + let query = graphql_client::QueryBody { + variables: (), + query: "query Pools { pools { id } }", + operation_name: "Pools", + }; + let future = client.graphql.exec::<_, ()>("Pools", &query); + drop(future); + Ok(()) +} diff --git a/tonco_api_client/tests/test_graphql.rs b/tonco_api_client/tests/test_graphql.rs index ec81d6b..e2f965f 100644 --- a/tonco_api_client/tests/test_graphql.rs +++ b/tonco_api_client/tests/test_graphql.rs @@ -22,7 +22,7 @@ pub struct Pools; async fn test_tonco_pools() -> anyhow::Result<()> { let client = init_env(); let query = Pools::build_query(pools::Variables); - let response: pools::ResponseData = client.exec_graphql(pools::OPERATION_NAME, &query).await?; + let response: pools::ResponseData = client.graphql.exec(pools::OPERATION_NAME, &query).await?; assert!(response.pools.is_some()); let pools = response.pools.unwrap(); assert!(!pools.is_empty());