Skip to main content

mesa_dev/client/
mod.rs

1//! Ergonomic client for the Mesa API.
2//!
3//! Provides a directory-style navigation pattern:
4//!
5//! ```rust,no_run
6//! use mesa_dev::MesaClient;
7//! use futures::TryStreamExt;
8//!
9//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
10//! let client = MesaClient::builder()
11//!     .build()?;
12//! let repos: Vec<_> = client.org("my-org").repos().list(None).try_collect().await?;
13//! let branches: Vec<_> = client.org("my-org").repos().at("my-repo").branches().list(None).try_collect().await?;
14//! # Ok(())
15//! # }
16//! ```
17
18mod api_keys;
19mod branches;
20mod change;
21mod commits;
22mod content;
23mod org;
24mod repo;
25mod repos;
26mod webhooks;
27
28mod pagination;
29
30pub use api_keys::ApiKeysClient;
31pub use branches::BranchesClient;
32pub use change::ChangeClient;
33pub use commits::CommitsClient;
34pub use content::ContentClient;
35pub use org::OrgClient;
36pub use repo::RepoClient;
37pub use repos::ReposClient;
38pub use webhooks::WebhooksClient;
39
40use crate::low_level::apis::configuration::Configuration;
41
42/// Default gRPC endpoint for the Mesa VCS data plane.
43pub const DEFAULT_GRPC_ENDPOINT: &str = "https://vcs.depot.mesa.dev";
44
45/// Error returned when building a [`MesaClient`] fails.
46#[derive(Debug)]
47pub enum BuildError {
48    /// The gRPC endpoint URL is invalid.
49    InvalidGrpcEndpoint(tonic::codegen::http::uri::InvalidUri),
50    /// TLS configuration for the gRPC endpoint failed.
51    TlsConfig(tonic::transport::Error),
52}
53
54impl std::fmt::Display for BuildError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::InvalidGrpcEndpoint(e) => write!(f, "invalid gRPC endpoint: {e}"),
58            Self::TlsConfig(e) => write!(f, "gRPC TLS configuration failed: {e}"),
59        }
60    }
61}
62
63impl std::error::Error for BuildError {
64    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
65        match self {
66            Self::InvalidGrpcEndpoint(e) => Some(e),
67            Self::TlsConfig(e) => Some(e),
68        }
69    }
70}
71
72/// Builder for configuring and constructing a [`MesaClient`].
73#[derive(Clone, Debug, Default)]
74pub struct MesaClientBuilder {
75    base_path: Option<String>,
76    user_agent: Option<String>,
77    client: Option<reqwest_middleware::ClientWithMiddleware>,
78    api_key: Option<String>,
79    grpc_endpoint: Option<String>,
80}
81
82impl MesaClientBuilder {
83    /// Attach a non-default base URL for the API (e.g. for testing against a staging environment).
84    #[must_use]
85    pub fn with_base_path(mut self, base_path: impl Into<String>) -> Self {
86        self.base_path = Some(base_path.into());
87        self
88    }
89
90    /// Attach a custom User-Agent header to all requests.
91    #[must_use]
92    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
93        self.user_agent = Some(user_agent.into());
94        self
95    }
96
97    /// Attach a custom HTTP client (e.g. with additional middleware or custom timeout settings).
98    #[must_use]
99    pub fn with_client(mut self, client: reqwest_middleware::ClientWithMiddleware) -> Self {
100        self.client = Some(client);
101        self
102    }
103
104    /// Attach an API key for authentication.
105    #[must_use]
106    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
107        self.api_key = Some(api_key.into());
108        self
109    }
110
111    /// Override the gRPC endpoint URL.
112    ///
113    /// Defaults to [`DEFAULT_GRPC_ENDPOINT`].
114    #[must_use]
115    pub fn with_grpc_endpoint(mut self, endpoint: impl Into<String>) -> Self {
116        self.grpc_endpoint = Some(endpoint.into());
117        self
118    }
119
120    /// Finalize the builder and construct a [`MesaClient`].
121    ///
122    /// # Errors
123    ///
124    /// Returns [`BuildError`] if the gRPC endpoint URL is invalid.
125    pub fn build(self) -> Result<MesaClient, BuildError> {
126        let mut config = Configuration::default();
127
128        if let Some(base_path) = self.base_path {
129            config.base_path = base_path;
130        }
131
132        config.user_agent = self.user_agent.clone().or(Some(Self::default_user_agent()));
133        if let Some(client) = self.client {
134            config.client = client;
135        }
136
137        if let Some(api_key) = self.api_key {
138            config.bearer_access_token = Some(api_key);
139        }
140
141        let endpoint_str = self
142            .grpc_endpoint
143            .unwrap_or_else(|| DEFAULT_GRPC_ENDPOINT.to_owned());
144        let mut endpoint = tonic::transport::Channel::from_shared(endpoint_str)
145            .map_err(BuildError::InvalidGrpcEndpoint)?
146            .http2_adaptive_window(true);
147
148        // Enable TLS when the endpoint uses HTTPS.
149        if endpoint
150            .uri()
151            .scheme_str()
152            .is_some_and(|s| s.eq_ignore_ascii_case("https"))
153        {
154            endpoint = endpoint
155                .tls_config(
156                    tonic::transport::ClientTlsConfig::new()
157                        .with_native_roots()
158                        .with_enabled_roots(),
159                )
160                .map_err(BuildError::TlsConfig)?;
161        }
162
163        let grpc_channel = endpoint.connect_lazy();
164
165        Ok(MesaClient {
166            config,
167            grpc_channel,
168        })
169    }
170
171    fn default_user_agent() -> String {
172        format!(
173            "mesa-dev/{} (rust/{})",
174            env!("CARGO_PKG_VERSION"),
175            env!("MESA_RUSTC_VERSION"),
176        )
177    }
178}
179
180/// Top-level Mesa API client.
181///
182/// Create one with [`MesaClient::builder`] or [`MesaClient::from_configuration`]
183/// and navigate to sub-resources with [`MesaClient::org`].
184#[derive(Clone, Debug)]
185pub struct MesaClient {
186    pub(crate) config: Configuration,
187    pub(crate) grpc_channel: tonic::transport::Channel,
188}
189
190impl MesaClient {
191    /// Create a new builder with default configuration.
192    #[must_use]
193    pub fn builder() -> MesaClientBuilder {
194        MesaClientBuilder::default()
195    }
196
197    /// Create a new client from an existing [`Configuration`] and gRPC channel.
198    #[must_use]
199    pub fn from_configuration(
200        config: Configuration,
201        grpc_channel: tonic::transport::Channel,
202    ) -> Self {
203        Self {
204            config,
205            grpc_channel,
206        }
207    }
208
209    /// Navigate to an organization.
210    #[must_use]
211    pub fn org<'a>(&'a self, name: &'a str) -> OrgClient<'a> {
212        OrgClient {
213            client: self,
214            org: name,
215        }
216    }
217}