use http::StatusCode;
use jsonapi_core::{ApiError, ErrorSource};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClientIdPolicy {
#[default]
Assign,
Accept,
Forbid,
}
fn pointer_error(status: StatusCode, detail: String, pointer: &str) -> ApiError {
ApiError {
status: Some(status.as_u16().to_string()),
title: status.canonical_reason().map(str::to_string),
detail: Some(detail),
source: Some(ErrorSource {
pointer: Some(pointer.to_string()),
..Default::default()
}),
..Default::default()
}
}
pub fn check_id_matches(body_id: Option<&str>, path_id: &str) -> Result<(), Box<ApiError>> {
match body_id {
Some(id) if id != path_id => Err(Box::new(pointer_error(
StatusCode::CONFLICT,
format!("resource `id` \"{id}\" does not match the endpoint id \"{path_id}\""),
"/data/id",
))),
_ => Ok(()),
}
}
pub fn check_client_id(policy: ClientIdPolicy, body_id: Option<&str>) -> Result<(), Box<ApiError>> {
match (policy, body_id) {
(ClientIdPolicy::Forbid, Some(id)) => Err(Box::new(pointer_error(
StatusCode::FORBIDDEN,
format!("client-generated ids are not supported; remove `id` \"{id}\""),
"/data/id",
))),
_ => Ok(()),
}
}
#[must_use]
pub fn id_conflict(detail: impl Into<String>) -> ApiError {
pointer_error(StatusCode::CONFLICT, detail.into(), "/data/id")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matching_id_is_ok() {
assert!(check_id_matches(Some("1"), "1").is_ok());
}
#[test]
fn absent_body_id_is_ok() {
assert!(check_id_matches(None, "1").is_ok());
}
#[test]
fn mismatched_id_is_409_with_pointer() {
let err = check_id_matches(Some("2"), "1").unwrap_err();
assert_eq!(err.status.as_deref(), Some("409"));
assert_eq!(
err.source.as_ref().and_then(|s| s.pointer.as_deref()),
Some("/data/id")
);
assert!(err.detail.as_deref().unwrap().contains("\"2\""));
}
#[test]
fn forbid_policy_rejects_client_id_with_403() {
let err = check_client_id(ClientIdPolicy::Forbid, Some("client-1")).unwrap_err();
assert_eq!(err.status.as_deref(), Some("403"));
assert_eq!(
err.source.as_ref().and_then(|s| s.pointer.as_deref()),
Some("/data/id")
);
}
#[test]
fn forbid_policy_allows_absent_client_id() {
assert!(check_client_id(ClientIdPolicy::Forbid, None).is_ok());
}
#[test]
fn assign_and_accept_always_ok() {
assert!(check_client_id(ClientIdPolicy::Assign, Some("x")).is_ok());
assert!(check_client_id(ClientIdPolicy::Accept, Some("x")).is_ok());
}
#[test]
fn id_conflict_is_409_with_pointer() {
let err = id_conflict("id `1` already exists");
assert_eq!(err.status.as_deref(), Some("409"));
assert_eq!(
err.source.as_ref().and_then(|s| s.pointer.as_deref()),
Some("/data/id")
);
}
}