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)]
18pub struct OptionalDocument<A> {
19 pub data: Option<Resource<A>>,
20}
21
22#[derive(Debug, Clone, Deserialize)]
32pub struct ListDocument<A> {
33 pub data: Vec<Resource<A>>,
34 #[serde(default)]
35 pub links: Option<Links>,
36}
37
38#[derive(Debug, Clone, Deserialize)]
46pub struct Links {
47 #[serde(default)]
50 pub next: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Resource<A> {
59 pub id: String,
60 #[serde(rename = "type")]
61 pub resource_type: String,
62 pub attributes: A,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub relationships: Option<serde_json::Value>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ResourceId {
71 #[serde(rename = "type")]
72 pub resource_type: &'static str,
73 pub id: String,
74}
75
76#[derive(Debug, Clone, Serialize)]
79pub struct ToOne {
80 pub data: ResourceId,
81}
82
83#[derive(Debug, Clone, Serialize)]
92pub struct ToMany {
93 pub data: Vec<ResourceId>,
94}
95
96#[derive(Debug, Clone, Serialize)]
97pub struct CreateBody<A, R> {
98 pub data: CreateData<A, R>,
99}
100
101#[derive(Debug, Clone, Serialize)]
102pub struct CreateData<A, R> {
103 #[serde(rename = "type")]
104 pub resource_type: &'static str,
105 pub attributes: A,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub relationships: Option<R>,
108}
109
110#[derive(Debug, Clone, Serialize)]
111pub struct UpdateBody<A> {
112 pub data: UpdateData<A>,
113}
114
115#[derive(Debug, Clone, Serialize)]
118pub struct UpdateRelationshipsBody<R> {
119 pub data: UpdateRelationshipsData<R>,
120}
121
122#[derive(Debug, Clone, Serialize)]
123pub struct UpdateRelationshipsData<R> {
124 #[serde(rename = "type")]
125 pub resource_type: &'static str,
126 pub id: String,
127 pub relationships: R,
128}
129
130#[derive(Debug, Clone, Serialize)]
131pub struct UpdateData<A> {
132 #[serde(rename = "type")]
133 pub resource_type: &'static str,
134 pub id: String,
135 pub attributes: A,
136}
137
138#[derive(Debug, Clone, Deserialize, Default)]
140pub struct ErrorDocument {
141 #[serde(default)]
142 pub errors: Vec<ApiError>,
143}
144
145#[derive(Debug, Clone, Deserialize)]
146pub struct ApiError {
147 pub status: String,
148 pub code: String,
149 pub title: String,
150 #[serde(default)]
151 pub detail: String,
152}
153
154pub fn path_and_query(url: &str) -> Option<String> {
163 let after_scheme = url.split_once("://")?.1;
164 let (_host, rest) = after_scheme.split_once('/')?;
165 Some(format!("/{rest}"))
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use serde::Deserialize;
172
173 #[derive(Debug, Deserialize)]
174 struct Attrs {
175 name: String,
176 }
177
178 #[test]
181 fn reads_a_next_link_from_a_collection_page() {
182 let doc: ListDocument<Attrs> = serde_json::from_value(serde_json::json!({
183 "data": [{ "id": "1", "type": "apps", "attributes": { "name": "Klepon" } }],
184 "links": {
185 "self": "https://api.appstoreconnect.apple.com/v1/apps?limit=200",
186 "next": "https://api.appstoreconnect.apple.com/v1/apps?limit=200&cursor=AQ%3D%3D"
187 }
188 }))
189 .unwrap();
190
191 assert_eq!(doc.data.len(), 1);
192 assert_eq!(doc.data[0].attributes.name, "Klepon");
193 assert_eq!(
194 doc.links.expect("links present").next.as_deref(),
195 Some("https://api.appstoreconnect.apple.com/v1/apps?limit=200&cursor=AQ%3D%3D")
196 );
197 }
198
199 #[test]
203 fn a_page_without_a_next_link_is_the_last_one() {
204 let with_self_only: ListDocument<Attrs> = serde_json::from_value(serde_json::json!({
205 "data": [],
206 "links": { "self": "https://api.appstoreconnect.apple.com/v1/apps?limit=200" }
207 }))
208 .unwrap();
209 assert!(with_self_only.links.expect("links present").next.is_none());
210
211 let without_links: ListDocument<Attrs> =
212 serde_json::from_value(serde_json::json!({ "data": [] })).unwrap();
213 assert!(without_links.links.is_none());
214 }
215
216 #[test]
219 fn strips_the_host_off_a_next_link() {
220 assert_eq!(
221 path_and_query("https://api.appstoreconnect.apple.com/v1/apps?cursor=AQ").as_deref(),
222 Some("/v1/apps?cursor=AQ")
223 );
224 assert_eq!(
227 path_and_query("https://api.appstoreconnect.apple.com/v1/apps?cursor=AQ%3D%3D")
228 .as_deref(),
229 Some("/v1/apps?cursor=AQ%3D%3D")
230 );
231 }
232
233 #[test]
234 fn gives_up_on_a_link_that_is_not_a_whole_url() {
235 assert_eq!(path_and_query("/v1/apps?cursor=AQ"), None);
236 assert_eq!(
237 path_and_query("https://api.appstoreconnect.apple.com"),
238 None
239 );
240 }
241}