use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize)]
pub struct Document<A> {
pub data: Resource<A>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OptionalDocument<A> {
pub data: Option<Resource<A>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ListDocument<A> {
pub data: Vec<Resource<A>>,
#[serde(default)]
pub links: Option<Links>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Links {
#[serde(default)]
pub next: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource<A> {
pub id: String,
#[serde(rename = "type")]
pub resource_type: String,
pub attributes: A,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relationships: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceId {
#[serde(rename = "type")]
pub resource_type: &'static str,
pub id: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToOne {
pub data: ResourceId,
}
#[derive(Debug, Clone, Serialize)]
pub struct ToMany {
pub data: Vec<ResourceId>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateBody<A, R> {
pub data: CreateData<A, R>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateData<A, R> {
#[serde(rename = "type")]
pub resource_type: &'static str,
pub attributes: A,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationships: Option<R>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateBody<A> {
pub data: UpdateData<A>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateRelationshipsBody<R> {
pub data: UpdateRelationshipsData<R>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateRelationshipsData<R> {
#[serde(rename = "type")]
pub resource_type: &'static str,
pub id: String,
pub relationships: R,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateData<A> {
#[serde(rename = "type")]
pub resource_type: &'static str,
pub id: String,
pub attributes: A,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ErrorDocument {
#[serde(default)]
pub errors: Vec<ApiError>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ApiError {
pub status: String,
pub code: String,
pub title: String,
#[serde(default)]
pub detail: String,
}
pub fn path_and_query(url: &str) -> Option<String> {
let after_scheme = url.split_once("://")?.1;
let (_host, rest) = after_scheme.split_once('/')?;
Some(format!("/{rest}"))
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Attrs {
name: String,
}
#[test]
fn reads_a_next_link_from_a_collection_page() {
let doc: ListDocument<Attrs> = serde_json::from_value(serde_json::json!({
"data": [{ "id": "1", "type": "apps", "attributes": { "name": "Klepon" } }],
"links": {
"self": "https://api.appstoreconnect.apple.com/v1/apps?limit=200",
"next": "https://api.appstoreconnect.apple.com/v1/apps?limit=200&cursor=AQ%3D%3D"
}
}))
.unwrap();
assert_eq!(doc.data.len(), 1);
assert_eq!(doc.data[0].attributes.name, "Klepon");
assert_eq!(
doc.links.expect("links present").next.as_deref(),
Some("https://api.appstoreconnect.apple.com/v1/apps?limit=200&cursor=AQ%3D%3D")
);
}
#[test]
fn a_page_without_a_next_link_is_the_last_one() {
let with_self_only: ListDocument<Attrs> = serde_json::from_value(serde_json::json!({
"data": [],
"links": { "self": "https://api.appstoreconnect.apple.com/v1/apps?limit=200" }
}))
.unwrap();
assert!(with_self_only.links.expect("links present").next.is_none());
let without_links: ListDocument<Attrs> =
serde_json::from_value(serde_json::json!({ "data": [] })).unwrap();
assert!(without_links.links.is_none());
}
#[test]
fn strips_the_host_off_a_next_link() {
assert_eq!(
path_and_query("https://api.appstoreconnect.apple.com/v1/apps?cursor=AQ").as_deref(),
Some("/v1/apps?cursor=AQ")
);
assert_eq!(
path_and_query("https://api.appstoreconnect.apple.com/v1/apps?cursor=AQ%3D%3D")
.as_deref(),
Some("/v1/apps?cursor=AQ%3D%3D")
);
}
#[test]
fn gives_up_on_a_link_that_is_not_a_whole_url() {
assert_eq!(path_and_query("/v1/apps?cursor=AQ"), None);
assert_eq!(
path_and_query("https://api.appstoreconnect.apple.com"),
None
);
}
}