auric_runtime/data/adapters/
rest.rs1use 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 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 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 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 let url = format!("{}/{resource_type}/{id}", self.api_url);
89 let client = Client::new();
90 let res = client.get(url).send().await?;
91
92 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 let url = format!("{}/{resource_type}", self.api_url);
114 let client = Client::new();
115 let res = client.get(url).send().await?;
116
117 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 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 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 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 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 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 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 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 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 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 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 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 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}