auric_runtime/data/adapters/
rest.rs1use 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 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 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 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 let url = format!("{}/{resource_type}/{id}", self.api_url);
75 let client = Client::new();
76 let res = client.get(url).send().await?;
77
78 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 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 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 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 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 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 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 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 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 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 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}