Skip to main content

auric_runtime/data/adapters/
rest.rs

1use super::super::Adapter;
2use crate::data::QueryRecordOptions;
3use anyhow::{Result, anyhow};
4use async_trait::async_trait;
5use jsonapi_core::Resource;
6use pluralizer::pluralize;
7use reqwest::Client;
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[derive(Default)]
12pub struct RestAdapter {
13    api_url: String,
14}
15
16impl RestAdapter {
17    pub fn new(api_url: String) -> Self {
18        Self { api_url }
19    }
20}
21
22#[async_trait(?Send)]
23impl Adapter for RestAdapter {
24    async fn create_record(&self, resource: Resource) -> Result<Resource> {
25        // Perform request
26        let url = format!("{}/{}", self.api_url, resource.type_);
27        let client = Client::new();
28        let res = client.post(url).json(&resource.attributes).send().await?;
29
30        // Read response
31        let res_body: HashMap<String, Value> = res.json().await?;
32        let model_name = pluralize(&resource.type_, 1, false);
33        let data = match res_body.get(&model_name) {
34            None => return Err(anyhow!("resource not found")),
35            Some(data) => data,
36        };
37        let id = match data.as_object() {
38            None => return Err(anyhow!("expected an object")),
39            Some(map) => match map.get("id") {
40                None => return Err(anyhow!("expected returned object to include an id")),
41                Some(id) => id.as_str().unwrap_or_default().to_string(),
42            },
43        };
44        let finalized_resource = Resource {
45            type_: resource.type_,
46            id: Some(id),
47            lid: None,
48            attributes: data.clone(),
49            relationships: Default::default(),
50            links: None,
51            meta: None,
52        };
53        Ok(finalized_resource)
54    }
55
56    async fn delete_record(&self, resource: &Resource) -> Result<()> {
57        // Perform request
58        let url = format!(
59            "{}/{}/{}",
60            self.api_url,
61            resource.type_,
62            resource.id.clone().unwrap_or_default()
63        );
64        let client = Client::new();
65        let _res = client.delete(url).send().await?;
66        Ok(())
67    }
68
69    async fn find_record(&self, resource_type: &str, id: &str) -> Result<Resource> {
70        let resource_type = resource_type.to_string();
71        let id = id.to_string();
72
73        // Perform request
74        let url = format!("{}/{resource_type}/{id}", self.api_url);
75        let client = Client::new();
76        let res = client.get(url).send().await?;
77
78        // Read response
79        let res_body: HashMap<String, Value> = res.json().await?;
80        let model_name = pluralize(&resource_type, 1, false);
81        let data = match res_body.get(&model_name) {
82            None => return Err(anyhow!("resource not found")),
83            Some(data) => data,
84        };
85        let resource = Resource {
86            type_: resource_type,
87            id: Some(id),
88            lid: None,
89            attributes: data.clone(),
90            relationships: Default::default(),
91            links: None,
92            meta: None,
93        };
94        Ok(resource)
95    }
96
97    async fn query(&self, _resource_type: &str, _query: Value) -> Result<Vec<Resource>> {
98        todo!()
99    }
100
101    async fn query_record(
102        &self,
103        _resource_type: &str,
104        _query: Value,
105        _options: QueryRecordOptions,
106    ) -> Result<Resource> {
107        todo!()
108    }
109
110    async fn update_record(&self, resource: Resource) -> Result<Resource> {
111        // Perform request
112        let id = resource.id.clone().unwrap_or_default();
113        let url = format!("{}/{}/{}", self.api_url, resource.type_, id);
114        let client = Client::new();
115        let res = client.put(url).send().await?;
116
117        // Read response
118        let res_body: HashMap<String, Value> = res.json().await?;
119        let model_name = pluralize(&resource.type_, 1, false);
120        let data = match res_body.get(&model_name) {
121            None => return Err(anyhow!("resource not found")),
122            Some(data) => data,
123        };
124        let finalized_resource = Resource {
125            type_: resource.type_,
126            id: Some(resource.id.unwrap_or_default().to_string()),
127            lid: None,
128            attributes: data.clone(),
129            relationships: Default::default(),
130            links: None,
131            meta: None,
132        };
133        Ok(finalized_resource)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::RestAdapter;
140    use crate::data::Adapter;
141    use httptest::matchers::request;
142    use httptest::responders::{json_encoded, status_code};
143    use httptest::{Expectation, Server};
144    use jsonapi_core::Resource;
145    use serde_json::json;
146
147    #[tokio::test]
148    async fn can_create_record() {
149        // Setup mock server and expections for a posted Customer record
150        let server = Server::run();
151        server.expect(
152            Expectation::matching(request::method_path("POST", "/v1/customers"))
153                .respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
154        );
155
156        // Perform test
157        let customer = Resource {
158            type_: "customers".to_string(),
159            id: None,
160            lid: None,
161            attributes: json!({"name": "Acme Widgets"}),
162            relationships: Default::default(),
163            links: None,
164            meta: None,
165        };
166        let adapter = RestAdapter::new(server.url_str("/v1"));
167        let finalized_customer = adapter.create_record(customer).await.unwrap();
168        assert_eq!(finalized_customer.id, Some("123".to_string()));
169        assert!(finalized_customer.attributes.to_string().contains("Acme Widgets"))
170    }
171
172    #[tokio::test]
173    async fn can_delete_record() {
174        // Setup mock server and expections for a delete Customer operation
175        let server = Server::run();
176        server.expect(
177            Expectation::matching(request::method_path("DELETE", "/v1/customers/123")).respond_with(status_code(200)),
178        );
179
180        // Perform test
181        let customer = Resource {
182            type_: "customers".to_string(),
183            id: Some("123".to_string()),
184            lid: None,
185            attributes: json!({}),
186            relationships: Default::default(),
187            links: None,
188            meta: None,
189        };
190        let adapter = RestAdapter::new(server.url_str("/v1"));
191        adapter.delete_record(&customer).await.unwrap();
192    }
193
194    #[tokio::test]
195    async fn can_find_record() {
196        // Setup mock server and expections for a delete Customer operation
197        let server = Server::run();
198        server.expect(
199            Expectation::matching(request::method_path("GET", "/v1/customers/123"))
200                .respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
201        );
202
203        // Perform test
204        let adapter = RestAdapter::new(server.url_str("/v1"));
205        let customer = adapter.find_record("customers", "123").await.unwrap();
206        assert_eq!(customer.id, Some("123".to_string()));
207    }
208
209    #[tokio::test]
210    async fn can_update_record() {
211        // Setup mock server and expections for a put-ted Customer record
212        let server = Server::run();
213        server.expect(
214            Expectation::matching(request::method_path("PUT", "/v1/customers/123")).respond_with(json_encoded(
215                json!({"customer": {"id": "123", "name": "Acme Widgets 2"}}),
216            )),
217        );
218
219        // Perform test
220        let customer = Resource {
221            type_: "customers".to_string(),
222            id: Some("123".to_string()),
223            lid: None,
224            attributes: json!({"name": "Acme Widgets"}),
225            relationships: Default::default(),
226            links: None,
227            meta: None,
228        };
229        let adapter = RestAdapter::new(server.url_str("/v1"));
230        let finalized_customer = adapter.update_record(customer).await.unwrap();
231        assert_eq!(finalized_customer.id, Some("123".to_string()));
232        assert!(finalized_customer.attributes.to_string().contains("Acme Widgets 2"))
233    }
234}