use axum::http::{HeaderMap, HeaderValue};
use serde_json::{Value, json};
use xxhash_rust::xxh3::xxh3_64;
pub(super) fn compute_etag(body: &[u8]) -> String {
let hash = xxh3_64(body);
format!("W/\"{hash:016x}\"")
}
pub(super) fn check_if_none_match(headers: &HeaderMap, etag: &str) -> Option<bool> {
let inm = headers.get("if-none-match")?.to_str().ok()?;
if inm.trim() == "*" {
return Some(true);
}
Some(inm.split(',').any(|tag| tag.trim() == etag))
}
pub(super) fn extract_single_data(result: &Value) -> Result<Value, super::RestError> {
if let Some(data_obj) = result.get("data") {
if let Value::Object(map) = data_obj {
Ok(map.values().next().cloned().unwrap_or(Value::Null))
} else {
Ok(data_obj.clone())
}
} else {
Ok(result.clone())
}
}
pub(super) fn extract_collection_data(result: &Value) -> Result<Value, super::RestError> {
extract_single_data(result)
}
pub(super) fn extract_mutation_data(result: &Value) -> Result<Value, super::RestError> {
if let Some(data_obj) = result.get("data") {
if let Value::Object(map) = data_obj {
if let Some(mutation_result) = map.values().next() {
if let Some(entity) = mutation_result.get("entity") {
if !entity.is_null() {
return Ok(entity.clone());
}
}
return Ok(mutation_result.clone());
}
}
Ok(data_obj.clone())
} else {
Ok(result.clone())
}
}
pub(super) fn extract_delete_entity(result: &Value, mutation_name: &str) -> Option<Value> {
let entity = result.get("data")?.get(mutation_name)?.get("entity")?;
if entity.is_null() {
None
} else {
Some(entity.clone())
}
}
pub(super) fn extract_relay_page_info(data: &Value) -> Option<&Value> {
data.get("pageInfo")
}
pub(super) fn extract_id_from_data(data: &Value) -> Option<&Value> {
data.get("id")
}
pub(super) fn format_id_for_url(id: &Value) -> String {
match id {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
other => other.to_string(),
}
}
pub(super) fn build_offset_links(base: &str, limit: u64, offset: u64, total: Option<u64>) -> Value {
let mut links = serde_json::Map::new();
links.insert("self".to_string(), json!(format!("{base}?limit={limit}&offset={offset}")));
links.insert("first".to_string(), json!(format!("{base}?limit={limit}&offset=0")));
let next_offset = offset + limit;
let has_next = total.is_none_or(|t| next_offset < t);
if has_next {
links.insert(
"next".to_string(),
json!(format!("{base}?limit={limit}&offset={next_offset}")),
);
} else {
links.insert("next".to_string(), Value::Null);
}
if offset > 0 {
let prev_offset = offset.saturating_sub(limit);
links.insert(
"prev".to_string(),
json!(format!("{base}?limit={limit}&offset={prev_offset}")),
);
} else {
links.insert("prev".to_string(), Value::Null);
}
if let Some(total) = total {
if total > 0 {
let last_offset = ((total - 1) / limit) * limit;
links.insert(
"last".to_string(),
json!(format!("{base}?limit={limit}&offset={last_offset}")),
);
} else {
links.insert("last".to_string(), json!(format!("{base}?limit={limit}&offset=0")));
}
}
Value::Object(links)
}
pub(super) fn build_cursor_links(
base: &str,
first: Option<u64>,
after: Option<&str>,
data: &Value,
) -> Value {
let mut links = serde_json::Map::new();
let mut self_url = base.to_string();
if let Some(f) = first {
self_url = format!("{self_url}?first={f}");
if let Some(a) = after {
self_url = format!("{self_url}&after={a}");
}
}
links.insert("self".to_string(), json!(self_url));
let has_next = data
.get("pageInfo")
.and_then(|pi| pi.get("hasNextPage"))
.and_then(Value::as_bool)
.unwrap_or(false);
if has_next {
if let Some(end_cursor) = extract_end_cursor(data) {
let mut next_url = base.to_string();
if let Some(f) = first {
next_url = format!("{next_url}?first={f}&after={end_cursor}");
} else {
next_url = format!("{next_url}?after={end_cursor}");
}
links.insert("next".to_string(), json!(next_url));
}
}
Value::Object(links)
}
pub(super) fn extract_end_cursor(data: &Value) -> Option<&str> {
data.get("pageInfo")?.get("endCursor")?.as_str()
}
pub(super) fn header_value(s: &str) -> HeaderValue {
HeaderValue::from_str(s).expect("ETag string must be valid ASCII")
}