1pub mod languagetool;
7
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10use std::fmt;
11
12#[link(wasm_import_module = "extism:host/user")]
13unsafe extern "C" {
14 fn hammer_server_request(request: u64, body: u64) -> u64;
15}
16
17pub struct Response {
18 pub status: u16,
19 pub body: String,
20}
21
22impl Response {
23 pub fn is_success(&self) -> bool {
24 (200..300).contains(&self.status)
25 }
26
27 pub fn error(&self) -> Option<ServerError> {
30 if self.is_success() {
31 return None;
32 }
33 let kind = match self.status {
34 429 => ErrorKind::RateLimited,
35 400..500 => ErrorKind::Rejected,
36 _ => ErrorKind::ServiceFailed,
37 };
38 let message = display_message(&self.body).unwrap_or_else(|| {
39 let detail: String = self.body.chars().take(200).collect();
40 match detail.trim().is_empty() {
41 true => format!("The service replied {}", self.status),
42 false => format!("The service replied {}: {}", self.status, detail.trim()),
43 }
44 });
45 Some(ServerError { kind, message })
46 }
47}
48
49fn display_message(body: &str) -> Option<String> {
51 let reply: serde_json::Value = serde_json::from_str(body).ok()?;
52 reply.get("displayMessage")?.as_str().filter(|it| !it.is_empty()).map(str::to_owned)
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum ErrorKind {
60 NotSignedIn,
62 Unavailable,
64 Network,
66 NotPermitted,
68 Invalid,
70 RateLimited,
72 Rejected,
74 ServiceFailed,
76}
77
78#[derive(Debug)]
79pub struct ServerError {
80 pub kind: ErrorKind,
81 pub message: String,
82}
83
84impl fmt::Display for ServerError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(&self.message)
87 }
88}
89
90impl std::error::Error for ServerError {}
91
92pub fn request(
96 service: &str,
97 method: &str,
98 path: &str,
99 headers: &[(&str, &str)],
100 body: Option<&[u8]>,
101) -> Result<Response, ServerError> {
102 let request = crate::to_kernel(&encode_request(service, method, path, headers));
103 let body = body.filter(|it| !it.is_empty()).map_or(0, crate::to_kernel);
104 let reply = unsafe { hammer_server_request(request, body) };
105 unsafe {
106 crate::free(request);
107 if body != 0 {
108 crate::free(body);
109 }
110 }
111 if reply == 0 {
112 return Err(ServerError { kind: ErrorKind::Invalid, message: "The sync server gave no reply".into() });
113 }
114 let bytes = crate::from_kernel(reply);
115 unsafe { crate::free(reply) };
116 parse_reply(&bytes)
117}
118
119fn encode_request(service: &str, method: &str, path: &str, headers: &[(&str, &str)]) -> Vec<u8> {
121 #[derive(Serialize)]
122 struct Request<'a> {
123 service: &'a str,
124 method: &'a str,
125 path: &'a str,
126 headers: BTreeMap<&'a str, &'a str>,
127 }
128 let request = Request { service, method, path, headers: headers.iter().copied().collect() };
129 serde_json::to_vec(&request).expect("strings always serialize")
130}
131
132fn parse_reply(bytes: &[u8]) -> Result<Response, ServerError> {
133 #[derive(Deserialize)]
134 struct Reply {
135 status: Option<u16>,
136 body: Option<String>,
137 error: Option<String>,
138 message: Option<String>,
139 }
140 let reply: Reply = serde_json::from_slice(bytes).map_err(|e| ServerError { kind: ErrorKind::Invalid, message: e.to_string() })?;
141 if let Some(status) = reply.status {
142 return Ok(Response { status, body: reply.body.unwrap_or_default() });
143 }
144 let kind = match reply.error.as_deref() {
145 Some("not_signed_in") => ErrorKind::NotSignedIn,
146 Some("unavailable") => ErrorKind::Unavailable,
147 Some("network") => ErrorKind::Network,
148 Some("not_permitted") => ErrorKind::NotPermitted,
149 _ => ErrorKind::Invalid,
150 };
151 Err(ServerError { kind, message: reply.message.unwrap_or_else(|| "The sync server gave no reply".into()) })
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn a_request_names_its_headers_in_an_object() {
160 let json = encode_request("languagetool", "POST", "/v2/check", &[("Content-Type", "application/x-www-form-urlencoded")]);
161 assert_eq!(
162 String::from_utf8(json).unwrap(),
163 r#"{"service":"languagetool","method":"POST","path":"/v2/check","headers":{"Content-Type":"application/x-www-form-urlencoded"}}"#
164 );
165 }
166
167 #[test]
168 fn a_status_is_a_response() {
169 let response = parse_reply(br#"{"status":200,"body":"{\"matches\":[]}"}"#).unwrap();
170 assert_eq!(response.status, 200);
171 assert_eq!(response.body, r#"{"matches":[]}"#);
172 assert!(response.is_success());
173 }
174
175 #[test]
176 fn an_error_keeps_its_kind_and_message() {
177 let error = parse_reply(br#"{"error":"not_signed_in","message":"Sign in"}"#).err().unwrap();
178 assert_eq!(error.kind, ErrorKind::NotSignedIn);
179 assert_eq!(error.to_string(), "Sign in");
180 }
181
182 #[test]
183 fn a_status_names_its_error_with_the_servers_message() {
184 let response = |status, body: &str| Response { status, body: body.into() };
185 assert!(response(200, "").error().is_none());
186 assert_eq!(response(429, "").error().unwrap().kind, ErrorKind::RateLimited);
187 let refused = response(413, r#"{"error":"api_services_error_toolarge","displayMessage":"Too large"}"#).error().unwrap();
188 assert_eq!((refused.kind, refused.message.as_str()), (ErrorKind::Rejected, "Too large"));
189 let failed = response(502, "").error().unwrap();
190 assert_eq!((failed.kind, failed.message.as_str()), (ErrorKind::ServiceFailed, "The service replied 502"));
191 assert_eq!(response(400, "Bad language").error().unwrap().message, "The service replied 400: Bad language");
192 }
193
194 #[test]
195 fn an_unknown_error_is_invalid() {
196 let error = parse_reply(br#"{"error":"something_new"}"#).err().unwrap();
197 assert_eq!(error.kind, ErrorKind::Invalid);
198 }
199}