coda_api/
ext.rs

1use crate::types::{Column, ColumnList, Doc, DocList, GetTableResponse, ListTablesResponse, NextPageToken, Row, RowList, TableList, TableReference};
2use crate::{Error, RawClient, types};
3use progenitor_client::{ClientHooks, ClientInfo, OperationInfo, ResponseValue, encode_path};
4use serde::de::DeserializeOwned;
5use thiserror::Error;
6
7#[cfg(feature = "time")]
8mod duration_value_parser;
9mod impl_from_for_value;
10mod items_list;
11mod parse_cell_value;
12mod parse_rich_value;
13mod rich_rows;
14mod row;
15mod string_or_f64;
16mod value_format_provider;
17
18#[cfg(feature = "time")]
19pub use duration_value_parser::*;
20pub use items_list::*;
21pub use parse_cell_value::*;
22pub use parse_rich_value::*;
23pub use rich_rows::*;
24pub(crate) use string_or_f64::*;
25pub use value_format_provider::*;
26
27pub type DocId = String;
28
29pub type TableId = String;
30
31pub type RowId = String;
32
33// Generic pagination trait for Coda API responses
34pub trait PaginatedResponse<T> {
35    fn items(&self) -> &Vec<T>;
36    fn next_page_token(&self) -> Option<&NextPageToken>;
37    fn into_items(self) -> Vec<T>;
38}
39
40// Generic pagination helper
41pub struct PaginationState {
42    pub next_page_token: Option<String>,
43}
44
45impl PaginationState {
46    pub fn new() -> Self {
47        Self {
48            next_page_token: None,
49        }
50    }
51
52    pub fn update_from_response<T, R: PaginatedResponse<T>>(&mut self, response: &R) {
53        self.next_page_token = response.next_page_token().map(|token| token.clone().into());
54    }
55
56    pub fn has_more_pages(&self) -> bool {
57        self.next_page_token.is_some()
58    }
59
60    pub fn page_token(&self) -> Option<&str> {
61        self.next_page_token.as_deref()
62    }
63}
64
65impl Default for PaginationState {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71// Implement PaginatedResponse for the specific types we need
72impl PaginatedResponse<TableReference> for TableList {
73    fn items(&self) -> &Vec<TableReference> {
74        &self.items
75    }
76
77    fn next_page_token(&self) -> Option<&NextPageToken> {
78        self.next_page_token.as_ref()
79    }
80
81    fn into_items(self) -> Vec<TableReference> {
82        self.items
83    }
84}
85
86impl PaginatedResponse<Column> for ColumnList {
87    fn items(&self) -> &Vec<Column> {
88        &self.items
89    }
90
91    fn next_page_token(&self) -> Option<&NextPageToken> {
92        self.next_page_token.as_ref()
93    }
94
95    fn into_items(self) -> Vec<Column> {
96        self.items
97    }
98}
99
100impl PaginatedResponse<Doc> for DocList {
101    fn items(&self) -> &Vec<Doc> {
102        &self.items
103    }
104
105    fn next_page_token(&self) -> Option<&NextPageToken> {
106        self.next_page_token.as_ref()
107    }
108
109    fn into_items(self) -> Vec<Doc> {
110        self.items
111    }
112}
113
114impl PaginatedResponse<Row> for RowList {
115    fn items(&self) -> &Vec<Row> {
116        &self.items
117    }
118
119    fn next_page_token(&self) -> Option<&NextPageToken> {
120        self.next_page_token.as_ref()
121    }
122
123    fn into_items(self) -> Vec<Row> {
124        self.items
125    }
126}
127
128impl<T> PaginatedResponse<T> for ItemsList<T> {
129    fn items(&self) -> &Vec<T> {
130        &self.items
131    }
132
133    fn next_page_token(&self) -> Option<&NextPageToken> {
134        self.next_page_token.as_ref()
135    }
136
137    fn into_items(self) -> Vec<T> {
138        self.items
139    }
140}
141
142impl RawClient {
143    pub const BASE_URL: &'static str = "https://coda.io/apis/v1";
144
145    pub fn new_with_key(api_key: &str) -> reqwest::Result<Self> {
146        let authorization_header = format!("Bearer {api_key}")
147            .parse()
148            .expect("API key should be valid");
149
150        let mut headers = reqwest::header::HeaderMap::with_capacity(1);
151        headers.insert(reqwest::header::AUTHORIZATION, authorization_header);
152
153        let client_with_custom_defaults = reqwest::ClientBuilder::new()
154            .default_headers(headers)
155            .build()?;
156
157        let client = Self::new_with_client(Self::BASE_URL, client_with_custom_defaults);
158
159        Ok(client)
160    }
161
162    #[allow(clippy::too_many_arguments)]
163    pub async fn list_rows_correct<'a, T: DeserializeOwned + ValueFormatProvider>(&'a self, doc_id: &'a str, table_id_or_name: &'a str, limit: Option<::std::num::NonZeroU64>, page_token: Option<&'a str>, query: Option<&'a str>, sort_by: Option<types::RowsSortBy>, sync_token: Option<&'a str>, use_column_names: Option<bool>, visible_only: Option<bool>) -> Result<ResponseValue<ItemsList<T>>, Error<types::ListRowsResponse>> {
164        let url = format!("{}/docs/{}/tables/{}/rows", self.baseurl, encode_path(doc_id), encode_path(table_id_or_name),);
165        let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize);
166        header_map.append(::reqwest::header::HeaderName::from_static("api-version"), ::reqwest::header::HeaderValue::from_static(Self::api_version()));
167        let value_format = Some(T::value_format());
168        #[allow(unused_mut)]
169        let mut request = self
170            .client
171            .get(url)
172            .header(::reqwest::header::ACCEPT, ::reqwest::header::HeaderValue::from_static("application/json"))
173            .query(&progenitor_client::QueryParam::new("limit", &limit))
174            .query(&progenitor_client::QueryParam::new("pageToken", &page_token))
175            .query(&progenitor_client::QueryParam::new("query", &query))
176            .query(&progenitor_client::QueryParam::new("sortBy", &sort_by))
177            .query(&progenitor_client::QueryParam::new("syncToken", &sync_token))
178            .query(&progenitor_client::QueryParam::new("useColumnNames", &use_column_names))
179            .query(&progenitor_client::QueryParam::new("valueFormat", &value_format))
180            .query(&progenitor_client::QueryParam::new("visibleOnly", &visible_only))
181            .headers(header_map)
182            .build()?;
183        let info = OperationInfo {
184            operation_id: "list_rows_rich",
185        };
186        self.pre(&mut request, &info).await?;
187        let result = self.exec(request, &info).await;
188        self.post(&result, &info).await?;
189        let response = result?;
190        match response.status().as_u16() {
191            200u16 => ResponseValue::from_response(response).await,
192            400u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
193            401u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
194            403u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
195            404u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
196            429u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
197            _ => Err(Error::UnexpectedResponse(response)),
198        }
199    }
200
201    ///Get a row
202    ///
203    ///Returns details about a row in a table.
204    ///
205    ///Sends a `GET` request to
206    /// `/docs/{docId}/tables/{tableIdOrName}/rows/{rowIdOrName}`
207    ///
208    ///Arguments:
209    /// - `doc_id`: ID of the doc.
210    /// - `table_id_or_name`: ID or name of the table. Names are discouraged
211    ///   because they're easily prone to being changed by users. If you're
212    ///   using a name, be sure to URI-encode it.
213    /// - `row_id_or_name`: ID or name of the row. Names are discouraged because
214    ///   they're easily prone to being changed by users. If you're using a
215    ///   name, be sure to URI-encode it. If there are multiple rows with the
216    ///   same value in the identifying column, an arbitrary one will be
217    ///   selected.
218    ///
219    /// - `use_column_names`: Use column names instead of column IDs in the
220    ///   returned output. This is generally discouraged as it is fragile. If
221    ///   columns are renamed, code using original names may throw errors.
222    ///
223    /// - `value_format`: The format that cell values are returned as.
224    pub async fn get_row_correct<'a, T: DeserializeOwned + ValueFormatProvider>(&'a self, doc_id: &'a str, table_id_or_name: &'a str, row_id_or_name: &'a str, use_column_names: Option<bool>) -> Result<ResponseValue<T>, Error<types::GetRowResponse>> {
225        let url = format!("{}/docs/{}/tables/{}/rows/{}", self.baseurl, encode_path(doc_id), encode_path(table_id_or_name), encode_path(row_id_or_name),);
226        let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize);
227        header_map.append(::reqwest::header::HeaderName::from_static("api-version"), ::reqwest::header::HeaderValue::from_static(Self::api_version()));
228        let value_format = Some(T::value_format());
229        #[allow(unused_mut)]
230        let mut request = self
231            .client
232            .get(url)
233            .header(::reqwest::header::ACCEPT, ::reqwest::header::HeaderValue::from_static("application/json"))
234            .query(&progenitor_client::QueryParam::new("useColumnNames", &use_column_names))
235            .query(&progenitor_client::QueryParam::new("valueFormat", &value_format))
236            .headers(header_map)
237            .build()?;
238        let info = OperationInfo {
239            operation_id: "get_row",
240        };
241        self.pre(&mut request, &info).await?;
242        let result = self.exec(request, &info).await;
243        self.post(&result, &info).await?;
244        let response = result?;
245        match response.status().as_u16() {
246            200u16 => ResponseValue::from_response(response).await,
247            401u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
248            403u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
249            404u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
250            429u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
251            _ => Err(Error::UnexpectedResponse(response)),
252        }
253    }
254
255    ///Insert/upsert rows
256    ///
257    ///Inserts rows into a table, optionally updating existing rows if any
258    /// upsert key columns are provided. This endpoint will always return a 202,
259    /// so long as the doc and table exist and are accessible (and the update is
260    /// structurally valid). Row inserts/upserts are generally processed within
261    /// several seconds. Note: this endpoint only works for base tables, not
262    /// views. When upserting, if multiple rows match the specified key
263    /// column(s), they will all be updated with the specified value.
264    ///
265    ///
266    ///Sends a `POST` request to `/docs/{docId}/tables/{tableIdOrName}/rows`
267    ///
268    ///Arguments:
269    /// - `doc_id`: ID of the doc.
270    /// - `table_id_or_name`: ID or name of the table. Names are discouraged
271    ///   because they're easily prone to being changed by users. If you're
272    ///   using a name, be sure to URI-encode it.
273    /// - `disable_parsing`: If true, the API will not attempt to parse the data
274    ///   in any way.
275    /// - `body`: Rows to insert or upsert.
276    pub async fn upsert_rows_correct<'a>(&'a self, doc_id: &'a str, table_id_or_name: &'a str, disable_parsing: Option<bool>, body: &'a types::RowsUpsert) -> Result<ResponseValue<RowsUpsertResultCorrect>, Error<types::UpsertRowsResponse>> {
277        let url = format!("{}/docs/{}/tables/{}/rows", self.baseurl, encode_path(doc_id), encode_path(table_id_or_name),);
278        let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize);
279        header_map.append(::reqwest::header::HeaderName::from_static("api-version"), ::reqwest::header::HeaderValue::from_static(Self::api_version()));
280        #[allow(unused_mut)]
281        let mut request = self
282            .client
283            .post(url)
284            .header(::reqwest::header::ACCEPT, ::reqwest::header::HeaderValue::from_static("application/json"))
285            .json(&body)
286            .query(&progenitor_client::QueryParam::new("disableParsing", &disable_parsing))
287            .headers(header_map)
288            .build()?;
289        let info = OperationInfo {
290            operation_id: "upsert_rows",
291        };
292        self.pre(&mut request, &info).await?;
293        let result = self.exec(request, &info).await;
294        self.post(&result, &info).await?;
295        let response = result?;
296        match response.status().as_u16() {
297            202u16 => ResponseValue::from_response(response).await,
298            400u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
299            401u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
300            403u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
301            404u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
302            429u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
303            _ => Err(Error::UnexpectedResponse(response)),
304        }
305    }
306
307    ///Update row
308    ///
309    ///Updates the specified row in the table. This endpoint will always return
310    /// a 202, so long as the row exists and is accessible (and the update is
311    /// structurally valid). Row updates are generally processed within several
312    /// seconds. When updating using a name as opposed to an ID, an arbitrary
313    /// row will be affected.
314    ///
315    ///
316    ///Sends a `PUT` request to
317    /// `/docs/{docId}/tables/{tableIdOrName}/rows/{rowIdOrName}`
318    ///
319    ///Arguments:
320    /// - `doc_id`: ID of the doc.
321    /// - `table_id_or_name`: ID or name of the table. Names are discouraged
322    ///   because they're easily prone to being changed by users. If you're
323    ///   using a name, be sure to URI-encode it.
324    /// - `row_id_or_name`: ID or name of the row. Names are discouraged because
325    ///   they're easily prone to being changed by users. If you're using a
326    ///   name, be sure to URI-encode it. If there are multiple rows with the
327    ///   same value in the identifying column, an arbitrary one will be
328    ///   selected.
329    ///
330    /// - `disable_parsing`: If true, the API will not attempt to parse the data
331    ///   in any way.
332    /// - `body`: Row update.
333    pub async fn update_row_correct<'a>(&'a self, doc_id: &'a str, table_id_or_name: &'a str, row_id_or_name: &'a str, disable_parsing: Option<bool>, body: &'a types::RowUpdate) -> Result<ResponseValue<RowUpdateResultCorrect>, Error<types::UpdateRowResponse>> {
334        let url = format!("{}/docs/{}/tables/{}/rows/{}", self.baseurl, encode_path(doc_id), encode_path(table_id_or_name), encode_path(row_id_or_name),);
335        let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize);
336        header_map.append(::reqwest::header::HeaderName::from_static("api-version"), ::reqwest::header::HeaderValue::from_static(Self::api_version()));
337        #[allow(unused_mut)]
338        let mut request = self
339            .client
340            .put(url)
341            .header(::reqwest::header::ACCEPT, ::reqwest::header::HeaderValue::from_static("application/json"))
342            .json(&body)
343            .query(&progenitor_client::QueryParam::new("disableParsing", &disable_parsing))
344            .headers(header_map)
345            .build()?;
346        let result = self.client.execute(request).await;
347        let response = result?;
348        match response.status().as_u16() {
349            202u16 => ResponseValue::from_response(response).await,
350            400u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
351            401u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
352            403u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
353            404u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
354            429u16 => Err(Error::ErrorResponse(ResponseValue::from_response(response).await?)),
355            _ => Err(Error::UnexpectedResponse(response)),
356        }
357    }
358}
359
360#[derive(Debug, Error)]
361pub enum ClientTablesError {
362    #[error("list tables request failed: {source}")]
363    ListTablesFailed {
364        #[source]
365        source: Error<ListTablesResponse>,
366    },
367    #[error("get table request failed: {source}")]
368    GetTableFailed {
369        #[source]
370        source: Error<GetTableResponse>,
371    },
372}
373
374///`RowUpdateResult`
375///
376/// <details><summary>JSON schema</summary>
377///
378/// ```json
379///{
380///  "description": "The result of a row update.",
381///  "allOf": [
382///    {
383///      "$ref": "#/components/schemas/DocumentMutateResponse"
384///    },
385///    {
386///      "type": "object",
387///      "required": [
388///        "id"
389///      ],
390///      "properties": {
391///        "id": {
392///          "description": "ID of the updated row.",
393///          "examples": [
394///            "i-tuVwxYz"
395///          ],
396///          "type": "string"
397///        }
398///      },
399///      "additionalProperties": false
400///    }
401///  ],
402///  "x-schema-name": "RowUpdateResult"
403///}
404/// ```
405/// </details>
406#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
407#[serde(deny_unknown_fields)]
408pub struct RowUpdateResultCorrect {
409    #[serde(rename = "id")]
410    pub id: RowId,
411    #[serde(rename = "requestId")]
412    pub request_id: String,
413}
414
415///`RowsUpsertResult`
416///
417/// <details><summary>JSON schema</summary>
418///
419/// ```json
420///{
421///  "description": "The result of a rows insert/upsert operation.",
422///  "allOf": [
423///    {
424///      "$ref": "#/components/schemas/DocumentMutateResponse"
425///    },
426///    {
427///      "type": "object",
428///      "properties": {
429///        "addedRowIds": {
430///          "description": "Row IDs for rows that will be added. Only
431/// applicable when keyColumns is not set or empty.",
432///          "examples": [
433///            [
434///              "i-bCdeFgh",
435///              "i-CdEfgHi"
436///            ]
437///          ],
438///          "type": "array",
439///          "items": {
440///            "type": "string"
441///          }
442///        }
443///      },
444///      "additionalProperties": false
445///    }
446///  ],
447///  "x-schema-name": "RowsUpsertResult"
448///}
449/// ```
450/// </details>
451#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
452#[serde(deny_unknown_fields)]
453pub struct RowsUpsertResultCorrect {
454    #[serde(rename = "addedRowIds", default)]
455    pub added_row_ids: Vec<String>,
456    #[serde(rename = "requestId")]
457    pub request_id: String,
458}
459
460pub fn format_row_url(doc_id: &str, table_id: &str, row_id: &str) -> String {
461    format!("https://coda.io/d/_d{doc_id}#_tu{table_id}/_ru{row_id}")
462}
463
464pub async fn paginate_all<T, R, F, Fut, E>(mut request_fn: F) -> Result<Vec<T>, E>
465where
466    // TODO: Remove the `T: Clone` requirement
467    T: Clone,
468    R: PaginatedResponse<T>,
469    F: FnMut(Option<String>) -> Fut,
470    Fut: Future<Output = Result<R, E>>,
471{
472    let mut all_items = Vec::new();
473    let mut pagination_state = PaginationState::new();
474
475    loop {
476        match request_fn(pagination_state.next_page_token.clone()).await {
477            Ok(response) => {
478                all_items.extend(response.items().iter().cloned());
479
480                if let Some(next_token) = response.next_page_token() {
481                    pagination_state.next_page_token = Some(next_token.clone().into());
482                } else {
483                    break;
484                }
485            }
486            Err(err) => return Err(err),
487        }
488    }
489
490    Ok(all_items)
491}