#[derive(Debug, Clone, Copy)]
pub struct Endpoint {
pub method: &'static str,
pub path: &'static str,
}
impl Endpoint {
pub const fn new(method: &'static str, path: &'static str) -> Self {
Self { method, path }
}
pub fn render(&self, params: &[(&str, &str)]) -> String {
let mut out = self.path.to_string();
for (name, value) in params {
let placeholder = format!("{{{name}}}");
assert!(
out.contains(&placeholder),
"Endpoint {} has no placeholder named {{{}}}; check `endpoints.rs`",
self.path, name
);
out = out.replace(&placeholder, &urlencoding::encode(value));
}
assert!(
!out.contains('{'),
"Endpoint {} still has unfilled placeholders after substitution: {}",
self.path, out
);
out
}
pub fn url(&self, api_base: &str, params: &[(&str, &str)]) -> String {
format!("{api_base}{}", self.render(params))
}
}
pub const LOOKUP_DOI: Endpoint = Endpoint::new("POST", "/api/v1/lookup/doi");
pub const LOOKUP_ISBN: Endpoint = Endpoint::new("POST", "/api/v1/lookup/isbn");
pub const REVERSE: Endpoint = Endpoint::new("POST", "/api/v1/reverse");
pub const RESOLVE: Endpoint = Endpoint::new("POST", "/api/v1/resolve");
pub const PARSE_CITATIONS: Endpoint = Endpoint::new("POST", "/api/v1/parse-citations");
pub const RESOLVE_DEBUG: Endpoint = Endpoint::new("POST", "/api/v1/resolve/debug");
pub const HEALTH: Endpoint = Endpoint::new("GET", "/api/health");
pub const ME: Endpoint = Endpoint::new("GET", "/api/v1/me");
pub const FORMAT: Endpoint = Endpoint::new("POST", "/api/v1/format");
pub const FORMAT_GROUP_CITE: Endpoint = Endpoint::new("POST", "/api/v1/format/group-cite");
pub const STYLES_SEARCH: Endpoint = Endpoint::new("GET", "/api/v1/styles/search");
pub const COLLECTIONS_LIST: Endpoint = Endpoint::new("GET", "/api/v1/collections");
pub const COLLECTIONS_CREATE: Endpoint = Endpoint::new("POST", "/api/v1/collections");
pub const COLLECTION_GET: Endpoint = Endpoint::new("GET", "/api/v1/collections/{id}");
pub const COLLECTION_UPDATE: Endpoint = Endpoint::new("PATCH", "/api/v1/collections/{id}");
pub const COLLECTION_DELETE: Endpoint = Endpoint::new("DELETE", "/api/v1/collections/{id}");
pub const COLLECTION_ENTRIES_ADD: Endpoint = Endpoint::new("POST", "/api/v1/collections/{id}/entries");
pub const COLLECTION_ENTRIES_BATCH: Endpoint = Endpoint::new("POST", "/api/v1/collections/{id}/entries/batch");
pub const COLLECTION_ENTRY_REMOVE: Endpoint = Endpoint::new("DELETE", "/api/v1/collections/{id}/entries/{eid}");
pub const COLLECTION_EXPORT_BIB: Endpoint = Endpoint::new("GET", "/api/v1/collections/{id}/export.bib");
pub const COLLECTION_IMPORT: Endpoint = Endpoint::new("POST", "/api/v1/collections/{id}/import");
pub const COLLECTION_TAGS: Endpoint = Endpoint::new("PATCH", "/api/v1/collections/{id}/tags");
pub const COLLECTION_CHECK_DUPLICATES: Endpoint = Endpoint::new("POST", "/api/v1/collections/{id}/check-duplicates");
pub const COLLECTION_REORDER: Endpoint = Endpoint::new("PATCH", "/api/v1/collections/{id}/reorder");
pub const COLLECTION_SHARE: Endpoint = Endpoint::new("POST", "/api/v1/collections/{id}/share");
pub const COLLECTION_UNSHARE: Endpoint = Endpoint::new("DELETE", "/api/v1/collections/{id}/share");
pub const COLLECTIONS_MERGE: Endpoint = Endpoint::new("POST", "/api/v1/collections/merge");
pub const COLLECTIONS_BATCH_MOVE: Endpoint = Endpoint::new("POST", "/api/v1/collections/batch-move");
pub const SHARED_GET: Endpoint = Endpoint::new("GET", "/api/v1/shared/{token}");
pub const CITATION_KEYS: Endpoint = Endpoint::new("POST", "/api/v1/citation-keys");
pub const JOURNAL_EXPAND: Endpoint = Endpoint::new("POST", "/api/v1/journal/expand");
pub const ALL: &[Endpoint] = &[
LOOKUP_DOI, LOOKUP_ISBN, REVERSE, RESOLVE, PARSE_CITATIONS, RESOLVE_DEBUG, HEALTH, ME,
FORMAT, FORMAT_GROUP_CITE, STYLES_SEARCH,
COLLECTIONS_LIST, COLLECTIONS_CREATE,
COLLECTION_GET, COLLECTION_UPDATE, COLLECTION_DELETE,
COLLECTION_ENTRIES_ADD, COLLECTION_ENTRIES_BATCH, COLLECTION_ENTRY_REMOVE,
COLLECTION_EXPORT_BIB, COLLECTION_IMPORT, COLLECTION_TAGS,
COLLECTION_CHECK_DUPLICATES, COLLECTION_REORDER,
COLLECTION_SHARE, COLLECTION_UNSHARE,
COLLECTIONS_MERGE, COLLECTIONS_BATCH_MOVE, SHARED_GET,
CITATION_KEYS, JOURNAL_EXPAND,
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_substitutes_placeholders() {
assert_eq!(
COLLECTION_GET.render(&[("id", "abc-123")]),
"/api/v1/collections/abc-123"
);
}
#[test]
fn render_url_encodes_values() {
assert_eq!(
SHARED_GET.render(&[("token", "a b/c")]),
"/api/v1/shared/a%20b%2Fc"
);
}
#[test]
fn render_handles_two_placeholders() {
assert_eq!(
COLLECTION_ENTRY_REMOVE.render(&[("id", "col1"), ("eid", "ent2")]),
"/api/v1/collections/col1/entries/ent2"
);
}
#[test]
#[should_panic(expected = "no placeholder named")]
fn render_panics_on_unknown_placeholder() {
COLLECTION_GET.render(&[("nope", "x")]);
}
#[test]
#[should_panic(expected = "still has unfilled placeholders")]
fn render_panics_on_missing_substitution() {
COLLECTION_GET.render(&[]);
}
#[test]
fn all_endpoints_have_unique_method_path_pairs() {
let mut seen = std::collections::HashSet::new();
for ep in ALL {
assert!(
seen.insert((ep.method, ep.path)),
"duplicate endpoint in ALL: {} {}",
ep.method, ep.path
);
}
}
}