Skip to main content

clickhouse_cloud_api/
lib.rs

1//! # clickhouse-cloud-api
2//!
3//! Typed Rust client for the ClickHouse Cloud API.
4//!
5//! ## Usage
6//!
7//! ```rust,no_run
8//! use clickhouse_cloud_api::Client;
9//!
10//! #[tokio::main]
11//! async fn main() -> Result<(), clickhouse_cloud_api::Error> {
12//!     let client = Client::new("your-key-id", "your-key-secret");
13//!     let orgs = client.organization_get_list().await?;
14//!     println!("{:?}", orgs);
15//!     Ok(())
16//! }
17//! ```
18//!
19//! ## Request and response models
20//!
21//! Requests are strict and responses are tolerant, and both are expressed in the
22//! type system rather than through serde attributes.
23//!
24//! A request model mirrors the spec: a required field is `T`, an optional or
25//! nullable field is `Option<T>`. **Every field of every response model is
26//! `Option<T>`**, so a field the API stops sending, or sends as `null`,
27//! deserializes to `None` instead of failing the whole response. Several teams
28//! evolve the Cloud API independently; a strict response field would make each of
29//! those changes a breaking one for you. Nothing is fabricated to fill a gap, so
30//! "the server sent `0`" and "the server dropped the field" stay distinguishable,
31//! and absence is resolved where the value is used.
32//!
33//! A schema the API uses in both directions is therefore two Rust types: the
34//! request variant keeps the schema's name and the response variant is
35//! `{Name}Response`, as in [`models::PostgresInstanceConfig`] and
36//! [`models::PostgresInstanceConfigResponse`]. Response models implement
37//! [`serde::Serialize`] with absent fields **omitted**, never written as `null`,
38//! so serializing one reproduces the key set the API sent.
39//!
40//! Editing a fetched resource and writing it back crosses that boundary
41//! deliberately. [`convert`] holds the conversions, and a fallible one names the
42//! wire fields it needs via [`MissingRequiredFields`]:
43//!
44//! ```rust,no_run
45//! use clickhouse_cloud_api::{Client, PostgresInstanceConfig};
46//!
47//! # async fn write_back() -> Result<(), Box<dyn std::error::Error>> {
48//! let client = Client::new("your-key-id", "your-key-secret");
49//! let fetched = client
50//!     .postgres_instance_config_get("org-id", "postgres-id")
51//!     .await?
52//!     .result
53//!     .ok_or("the API returned no result")?;
54//!
55//! // Absence has to be resolved before the value can become a write body.
56//! let mut body = PostgresInstanceConfig::try_from(fetched)?;
57//! body.pg_config.autovacuum_max_workers = Some(4.into());
58//! client
59//!     .postgres_instance_config_post("org-id", "postgres-id", &body)
60//!     .await?;
61//! # Ok(())
62//! # }
63//! ```
64//!
65//! Enums and unions are tolerant in their own right, in both directions: a string
66//! enum keeps an `Unknown(String)` catch-all and an object union an
67//! `Unknown(serde_json::Value)` fallback holding the payload verbatim, so an
68//! unrecognized or reshaped variant round-trips rather than being rejected.
69//! Unknown object fields are ignored everywhere.
70//!
71//! One failure mode remains, honestly: a field that is *present* with a different
72//! type than the spec declares. `Option<T>` absorbs absence and `null`, not an
73//! object where a string used to be, so such a change still fails a plain struct
74//! field. Enums and unions absorb it through the catch-alls above. Detecting
75//! spec drift of that kind is the job of the repository's daily OpenAPI drift
76//! check, not of runtime deserialization.
77
78pub mod client;
79pub mod convert;
80pub mod error;
81pub mod meta;
82#[allow(non_camel_case_types)]
83pub mod models;
84pub mod serde_helpers;
85
86pub use client::Client;
87pub use convert::MissingRequiredFields;
88pub use error::Error;
89pub use meta::{BETA_OPERATIONS, DEPRECATED_FIELDS, is_beta_operation, is_deprecated_field};
90pub use models::*;