Skip to main content

auric_runtime/data/adapters/
rest.rs

1use super::super::Adapter;
2use crate::Config;
3use crate::data::QueryRecordOptions;
4use anyhow::{Result, anyhow};
5use async_trait::async_trait;
6use jsonapi_core::Resource;
7use pluralizer::pluralize;
8use reqwest::Client;
9use serde_json::Value;
10use std::collections::HashMap;
11
12#[derive(Default)]
13pub struct RestAdapter {
14    api_url: String,
15}
16
17impl RestAdapter {
18    pub fn new(api_url: String) -> Self {
19        Self { api_url }
20    }
21}
22
23#[async_trait(?Send)]
24impl Adapter for RestAdapter {
25    fn init(&mut self, config: &Config) -> Result<()> {
26        let config = config.as_object().expect("Expected config to be an object");
27        let api_url = match config.get("api_url") {
28            None => return Err(anyhow!("Configuration requires an api_url field")),
29            Some(api_url) => match api_url.as_str() {
30                None => return Err(anyhow!("Expected api_url config field to be a string")),
31                Some(api_url) => api_url.to_owned(),
32            },
33        };
34        self.api_url = api_url;
35        Ok(())
36    }
37
38    async fn create_record(&self, resource: Resource) -> Result<Resource> {
39        // Perform request
40        let url = format!("{}/{}", self.api_url, resource.type_);
41        let client = Client::new();
42        let res = client.post(url).json(&resource.attributes).send().await?;
43
44        // Read response
45        let res_body: HashMap<String, Value> = res.json().await?;
46        let model_name = pluralize(&resource.type_, 1, false);
47        let data = match res_body.get(&model_name) {
48            None => return Err(anyhow!("resource not found")),
49            Some(data) => data,
50        };
51        let id = match data.as_object() {
52            None => return Err(anyhow!("expected an object")),
53            Some(map) => match map.get("id") {
54                None => return Err(anyhow!("expected returned object to include an id")),
55                Some(id) => id.as_str().unwrap_or_default().to_string(),
56            },
57        };
58        let finalized_resource = Resource {
59            type_: resource.type_,
60            id: Some(id),
61            lid: None,
62            attributes: data.clone(),
63            relationships: Default::default(),
64            links: None,
65            meta: None,
66        };
67        Ok(finalized_resource)
68    }
69
70    async fn delete_record(&self, resource: &Resource) -> Result<()> {
71        // Perform request
72        let url = format!(
73            "{}/{}/{}",
74            self.api_url,
75            resource.type_,
76            resource.id.clone().unwrap_or_default()
77        );
78        let client = Client::new();
79        let _res = client.delete(url).send().await?;
80        Ok(())
81    }
82
83    async fn find_record(&self, resource_type: &str, id: &str) -> Result<Resource> {
84        let resource_type = resource_type.to_string();
85        let id = id.to_string();
86
87        // Perform request
88        let url = format!("{}/{resource_type}/{id}", self.api_url);
89        let client = Client::new();
90        let res = client.get(url).send().await?;
91
92        // Read response
93        let res_body: HashMap<String, Value> = res.json().await?;
94        let model_name = pluralize(&resource_type, 1, false);
95        let data = match res_body.get(&model_name) {
96            None => return Err(anyhow!("resource not found")),
97            Some(data) => data,
98        };
99        let resource = Resource {
100            type_: resource_type,
101            id: Some(id),
102            lid: None,
103            attributes: data.clone(),
104            relationships: Default::default(),
105            links: None,
106            meta: None,
107        };
108        Ok(resource)
109    }
110
111    async fn query(&self, resource_type: &str, _query: Value) -> Result<Vec<Resource>> {
112        // Perform request
113        let url = format!("{}/{resource_type}", self.api_url);
114        let client = Client::new();
115        let res = client.get(url).send().await?;
116
117        // Read response
118        let res_body: HashMap<String, Value> = res.json().await?;
119        let array: Vec<Value> = match res_body.get(resource_type) {
120            None => return Err(anyhow!("resource not found")),
121            Some(data) => match data.as_array() {
122                None => return Err(anyhow!("expected an array of resources")),
123                Some(array) => array.clone(),
124            },
125        };
126        let mut resources = vec![];
127        for object in array {
128            let id = match object.get("id") {
129                None => return Err(anyhow!("resources require an id")),
130                Some(id) => match id.as_str() {
131                    None => return Err(anyhow!("resources require id to be a string")),
132                    Some(id) => id.to_string(),
133                },
134            };
135            let resource = Resource {
136                type_: resource_type.to_string(),
137                id: Some(id),
138                lid: None,
139                attributes: object.clone(),
140                relationships: Default::default(),
141                links: None,
142                meta: None,
143            };
144            resources.push(resource);
145        }
146        Ok(resources)
147    }
148
149    async fn query_record(
150        &self,
151        _resource_type: &str,
152        _query: Value,
153        _options: QueryRecordOptions,
154    ) -> Result<Option<Resource>> {
155        todo!()
156    }
157
158    async fn update_record(&self, resource: Resource) -> Result<Resource> {
159        // Perform request
160        let id = resource.id.clone().unwrap_or_default();
161        let url = format!("{}/{}/{}", self.api_url, resource.type_, id);
162        let client = Client::new();
163        let res = client.put(url).send().await?;
164
165        // Read response
166        let res_body: HashMap<String, Value> = res.json().await?;
167        let model_name = pluralize(&resource.type_, 1, false);
168        let data = match res_body.get(&model_name) {
169            None => return Err(anyhow!("resource not found")),
170            Some(data) => data,
171        };
172        let finalized_resource = Resource {
173            type_: resource.type_,
174            id: Some(resource.id.unwrap_or_default().to_string()),
175            lid: None,
176            attributes: data.clone(),
177            relationships: Default::default(),
178            links: None,
179            meta: None,
180        };
181        Ok(finalized_resource)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::RestAdapter;
188    use crate::data::Adapter;
189    use httptest::matchers::request;
190    use httptest::responders::{json_encoded, status_code};
191    use httptest::{Expectation, Server};
192    use jsonapi_core::Resource;
193    use serde_json::json;
194
195    #[tokio::test]
196    async fn can_create_record() {
197        // Setup mock server and expections for a posted Customer record
198        let server = Server::run();
199        server.expect(
200            Expectation::matching(request::method_path("POST", "/v1/customers"))
201                .respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
202        );
203
204        // Perform test
205        let customer = Resource {
206            type_: "customers".to_string(),
207            id: None,
208            lid: None,
209            attributes: json!({"name": "Acme Widgets"}),
210            relationships: Default::default(),
211            links: None,
212            meta: None,
213        };
214        let adapter = RestAdapter::new(server.url_str("/v1"));
215        let finalized_customer = adapter
216            .create_record(customer)
217            .await
218            .expect("Expected to create customer record");
219        assert_eq!(finalized_customer.id, Some("123".to_string()));
220        assert!(finalized_customer.attributes.to_string().contains("Acme Widgets"))
221    }
222
223    #[tokio::test]
224    async fn can_delete_record() {
225        // Setup mock server and expections for a delete Customer operation
226        let server = Server::run();
227        server.expect(
228            Expectation::matching(request::method_path("DELETE", "/v1/customers/123")).respond_with(status_code(200)),
229        );
230
231        // Perform test
232        let customer = Resource {
233            type_: "customers".to_string(),
234            id: Some("123".to_string()),
235            lid: None,
236            attributes: json!({}),
237            relationships: Default::default(),
238            links: None,
239            meta: None,
240        };
241        let adapter = RestAdapter::new(server.url_str("/v1"));
242        adapter
243            .delete_record(&customer)
244            .await
245            .expect("Expected to delete customer record");
246    }
247
248    #[tokio::test]
249    async fn can_find_record() {
250        // Setup mock server and expections for a delete Customer operation
251        let server = Server::run();
252        server.expect(
253            Expectation::matching(request::method_path("GET", "/v1/customers/123"))
254                .respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
255        );
256
257        // Perform test
258        let adapter = RestAdapter::new(server.url_str("/v1"));
259        let customer = adapter
260            .find_record("customers", "123")
261            .await
262            .expect("Expected to find customer record");
263        assert_eq!(customer.id, Some("123".to_string()));
264    }
265
266    #[tokio::test]
267    async fn can_query() {
268        // Setup mock server and expections for a get customers operation
269        let server = Server::run();
270        server.expect(
271            Expectation::matching(request::method_path("GET", "/v1/customers")).respond_with(json_encoded(
272                json!({"customers": [
273                    {"id": "123", "name": "Acme Widgets"},
274                    {"id": "456", "name": "Standard Paper Supplies, Inc."},
275                ]}),
276            )),
277        );
278
279        // Perform test
280        let adapter = RestAdapter::new(server.url_str("/v1"));
281        let customers = adapter
282            .query("customers", json!({}))
283            .await
284            .expect("Expected to find customer records");
285        assert_eq!(customers.len(), 2);
286        let first = customers.first().expect("Expected to find first customer");
287        assert_eq!(first.id, Some("123".to_string()));
288        let second = customers.get(1).expect("Expected to find second customer");
289        assert_eq!(second.id, Some("456".to_string()));
290    }
291
292    #[tokio::test]
293    async fn can_update_record() {
294        // Setup mock server and expections for a put-ted Customer record
295        let server = Server::run();
296        server.expect(
297            Expectation::matching(request::method_path("PUT", "/v1/customers/123")).respond_with(json_encoded(
298                json!({"customer": {"id": "123", "name": "Acme Widgets 2"}}),
299            )),
300        );
301
302        // Perform test
303        let customer = Resource {
304            type_: "customers".to_string(),
305            id: Some("123".to_string()),
306            lid: None,
307            attributes: json!({"name": "Acme Widgets"}),
308            relationships: Default::default(),
309            links: None,
310            meta: None,
311        };
312        let adapter = RestAdapter::new(server.url_str("/v1"));
313        let finalized_customer = adapter
314            .update_record(customer)
315            .await
316            .expect("Expected to update customer record");
317        assert_eq!(finalized_customer.id, Some("123".to_string()));
318        assert!(finalized_customer.attributes.to_string().contains("Acme Widgets 2"))
319    }
320}