1use crate::common::{get_url, GetModelsResponse, Predictions};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::time;
5
6#[derive(Serialize)]
7struct PredictRequest {
8 model_name: String,
9 input: String,
10}
11
12#[derive(Deserialize)]
13struct PredictResponse {
14 output: String,
15}
16
17#[derive(Serialize)]
18struct AddModelRequest {
19 model_name: String,
20}
21
22#[derive(Serialize)]
23struct UpdateModelRequest {
24 model_name: String,
25}
26
27#[async_trait]
28pub trait Client {
29 async fn health_check(&self) -> anyhow::Result<()>;
30 async fn predict(&self, model_name: String, model_input: String)
31 -> anyhow::Result<Predictions>;
32 async fn add_model(&self, model_name: String) -> anyhow::Result<()>;
33 async fn update_model(&self, model_name: String) -> anyhow::Result<()>;
34 async fn delete_model(&self, model_name: String) -> anyhow::Result<()>;
35 async fn get_models(&self) -> anyhow::Result<GetModelsResponse>;
36}
37
38pub struct ApiClient {
39 client: reqwest::Client,
40 base_url: String,
41 timeout: time::Duration,
42}
43
44impl ApiClient {
45 pub fn builder() -> ApiClientBuilder {
46 ApiClientBuilder::default()
47 }
48}
49
50#[derive(Default)]
51pub struct ApiClientBuilder {
52 base_url: String,
53 timeout: time::Duration,
54}
55
56impl ApiClientBuilder {
57 pub fn new(base_url: String) -> ApiClientBuilder {
58 ApiClientBuilder {
59 base_url: get_url(base_url),
60 timeout: time::Duration::from_secs(5),
61 }
62 }
63
64 pub fn with_timeout(mut self, timeout: u64) -> ApiClientBuilder {
65 self.timeout = time::Duration::from_secs(timeout);
66 self
67 }
68
69 pub fn build(self) -> anyhow::Result<ApiClient> {
70 let client = match reqwest::Client::builder().build() {
71 Ok(client) => client,
72 Err(err) => {
73 anyhow::bail!("failed to create reqwest client: {}", err)
74 }
75 };
76 Ok(ApiClient {
77 client,
78 base_url: self.base_url,
79 timeout: self.timeout,
80 })
81 }
82}
83
84#[async_trait]
85impl Client for ApiClient {
86 async fn health_check(&self) -> anyhow::Result<()> {
87 let url = format!("{}/{}", self.base_url, "healthcheck");
88 match self.client.get(url).timeout(self.timeout).send().await {
89 Ok(resp) => match resp.status().is_success() {
90 true => Ok(()),
91 false => {
92 anyhow::bail!(
93 "failed to health check J.A.M.S server ❌: {}",
94 resp.text().await.unwrap()
95 )
96 }
97 },
98 Err(err) => {
99 anyhow::bail!("failed to health check J.A.M.S server ❌: {}", err)
100 }
101 }
102 }
103
104 async fn predict(
105 &self,
106 model_name: String,
107 model_input: String,
108 ) -> anyhow::Result<Predictions> {
109 let url = format!("{}/{}", self.base_url, "api/predict");
110 match self
111 .client
112 .post(url)
113 .json(&PredictRequest {
114 model_name,
115 input: model_input,
116 })
117 .timeout(self.timeout)
118 .send()
119 .await
120 {
121 Ok(resp) => match resp.status().is_success() {
122 true => {
123 let predictions = resp.json::<PredictResponse>().await?;
124 match Predictions::from_bytes(predictions.output.as_ref()) {
125 Ok(predictions) => Ok(predictions),
126 Err(err) => {
127 anyhow::bail!(
128 "failed to parse response from bytes ❌: {}",
129 err.to_string()
130 )
131 }
132 }
133 }
134 false => {
135 anyhow::bail!(
136 "failed to get predictions ❌: {}",
137 resp.text().await.unwrap()
138 )
139 }
140 },
141 Err(err) => {
142 anyhow::bail!("failed to make predict request ❌: {}", err.to_string())
143 }
144 }
145 }
146
147 async fn add_model(&self, model_name: String) -> anyhow::Result<()> {
148 let url = format!("{}/{}", self.base_url, "api/models");
149 match self
150 .client
151 .post(url)
152 .json(&AddModelRequest { model_name })
153 .timeout(self.timeout)
154 .send()
155 .await
156 {
157 Ok(resp) => match resp.status().is_success() {
158 true => Ok(()),
159 false => {
160 anyhow::bail!("failed to add model ❌: {}", resp.text().await.unwrap())
161 }
162 },
163 Err(err) => {
164 anyhow::bail!("failed to make add_model request ❌: {}", err.to_string())
165 }
166 }
167 }
168
169 async fn update_model(&self, model_name: String) -> anyhow::Result<()> {
170 let url = format!("{}/{}", self.base_url, "api/models");
171 match self
172 .client
173 .put(url)
174 .json(&UpdateModelRequest { model_name })
175 .timeout(self.timeout)
176 .send()
177 .await
178 {
179 Ok(resp) => match resp.status().is_success() {
180 true => Ok(()),
181 false => {
182 anyhow::bail!("failed to update model ❌: {}", resp.text().await.unwrap())
183 }
184 },
185 Err(err) => {
186 anyhow::bail!(
187 "failed to make update_model request ❌: {}",
188 err.to_string()
189 )
190 }
191 }
192 }
193
194 async fn delete_model(&self, model_name: String) -> anyhow::Result<()> {
195 let url = format!(
196 "{}/{}?model_name={}",
197 self.base_url, "api/models", model_name
198 );
199 match self.client.delete(url).timeout(self.timeout).send().await {
200 Ok(resp) => match resp.status().is_success() {
201 true => Ok(()),
202 false => {
203 anyhow::bail!("failed to delete model ❌: {}", resp.text().await.unwrap())
204 }
205 },
206 Err(err) => {
207 anyhow::bail!(
208 "failed to make delete_model request ❌: {}",
209 err.to_string()
210 )
211 }
212 }
213 }
214
215 async fn get_models(&self) -> anyhow::Result<GetModelsResponse> {
216 let url = format!("{}/{}", self.base_url, "api/models");
217 match self.client.get(url).timeout(self.timeout).send().await {
218 Ok(resp) => match resp.status().is_success() {
219 true => Ok(resp.json::<GetModelsResponse>().await?),
220 false => {
221 anyhow::bail!("failed to get models ❌: {}", resp.text().await.unwrap())
222 }
223 },
224 Err(err) => {
225 anyhow::bail!("failed to make get_models request ❌: {}", err.to_string())
226 }
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use std::env;
235 fn get_url() -> String {
236 let hostname = env::var("JAMS_HTTP_HOSTNAME").unwrap_or("0.0.0.0".to_string());
237 format!("{}:3000", hostname)
238 }
239
240 #[tokio::test]
241 async fn successfully_sends_health_check_request() {
242 let client = ApiClientBuilder::new(get_url())
244 .with_timeout(2)
245 .build()
246 .unwrap();
247
248 let resp = client.health_check().await;
250
251 assert!(resp.is_ok())
253 }
254
255 #[tokio::test]
256 async fn successfully_sends_get_model_request() {
257 let client = ApiClientBuilder::new(get_url())
259 .with_timeout(2)
260 .build()
261 .unwrap();
262
263 let result = client.get_models().await;
265
266 assert!(result.is_ok());
268 let res = result.unwrap();
269 assert!(!res.models.is_empty());
271 }
272
273 #[tokio::test]
274 async fn successfully_sends_delete_model_request() {
275 let client = ApiClientBuilder::new(get_url())
277 .with_timeout(2)
278 .build()
279 .unwrap();
280
281 client
283 .add_model("pytorch-my_awesome_californiahousing_model".to_string())
284 .await
285 .unwrap();
286
287 let resp = client
288 .delete_model("my_awesome_californiahousing_model".to_string())
289 .await;
290
291 assert!(resp.is_ok())
293 }
294
295 #[tokio::test]
296 async fn successfully_sends_add_model_request() {
297 let client = ApiClientBuilder::new(get_url())
299 .with_timeout(2)
300 .build()
301 .unwrap();
302
303 client
305 .delete_model("my_awesome_penguin_model".to_string())
306 .await
307 .unwrap();
308
309 let resp = client
310 .add_model("tensorflow-my_awesome_penguin_model".to_string())
311 .await;
312
313 assert!(resp.is_ok())
315 }
316
317 #[tokio::test]
318 async fn successfully_sends_update_model_request() {
319 let client = ApiClientBuilder::new(get_url())
321 .with_timeout(2)
322 .build()
323 .unwrap();
324
325 let resp = client.update_model("titanic_model".to_string()).await;
327
328 assert!(resp.is_ok())
330 }
331
332 #[tokio::test]
333 async fn successfully_sends_predict_model_request() {
334 let client = ApiClientBuilder::new(get_url())
336 .with_timeout(2)
337 .build()
338 .unwrap();
339
340 let model_name = "titanic_model".to_string();
342 let model_input = serde_json::json!(
343 {
344 "pclass": ["1", "3"],
345 "sex": ["male", "female"],
346 "age": [22.0, 23.79929292929293],
347 "sibsp": ["0", "1", ],
348 "parch": ["0", "0"],
349 "fare": [151.55, 14.4542],
350 "embarked": ["S", "C"],
351 "class": ["First", "Third"],
352 "who": ["man", "woman"],
353 "adult_male": ["True", "False"],
354 "deck": ["Unknown", "Unknown"],
355 "embark_town": ["Southampton", "Cherbourg"],
356 "alone": ["True", "False"]
357 }
358 )
359 .to_string();
360 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
361 let resp = client.predict(model_name, model_input).await;
362
363 assert!(resp.is_ok())
365 }
366}