1use serde::de::Error as _;
7use serde::{Deserialize, Deserializer, Serialize};
8use std::collections::HashMap;
9
10fn deserialize_id<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
14where
15 D: Deserializer<'de>,
16{
17 let value = serde_json::Value::deserialize(deserializer)?;
18 match value {
19 serde_json::Value::String(s) => Ok(s),
20 serde_json::Value::Number(n) => Ok(n.to_string()),
21 other => Ok(other.to_string()),
22 }
23}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct SingleDocument {
28 pub data: Resource,
29 #[serde(default)]
30 pub included: Vec<Resource>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct MultiDocument {
36 pub data: Vec<Resource>,
37 #[serde(default)]
38 pub included: Vec<Resource>,
39}
40
41#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct ErrorDocument {
44 pub errors: Vec<ApiError>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct ApiError {
50 #[serde(default)]
51 pub status: Option<String>,
52 #[serde(default)]
53 pub code: Option<String>,
54 #[serde(default)]
55 pub title: Option<String>,
56 #[serde(default)]
57 pub detail: Option<String>,
58}
59
60#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct Resource {
63 #[serde(deserialize_with = "deserialize_id")]
64 pub id: String,
65 #[serde(rename = "type")]
66 pub resource_type: String,
67 #[serde(default)]
68 pub attributes: serde_json::Value,
69 #[serde(default)]
70 pub relationships: HashMap<String, Relationship>,
71}
72
73#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct Relationship {
76 pub data: RelationshipData,
77}
78
79#[derive(Debug, Clone, Deserialize, Serialize)]
81#[serde(untagged)]
82pub enum RelationshipData {
83 Single(ResourceIdentifier),
84 Many(Vec<ResourceIdentifier>),
85 Null,
86}
87
88#[derive(Debug, Clone, Deserialize, Serialize)]
90pub struct ResourceIdentifier {
91 #[serde(deserialize_with = "deserialize_id")]
92 pub id: String,
93 #[serde(rename = "type")]
94 pub resource_type: String,
95}
96
97impl Resource {
98 pub fn related_ids(&self, key: &str) -> Vec<String> {
100 match self.relationships.get(key) {
101 Some(rel) => match &rel.data {
102 RelationshipData::Single(ident) => vec![ident.id.clone()],
103 RelationshipData::Many(idents) => idents.iter().map(|i| i.id.clone()).collect(),
104 RelationshipData::Null => vec![],
105 },
106 None => vec![],
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
114pub enum ApiResponse {
115 Single(SingleDocument),
116 Multi(MultiDocument),
117 Error(ErrorDocument),
118}
119
120impl ApiResponse {
121 pub fn parse(body: &str) -> std::result::Result<Self, serde_json::Error> {
123 if let Ok(doc) = serde_json::from_str::<SingleDocument>(body) {
125 return Ok(ApiResponse::Single(doc));
126 }
127 if let Ok(doc) = serde_json::from_str::<MultiDocument>(body) {
129 return Ok(ApiResponse::Multi(doc));
130 }
131 if let Ok(doc) = serde_json::from_str::<ErrorDocument>(body) {
133 return Ok(ApiResponse::Error(doc));
134 }
135 Err(serde_json::Error::custom(
137 "response did not match any known JSON:API format",
138 ))
139 }
140
141 pub fn resources(&self) -> Vec<&Resource> {
143 match self {
144 ApiResponse::Single(doc) => vec![&doc.data],
145 ApiResponse::Multi(doc) => doc.data.iter().collect(),
146 ApiResponse::Error(_) => vec![],
147 }
148 }
149
150 pub fn included(&self) -> &[Resource] {
152 match self {
153 ApiResponse::Single(doc) => &doc.included,
154 ApiResponse::Multi(doc) => &doc.included,
155 ApiResponse::Error(_) => &[],
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn parse_single_document() {
166 let json = r#"{
167 "data": {
168 "id": "123",
169 "type": "devices/partition",
170 "attributes": { "state": 1, "desiredState": 1 },
171 "relationships": {}
172 },
173 "included": []
174 }"#;
175 let resp = ApiResponse::parse(json).unwrap();
176 assert!(matches!(resp, ApiResponse::Single(_)));
177 let resources = resp.resources();
178 assert_eq!(resources.len(), 1);
179 assert_eq!(resources[0].id, "123");
180 }
181
182 #[test]
183 fn parse_multi_document() {
184 let json = r#"{
185 "data": [
186 { "id": "1", "type": "devices/sensor", "attributes": {}, "relationships": {} },
187 { "id": "2", "type": "devices/sensor", "attributes": {}, "relationships": {} }
188 ],
189 "included": []
190 }"#;
191 let resp = ApiResponse::parse(json).unwrap();
192 assert!(matches!(resp, ApiResponse::Multi(_)));
193 assert_eq!(resp.resources().len(), 2);
194 }
195
196 #[test]
197 fn parse_error_document() {
198 let json = r#"{
199 "errors": [
200 { "status": "401", "code": "401", "title": "Unauthorized" }
201 ]
202 }"#;
203 let resp = ApiResponse::parse(json).unwrap();
204 assert!(matches!(resp, ApiResponse::Error(_)));
205 }
206
207 #[test]
208 fn resource_related_ids() {
209 let json = r#"{
210 "id": "sys-1",
211 "type": "systems/system",
212 "attributes": {},
213 "relationships": {
214 "partitions": {
215 "data": [
216 { "id": "p-1", "type": "devices/partition" },
217 { "id": "p-2", "type": "devices/partition" }
218 ]
219 },
220 "primaryLock": {
221 "data": { "id": "l-1", "type": "devices/lock" }
222 }
223 }
224 }"#;
225 let res: Resource = serde_json::from_str(json).unwrap();
226 assert_eq!(res.related_ids("partitions"), vec!["p-1", "p-2"]);
227 assert_eq!(res.related_ids("primaryLock"), vec!["l-1"]);
228 assert!(res.related_ids("nonexistent").is_empty());
229 }
230}