1use std::{collections::BTreeMap, fmt::Debug};
7
8use chrono::{DateTime, Utc};
9use log::{Level, debug, log_enabled};
10use oauth10a::client::{
11 ClientError, RestClient,
12 bytes::Buf,
13 reqwest::{self, Method},
14 url,
15};
16use serde::{Deserialize, Serialize};
17
18use crate::Client;
19
20pub mod deployments;
21
22#[derive(thiserror::Error, Debug)]
26pub enum Error {
27 #[error("failed to parse endpoint '{0}', {1}")]
28 ParseUrl(String, url::ParseError),
29 #[error("failed to list functions for organisation '{0}', {1}")]
30 List(String, ClientError),
31 #[error("failed to create function on organisation '{0}', {1}")]
32 Create(String, ClientError),
33 #[error("failed to get function '{0}' for organisation '{1}', {2}")]
34 Get(String, String, ClientError),
35 #[error("failed to update function '{0}' of organisation '{1}', {2}")]
36 Update(String, String, ClientError),
37 #[error("failed to delete function '{0}' of organisation '{1}', {2}")]
38 Delete(String, String, ClientError),
39 #[error("failed to aggregate body, {0}")]
40 BodyAggregation(reqwest::Error),
41 #[error("failed to deserialize execute response payload, {0}")]
42 Deserialize(serde_json::Error),
43 #[error("failed to execute request, {0}")]
44 Execute(reqwest::Error),
45 #[error("failed to execute request, got status code {0}")]
46 StatusCode(u16),
47}
48
49#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
53pub struct Opts {
54 #[serde(rename = "name")]
55 pub name: Option<String>,
56 #[serde(rename = "description")]
57 pub description: Option<String>,
58 #[serde(rename = "tag")]
59 pub tag: Option<String>,
60 #[serde(rename = "environment")]
61 pub environment: BTreeMap<String, String>,
62 #[serde(rename = "maxMemory")]
63 pub max_memory: u64,
64 #[serde(rename = "maxInstances")]
65 pub max_instances: u64,
66}
67
68impl Default for Opts {
69 fn default() -> Self {
70 Self {
71 name: None,
72 description: None,
73 tag: None,
74 environment: BTreeMap::new(),
75 max_memory: 512 * 1024 * 1024,
76 max_instances: 1,
77 }
78 }
79}
80
81#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
85pub struct Function {
86 #[serde(rename = "id")]
87 pub id: String,
88 #[serde(rename = "ownerId")]
89 pub owner_id: String,
90 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
91 pub name: Option<String>,
92 #[serde(rename = "description", skip_serializing_if = "Option::is_none")]
93 pub description: Option<String>,
94 #[serde(rename = "tag", skip_serializing_if = "Option::is_none")]
95 pub tag: Option<String>,
96 #[serde(rename = "environment")]
97 pub environment: BTreeMap<String, String>,
98 #[serde(rename = "maxMemory")]
99 pub max_memory: u64,
100 #[serde(rename = "maxInstances")]
101 pub max_instances: u64,
102 #[serde(rename = "createdAt")]
103 pub created_at: DateTime<Utc>,
104 #[serde(rename = "updatedAt")]
105 pub updated_at: DateTime<Utc>,
106}
107
108#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
112#[serde(untagged)]
113pub enum ExecutionResult {
114 Ok {
115 #[serde(rename = "stdout")]
116 stdout: String,
117 #[serde(rename = "stderr")]
118 stderr: String,
119 #[serde(rename = "dmesg")]
120 dmesg: String,
121 #[serde(rename = "current_pages")]
122 current_pages: Option<u64>,
123 },
124 Err {
125 #[serde(rename = "error")]
126 error: String,
127 },
128}
129
130impl ExecutionResult {
131 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
132 pub fn ok<T, U, V>(stdout: T, stderr: U, dmesg: V, current_pages: Option<u64>) -> Self
133 where
134 T: ToString,
135 U: ToString,
136 V: ToString,
137 {
138 Self::Ok {
139 stdout: stdout.to_string(),
140 stderr: stderr.to_string(),
141 dmesg: dmesg.to_string(),
142 current_pages,
143 }
144 }
145
146 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
147 pub fn err<T>(error: T) -> Self
148 where
149 T: ToString,
150 {
151 Self::Err {
152 error: error.to_string(),
153 }
154 }
155
156 #[cfg_attr(feature = "tracing", tracing::instrument)]
157 pub fn is_ok(&self) -> bool {
158 matches!(self, Self::Ok { .. })
159 }
160
161 #[cfg_attr(feature = "tracing", tracing::instrument)]
162 pub fn is_err(&self) -> bool {
163 !self.is_ok()
164 }
165}
166
167#[cfg_attr(feature = "tracing", tracing::instrument)]
171pub async fn list(client: &Client, organisation_id: &str) -> Result<Vec<Function>, Error> {
173 let path = format!(
174 "{}/v4/functions/organisations/{organisation_id}/functions",
175 client.endpoint
176 );
177
178 #[cfg(feature = "logging")]
179 if log_enabled!(Level::Debug) {
180 debug!(
181 "execute a request to list functions for organisation, path: '{path}', organisation: '{organisation_id}'"
182 );
183 }
184
185 client
186 .get(&path)
187 .await
188 .map_err(|err| Error::List(organisation_id.to_string(), err))
189}
190
191#[cfg_attr(feature = "tracing", tracing::instrument)]
192pub async fn create(
194 client: &Client,
195 organisation_id: &str,
196 opts: &Opts,
197) -> Result<Function, Error> {
198 let path = format!(
199 "{}/v4/functions/organisations/{organisation_id}/functions",
200 client.endpoint
201 );
202
203 #[cfg(feature = "logging")]
204 if log_enabled!(Level::Debug) {
205 debug!(
206 "execute a request to create function, path: '{path}', organisation: {organisation_id}"
207 );
208 }
209
210 client
211 .post(&path, opts)
212 .await
213 .map_err(|err| Error::Create(organisation_id.to_string(), err))
214}
215
216#[cfg_attr(feature = "tracing", tracing::instrument)]
217pub async fn get(
219 client: &Client,
220 organisation_id: &str,
221 function_id: &str,
222) -> Result<Function, Error> {
223 let path = format!(
224 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}",
225 client.endpoint
226 );
227
228 #[cfg(feature = "logging")]
229 if log_enabled!(Level::Debug) {
230 debug!(
231 "execute a request to get function, path: '{path}', organisation: {organisation_id}, function: {function_id}"
232 );
233 }
234
235 client
236 .get(&path)
237 .await
238 .map_err(|err| Error::Get(function_id.to_string(), organisation_id.to_string(), err))
239}
240
241#[cfg_attr(feature = "tracing", tracing::instrument)]
242pub async fn update(
244 client: &Client,
245 organisation_id: &str,
246 function_id: &str,
247 opts: &Opts,
248) -> Result<Function, Error> {
249 let path = format!(
250 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}",
251 client.endpoint
252 );
253
254 #[cfg(feature = "logging")]
255 if log_enabled!(Level::Debug) {
256 debug!(
257 "execute a request to update function, path: '{path}', organisation: {organisation_id}, function: {function_id}"
258 );
259 }
260
261 client
262 .put(&path, opts)
263 .await
264 .map_err(|err| Error::Update(function_id.to_string(), organisation_id.to_string(), err))
265}
266
267#[cfg_attr(feature = "tracing", tracing::instrument)]
268pub async fn delete(
270 client: &Client,
271 organisation_id: &str,
272 function_id: &str,
273) -> Result<(), Error> {
274 let path = format!(
275 "{}/v4/functions/organisations/{organisation_id}/functions/{function_id}",
276 client.endpoint
277 );
278
279 #[cfg(feature = "logging")]
280 if log_enabled!(Level::Debug) {
281 debug!(
282 "execute a request to delete function, path: '{path}', organisation: {organisation_id}, function: {function_id}"
283 );
284 }
285
286 client
287 .delete(&path)
288 .await
289 .map_err(|err| Error::Delete(function_id.to_string(), organisation_id.to_string(), err))
290}
291
292#[cfg_attr(feature = "tracing", tracing::instrument)]
293pub async fn execute(client: &Client, endpoint: &str) -> Result<ExecutionResult, Error> {
295 let req = reqwest::Request::new(
296 Method::GET,
297 endpoint
298 .parse()
299 .map_err(|err| Error::ParseUrl(endpoint.to_string(), err))?,
300 );
301
302 let res = client.inner().execute(req).await.map_err(Error::Execute)?;
303 let buf = res.bytes().await.map_err(Error::BodyAggregation)?;
304
305 serde_json::from_reader(buf.reader()).map_err(Error::Deserialize)
306}