sandkasten_client/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![warn(clippy::dbg_macro, clippy::use_debug, clippy::todo)]
4#![warn(missing_docs, missing_debug_implementations)]
5#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
6
7#[cfg(feature = "reqwest")]
8pub use client::*;
9
10pub mod schemas;
11
12/// The version of this client.
13pub const VERSION: &str = env!("CARGO_PKG_VERSION");
14
15#[cfg(feature = "reqwest")]
16mod client {
17    use std::{collections::HashMap, fmt::Display};
18
19    use serde::Deserialize;
20    use url::Url;
21
22    use crate::schemas::{
23        configuration::PublicConfig,
24        environments::{BaseResourceUsage, Environment, GetBaseResourceUsageError},
25        programs::{
26            BuildError, BuildRequest, BuildResult, BuildRunError, BuildRunRequest, BuildRunResult,
27            RunError, RunRequest, RunResult,
28        },
29        ErrorResponse,
30    };
31
32    /// An asynchronous client for Sandkasten.
33    #[derive(Debug, Clone)]
34    pub struct SandkastenClient {
35        base_url: Url,
36        client: reqwest::Client,
37    }
38
39    /// A synchronous client for Sandkasten.
40    #[cfg(feature = "blocking")]
41    #[derive(Debug, Clone)]
42    pub struct BlockingSandkastenClient {
43        base_url: Url,
44        client: reqwest::blocking::Client,
45    }
46
47    impl SandkastenClient {
48        /// Create a new client for the Sandkasten instance at `base_url`.
49        pub fn new(base_url: Url) -> Self {
50            Self {
51                base_url,
52                client: reqwest::Client::new(),
53            }
54        }
55
56        /// Return the version of the sandkasten server.
57        pub async fn version(&self) -> Result<String> {
58            Ok(self.openapi_spec().await?.info.version)
59        }
60    }
61
62    #[cfg(feature = "blocking")]
63    impl BlockingSandkastenClient {
64        /// Create a new client for the Sandkasten instance at `base_url`.
65        pub fn new(base_url: Url) -> Self {
66            Self {
67                base_url,
68                client: reqwest::blocking::Client::new(),
69            }
70        }
71
72        /// Return the version of the sandkasten server.
73        pub fn version(&self) -> Result<String> {
74            Ok(self.openapi_spec()?.info.version)
75        }
76    }
77
78    /// The errors that may occur when using the client.
79    #[derive(Debug, thiserror::Error)]
80    pub enum Error<E> {
81        /// The endpoint url could not be parsed.
82        #[error("could not parse url: {0}")]
83        UrlParseError(#[from] url::ParseError),
84        /// [`reqwest`] returned an error.
85        #[error("reqwest error: {0}")]
86        ReqwestError(#[from] reqwest::Error),
87        /// Sandkasten returned an error response.
88        #[error("sandkasten returned an error: {0:?}")]
89        ErrorResponse(Box<ErrorResponse<E>>),
90    }
91
92    /// Type alias for `Result<T, sandkasten_client::Error<E>>`.
93    pub type Result<T, E = ()> = std::result::Result<T, Error<E>>;
94
95    macro_rules! endpoints {
96        ($( $(#[doc = $doc:literal])* $vis:vis $func:ident( $(path: $args:ident),* $(,)? $(json: $data:ty)? ): $method:ident $path:literal => $ok:ty $(, $err:ty)?; )*) => {
97            impl SandkastenClient {
98                $(
99                    $(#[doc = $doc])*
100                    $vis async fn $func(&self, $($args: impl Display,)* $(data: &$data)?) -> Result<$ok, $($err)?> {
101                        let response = self
102                            .client
103                            .$method(self.base_url.join(&format!($path))?)
104                            $(.json(data as &$data))?
105                            .send()
106                            .await?;
107                        if response.status().is_success() {
108                            Ok(response.json().await?)
109                        } else {
110                            Err(Error::ErrorResponse(response.json().await?))
111                        }
112                    }
113                )*
114            }
115
116            #[cfg(feature = "blocking")]
117            impl BlockingSandkastenClient {
118                $(
119                    $(#[doc = $doc])*
120                    $vis fn $func(&self, $($args: impl Display,)* $(data: &$data)?) -> Result<$ok, $($err)?> {
121                        let response = self
122                            .client
123                            .$method(self.base_url.join(&format!($path))?)
124                            $(.json(data as &$data))?
125                            .send()?;
126                        if response.status().is_success() {
127                            Ok(response.json()?)
128                        } else {
129                            Err(Error::ErrorResponse(response.json()?))
130                        }
131                    }
132                )*
133            }
134        };
135    }
136
137    endpoints! {
138        /// Return the public configuration of Sandkasten.
139        pub get_config(): get "config" => PublicConfig;
140        /// Return a list of all environments.
141        pub list_environments(): get "environments" => HashMap<String, Environment>;
142        /// Return the base resource usage of an environment when running just a very basic program.
143        pub get_base_resource_usage(path: environment): get "environments/{environment}/resource_usage" => BaseResourceUsage, GetBaseResourceUsageError;
144        /// Build and immediately run a program.
145        pub build_and_run(json: BuildRunRequest): post "run" => BuildRunResult, BuildRunError;
146        /// Upload and compile a program.
147        pub build(json: BuildRequest): post "programs" => BuildResult, BuildError;
148        /// Run a program that has previously been built.
149        pub run(path: program_id, json: RunRequest): post "programs/{program_id}/run" => RunResult, RunError;
150
151        openapi_spec(): get "openapi.json" => OpenAPISpec;
152    }
153
154    #[derive(Deserialize)]
155    struct OpenAPISpec {
156        info: OpenAPISpecInfo,
157    }
158
159    #[derive(Deserialize)]
160    struct OpenAPISpecInfo {
161        version: String,
162    }
163}