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