1#[cfg(feature = "cli")]
34pub mod cli;
35mod http;
36mod retry;
37mod sse;
38
39pub use retry::RetryPolicy;
40pub use talea_core::api::*;
41
42use std::time::Duration;
43
44use async_trait::async_trait;
45use chrono::{DateTime, Utc};
46use talea_core::types::Seq;
47
48pub struct TaleaClient {
53 http: http::Http,
54}
55
56pub struct ClientBuilder {
59 base_url: String,
60 token: Option<String>,
61 timeout: Duration,
62 retry: RetryPolicy,
63 http_client: Option<reqwest::Client>,
64}
65
66impl TaleaClient {
67 pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
71 ClientBuilder {
72 base_url: base_url.into(),
73 token: None,
74 timeout: Duration::from_secs(30),
75 retry: RetryPolicy::default(),
76 http_client: None,
77 }
78 }
79}
80
81impl ClientBuilder {
82 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
86 self.token = Some(token.into());
87 self
88 }
89
90 pub fn timeout(mut self, timeout: Duration) -> Self {
92 self.timeout = timeout;
93 self
94 }
95
96 pub fn retry(mut self, retry: RetryPolicy) -> Self {
100 self.retry = retry;
101 self
102 }
103
104 pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
107 self.http_client = Some(client);
108 self
109 }
110
111 pub fn build(self) -> ApiResult<TaleaClient> {
115 let base = reqwest::Url::parse(&self.base_url).map_err(|e| ApiError::Transport {
116 message: format!("invalid base url: {e}"),
117 })?;
118 let client = match self.http_client {
119 Some(c) => c,
120 None => reqwest::Client::builder()
121 .connect_timeout(Duration::from_secs(10))
122 .build()
123 .map_err(|e| ApiError::Transport {
124 message: format!("building http client: {e}"),
125 })?,
126 };
127 Ok(TaleaClient {
128 http: http::Http {
129 client,
130 base,
131 token: self.token,
132 timeout: self.timeout,
133 retry: self.retry,
134 },
135 })
136 }
137}
138
139#[async_trait]
140impl LedgerApi for TaleaClient {
141 async fn register_asset(&self, draft: AssetDraft) -> ApiResult<()> {
142 let url = self.http.url(&["assets"])?;
143 self.http
144 .execute_unit(|| self.http.client.post(url.clone()).json(&draft))
145 .await
146 }
147
148 async fn open_account(&self, draft: AccountDraft) -> ApiResult<()> {
149 let url = self.http.url(&["accounts"])?;
150 self.http
151 .execute_unit(|| self.http.client.post(url.clone()).json(&draft))
152 .await
153 }
154
155 async fn post(&self, draft: TransactionDraft) -> ApiResult<Posted> {
156 let url = self.http.url(&["transactions"])?;
157 self.http
158 .execute(|| self.http.client.post(url.clone()).json(&draft))
159 .await
160 }
161
162 async fn balance(
163 &self,
164 book: &str,
165 path: &str,
166 as_of: Option<DateTime<Utc>>,
167 ) -> ApiResult<BalanceView> {
168 let mut url = self
169 .http
170 .url(&["books", book, "accounts", path, "balance"])?;
171 if let Some(t) = as_of {
172 url.query_pairs_mut().append_pair("as_of", &t.to_rfc3339());
173 }
174 self.http
175 .execute(|| self.http.client.get(url.clone()))
176 .await
177 }
178
179 async fn account_history(
180 &self,
181 book: &str,
182 path: &str,
183 page: Page,
184 ) -> ApiResult<Paged<PostingView>> {
185 let mut url = self
186 .http
187 .url(&["books", book, "accounts", path, "history"])?;
188 {
189 let mut q = url.query_pairs_mut();
190 if let Some(after) = page.after_seq {
191 q.append_pair("after_seq", &after.to_string());
192 }
193 q.append_pair("limit", &page.limit.to_string());
194 }
195 self.http
196 .execute(|| self.http.client.get(url.clone()))
197 .await
198 }
199
200 async fn transaction(&self, tx_id: &str) -> ApiResult<TransactionView> {
201 let url = self.http.url(&["transactions", tx_id])?;
202 self.http
203 .execute(|| self.http.client.get(url.clone()))
204 .await
205 }
206
207 async fn trial_balance(
208 &self,
209 book: &str,
210 as_of: Option<DateTime<Utc>>,
211 ) -> ApiResult<TrialBalance> {
212 let mut url = self.http.url(&["books", book, "trial-balance"])?;
213 if let Some(t) = as_of {
214 url.query_pairs_mut().append_pair("as_of", &t.to_rfc3339());
215 }
216 self.http
217 .execute(|| self.http.client.get(url.clone()))
218 .await
219 }
220
221 async fn post_batch(&self, drafts: Vec<TransactionDraft>) -> Vec<ApiResult<Posted>> {
234 if drafts.is_empty() {
235 return Vec::new();
236 }
237 let n = drafts.len();
238 let url = match self.http.url(&["transactions", "batch"]) {
239 Ok(u) => u,
240 Err(e) => return std::iter::repeat_with(|| Err(e.clone())).take(n).collect(),
241 };
242 self.http
243 .execute_batch(|| self.http.client.post(url.clone()).json(&drafts), n)
244 .await
245 }
246
247 async fn subscribe(&self, book: &str, from: Seq) -> ApiResult<EventStream> {
248 sse::subscribe(&self.http, book, from)
249 }
250}