1use std::path::Path;
13
14use serde_json::{json, Value};
15
16use crate::{
17 body, delete_server_query, delete_storage_query, label_query, redact_upload_url, Backups, BootOrder, Console, DeviceKind, Label, NewStorage,
18 Reply, Stop, UpCloudApi, WithStorages,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Method {
23 Get,
24 Post,
25 Put,
26 Delete,
27}
28
29impl Method {
30 pub fn as_str(self) -> &'static str {
31 match self {
32 Method::Get => "GET",
33 Method::Post => "POST",
34 Method::Put => "PUT",
35 Method::Delete => "DELETE",
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub enum Call<'a> {
43 Api { method: Method, path: String, body: Option<Value> },
46 Upload { url: &'a str, file: &'a Path },
49}
50
51impl Call<'_> {
52 pub fn line(&self) -> String {
54 match self {
55 Call::Api { method, path, .. } => format!("{} {path}", method.as_str()),
56 Call::Upload { url, .. } => format!("PUT {}", redact_upload_url(url)),
57 }
58 }
59}
60
61pub trait Exchange {
63 fn describe(&self) -> String;
64 fn is_the_account(&self) -> bool;
65 fn exchange(&self, call: Call<'_>) -> Result<Reply, String>;
66}
67
68impl<E: Exchange + ?Sized> Exchange for &E {
71 fn describe(&self) -> String {
72 (**self).describe()
73 }
74 fn is_the_account(&self) -> bool {
75 (**self).is_the_account()
76 }
77 fn exchange(&self, call: Call<'_>) -> Result<Reply, String> {
78 (**self).exchange(call)
79 }
80}
81
82pub struct Over<E>(pub E);
85
86fn api(method: Method, path: String, body: Option<Value>) -> Call<'static> {
87 Call::Api { method, path, body }
88}
89
90impl<E: Exchange> Over<E> {
91 fn get(&self, path: String) -> Result<Reply, String> {
92 self.0.exchange(api(Method::Get, path, None))
93 }
94 fn send(&self, method: Method, path: String, body: Value) -> Result<Reply, String> {
95 self.0.exchange(api(method, path, Some(body)))
96 }
97}
98
99impl<E: Exchange> UpCloudApi for Over<E> {
100 fn describe(&self) -> String {
101 self.0.describe()
102 }
103 fn is_the_account(&self) -> bool {
104 self.0.is_the_account()
105 }
106 fn account(&self) -> Result<Reply, String> {
107 self.get("/account".into())
108 }
109 fn price(&self) -> Result<Reply, String> {
110 self.get("/price".into())
111 }
112 fn servers(&self) -> Result<Reply, String> {
113 self.get("/server".into())
114 }
115 fn servers_labelled(&self, labels: &[Label<'_>]) -> Result<Reply, String> {
116 self.get(format!("/server{}", label_query(labels)))
117 }
118 fn server(&self, uuid: &str) -> Result<Reply, String> {
119 self.get(format!("/server/{uuid}"))
120 }
121 fn firewall_rules(&self, uuid: &str) -> Result<Reply, String> {
122 self.get(format!("/server/{uuid}/firewall_rule"))
123 }
124 fn storages_private(&self) -> Result<Reply, String> {
125 self.get("/storage/private".into())
126 }
127 fn storages_labelled(&self, labels: &[Label<'_>]) -> Result<Reply, String> {
128 self.get(format!("/storage{}", label_query(labels)))
129 }
130 fn storage(&self, uuid: &str) -> Result<Reply, String> {
131 self.get(format!("/storage/{uuid}"))
132 }
133 fn zones(&self) -> Result<Reply, String> {
134 self.get("/zone".into())
135 }
136 fn plans(&self) -> Result<Reply, String> {
137 self.get("/plan".into())
138 }
139 fn create_server(&self, document: &Value) -> Result<Reply, String> {
140 self.send(Method::Post, "/server".into(), document.clone())
141 }
142 fn stop_server(&self, uuid: &str, stop: Stop) -> Result<Reply, String> {
143 self.send(Method::Post, format!("/server/{uuid}/stop"), body::stop(stop))
144 }
145 fn start_server(&self, uuid: &str) -> Result<Reply, String> {
146 self.send(Method::Post, format!("/server/{uuid}/start"), json!({}))
147 }
148 fn modify_server_plan(&self, uuid: &str, plan: &str) -> Result<Reply, String> {
149 self.send(Method::Put, format!("/server/{uuid}"), body::server_plan(plan))
150 }
151 fn set_boot_order(&self, uuid: &str, order: BootOrder) -> Result<Reply, String> {
152 self.send(Method::Put, format!("/server/{uuid}"), body::boot_order(order))
153 }
154 fn set_console(&self, uuid: &str, console: Console<'_>) -> Result<Reply, String> {
155 self.send(Method::Put, format!("/server/{uuid}"), body::console(&console))
156 }
157 fn attach_storage(&self, server: &str, kind: DeviceKind, storage: &str, at: Option<&str>) -> Result<Reply, String> {
158 self.send(Method::Post, format!("/server/{server}/storage/attach"), body::attach(kind, storage, at))
159 }
160 fn detach_storage(&self, server: &str, address: &str) -> Result<Reply, String> {
161 self.send(Method::Post, format!("/server/{server}/storage/detach"), body::detach(address))
162 }
163 fn eject_cdrom(&self, server: &str) -> Result<Reply, String> {
164 self.send(Method::Post, format!("/server/{server}/cdrom/eject"), json!({}))
165 }
166 fn delete_server(&self, uuid: &str, with: WithStorages) -> Result<Reply, String> {
167 self.0.exchange(api(Method::Delete, format!("/server/{uuid}{}", delete_server_query(with)), None))
168 }
169 fn create_storage(&self, new: &NewStorage<'_>) -> Result<Reply, String> {
170 self.send(Method::Post, "/storage".into(), body::create_storage(new))
171 }
172 fn clone_storage(&self, uuid: &str, title: &str, zone: &str, tier: &str) -> Result<Reply, String> {
173 self.send(Method::Post, format!("/storage/{uuid}/clone"), body::clone_storage(title, zone, tier))
174 }
175 fn import_direct_upload(&self, uuid: &str) -> Result<Reply, String> {
176 self.send(Method::Post, format!("/storage/{uuid}/import"), body::direct_upload())
177 }
178 fn upload_direct(&self, url: &str, file: &Path) -> Result<Reply, String> {
179 self.0.exchange(Call::Upload { url, file })
180 }
181 fn modify_storage_size(&self, uuid: &str, gb: u64) -> Result<Reply, String> {
182 self.send(Method::Put, format!("/storage/{uuid}"), body::storage_size(gb))
183 }
184 fn resize_filesystem(&self, uuid: &str) -> Result<Reply, String> {
185 self.send(Method::Post, format!("/storage/{uuid}/resize"), json!({}))
186 }
187 fn delete_storage(&self, uuid: &str, backups: Backups) -> Result<Reply, String> {
188 self.0.exchange(api(Method::Delete, format!("/storage/{uuid}{}", delete_storage_query(backups)), None))
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use std::cell::RefCell;
196
197 #[derive(Default)]
198 struct Recorder(RefCell<Vec<String>>);
199 impl Exchange for Recorder {
200 fn describe(&self) -> String {
201 "recorder".into()
202 }
203 fn is_the_account(&self) -> bool {
204 false
205 }
206 fn exchange(&self, call: Call<'_>) -> Result<Reply, String> {
207 self.0.borrow_mut().push(call.line());
208 Ok(Reply { status: 200, body: Value::Null, text: String::new() })
209 }
210 }
211
212 #[test]
213 fn every_typed_call_is_one_line_of_the_real_api() {
214 let o = Over(Recorder::default());
215 o.server("u").unwrap();
216 o.delete_server("u", WithStorages::AndKeepBackups).unwrap();
217 o.storages_labelled(&[("monetize_ref", "r")]).unwrap();
218 o.upload_direct("https://fi-hel1.img.upcloud.com/uploader/session/SECRET", Path::new("/x")).unwrap();
219 assert_eq!(
220 *o.0 .0.borrow(),
221 vec![
222 "GET /server/u".to_string(),
223 "DELETE /server/u?storages=1&backups=keep".to_string(),
224 "GET /storage?label=monetize_ref%3Dr".to_string(),
225 "PUT https://fi-hel1.img.upcloud.com/uploader/session/…".to_string(),
226 ]
227 );
228 }
229}