Skip to main content

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