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
12pub 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 #[derive(Debug, Clone)]
34 pub struct SandkastenClient {
35 base_url: Url,
36 client: reqwest::Client,
37 }
38
39 #[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 pub fn new(base_url: Url) -> Self {
50 Self {
51 base_url,
52 client: reqwest::Client::new(),
53 }
54 }
55
56 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 pub fn new(base_url: Url) -> Self {
66 Self {
67 base_url,
68 client: reqwest::blocking::Client::new(),
69 }
70 }
71
72 pub fn version(&self) -> Result<String> {
74 Ok(self.openapi_spec()?.info.version)
75 }
76 }
77
78 #[derive(Debug, thiserror::Error)]
80 pub enum Error<E> {
81 #[error("could not parse url: {0}")]
83 UrlParseError(#[from] url::ParseError),
84 #[error("reqwest error: {0}")]
86 ReqwestError(#[from] reqwest::Error),
87 #[error("sandkasten returned an error: {0:?}")]
89 ErrorResponse(Box<ErrorResponse<E>>),
90 }
91
92 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 pub get_config(): get "config" => PublicConfig;
140 pub list_environments(): get "environments" => HashMap<String, Environment>;
142 pub get_base_resource_usage(path: environment): get "environments/{environment}/resource_usage" => BaseResourceUsage, GetBaseResourceUsageError;
144 pub build_and_run(json: BuildRunRequest): post "run" => BuildRunResult, BuildRunError;
146 pub build(json: BuildRequest): post "programs" => BuildResult, BuildError;
148 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}