smbcloud_ascapi_core/
jsonapi.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Deserialize)]
10pub struct Document<A> {
11 pub data: Resource<A>,
12}
13
14#[derive(Debug, Clone, Deserialize)]
24pub struct ListDocument<A> {
25 pub data: Vec<Resource<A>>,
26 #[serde(default)]
27 pub links: Option<Links>,
28}
29
30#[derive(Debug, Clone, Deserialize)]
38pub struct Links {
39 #[serde(default)]
42 pub next: Option<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Resource<A> {
51 pub id: String,
52 #[serde(rename = "type")]
53 pub resource_type: String,
54 pub attributes: A,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub relationships: Option<serde_json::Value>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ResourceId {
63 #[serde(rename = "type")]
64 pub resource_type: &'static str,
65 pub id: String,
66}
67
68#[derive(Debug, Clone, Serialize)]
71pub struct ToOne {
72 pub data: ResourceId,
73}
74
75#[derive(Debug, Clone, Serialize)]
84pub struct ToMany {
85 pub data: Vec<ResourceId>,
86}
87
88#[derive(Debug, Clone, Serialize)]
89pub struct CreateBody<A, R> {
90 pub data: CreateData<A, R>,
91}
92
93#[derive(Debug, Clone, Serialize)]
94pub struct CreateData<A, R> {
95 #[serde(rename = "type")]
96 pub resource_type: &'static str,
97 pub attributes: A,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub relationships: Option<R>,
100}
101
102#[derive(Debug, Clone, Serialize)]
103pub struct UpdateBody<A> {
104 pub data: UpdateData<A>,
105}
106
107#[derive(Debug, Clone, Serialize)]
110pub struct UpdateRelationshipsBody<R> {
111 pub data: UpdateRelationshipsData<R>,
112}
113
114#[derive(Debug, Clone, Serialize)]
115pub struct UpdateRelationshipsData<R> {
116 #[serde(rename = "type")]
117 pub resource_type: &'static str,
118 pub id: String,
119 pub relationships: R,
120}
121
122#[derive(Debug, Clone, Serialize)]
123pub struct UpdateData<A> {
124 #[serde(rename = "type")]
125 pub resource_type: &'static str,
126 pub id: String,
127 pub attributes: A,
128}
129
130#[derive(Debug, Clone, Deserialize, Default)]
132pub struct ErrorDocument {
133 #[serde(default)]
134 pub errors: Vec<ApiError>,
135}
136
137#[derive(Debug, Clone, Deserialize)]
138pub struct ApiError {
139 pub status: String,
140 pub code: String,
141 pub title: String,
142 #[serde(default)]
143 pub detail: String,
144}
145
146pub fn path_and_query(url: &str) -> Option<String> {
155 let after_scheme = url.split_once("://")?.1;
156 let (_host, rest) = after_scheme.split_once('/')?;
157 Some(format!("/{rest}"))
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use serde::Deserialize;
164
165 #[derive(Debug, Deserialize)]
166 struct Attrs {
167 name: String,
168 }
169
170 #[test]
173 fn reads_a_next_link_from_a_collection_page() {
174 let doc: ListDocument<Attrs> = serde_json::from_value(serde_json::json!({
175 "data": [{ "id": "1", "type": "apps", "attributes": { "name": "Klepon" } }],
176 "links": {
177 "self": "https://api.appstoreconnect.apple.com/v1/apps?limit=200",
178 "next": "https://api.appstoreconnect.apple.com/v1/apps?limit=200&cursor=AQ%3D%3D"
179 }
180 }))
181 .unwrap();
182
183 assert_eq!(doc.data.len(), 1);
184 assert_eq!(doc.data[0].attributes.name, "Klepon");
185 assert_eq!(
186 doc.links.expect("links present").next.as_deref(),
187 Some("https://api.appstoreconnect.apple.com/v1/apps?limit=200&cursor=AQ%3D%3D")
188 );
189 }
190
191 #[test]
195 fn a_page_without_a_next_link_is_the_last_one() {
196 let with_self_only: ListDocument<Attrs> = serde_json::from_value(serde_json::json!({
197 "data": [],
198 "links": { "self": "https://api.appstoreconnect.apple.com/v1/apps?limit=200" }
199 }))
200 .unwrap();
201 assert!(with_self_only.links.expect("links present").next.is_none());
202
203 let without_links: ListDocument<Attrs> =
204 serde_json::from_value(serde_json::json!({ "data": [] })).unwrap();
205 assert!(without_links.links.is_none());
206 }
207
208 #[test]
211 fn strips_the_host_off_a_next_link() {
212 assert_eq!(
213 path_and_query("https://api.appstoreconnect.apple.com/v1/apps?cursor=AQ").as_deref(),
214 Some("/v1/apps?cursor=AQ")
215 );
216 assert_eq!(
219 path_and_query("https://api.appstoreconnect.apple.com/v1/apps?cursor=AQ%3D%3D")
220 .as_deref(),
221 Some("/v1/apps?cursor=AQ%3D%3D")
222 );
223 }
224
225 #[test]
226 fn gives_up_on_a_link_that_is_not_a_whole_url() {
227 assert_eq!(path_and_query("/v1/apps?cursor=AQ"), None);
228 assert_eq!(
229 path_and_query("https://api.appstoreconnect.apple.com"),
230 None
231 );
232 }
233}