zotero-api-rs 1.0.0

A secure, typed Rust client for the Zotero Web API v3
Documentation
//! HTTP header parsing helpers.

use reqwest::header::HeaderMap;

use crate::responses::pagination_links::PaginationLinks;
use crate::responses::response_metadata::ResponseMetadata;

pub(crate) fn parse_response_metadata(headers: &HeaderMap) -> ResponseMetadata {
    ResponseMetadata {
        last_modified_version: parse_u64_header(headers, "Last-Modified-Version"),
        library_version: parse_u64_header(headers, "Zotero-Library-Version"),
        backoff_seconds: parse_u64_header(headers, "Backoff"),
        retry_after_seconds: parse_u64_header(headers, "Retry-After"),
        links: parse_link_header(headers),
    }
}

fn parse_u64_header(headers: &HeaderMap, name: &str) -> Option<u64> {
    headers
        .get(name)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.parse::<u64>().ok())
}

fn parse_link_header(headers: &HeaderMap) -> PaginationLinks {
    let mut links = PaginationLinks::default();

    let Some(value) = headers.get("Link").and_then(|header| header.to_str().ok()) else {
        return links;
    };

    for segment in value.split(',') {
        let mut parts = segment.split(';').map(|part| part.trim());
        let Some(url_part) = parts.next() else {
            continue;
        };

        let url = url_part
            .strip_prefix('<')
            .and_then(|part| part.strip_suffix('>'));

        let relation = parts
            .find(|part| part.starts_with("rel="))
            .map(|part| part.trim_start_matches("rel=").trim_matches('"'));

        let (Some(url), Some(rel)) = (url, relation) else {
            continue;
        };

        match rel {
            "first" => links.first = Some(url.to_owned()),
            "prev" => links.prev = Some(url.to_owned()),
            "next" => links.next = Some(url.to_owned()),
            "last" => links.last = Some(url.to_owned()),
            "alternate" => links.alternate = Some(url.to_owned()),
            _ => {}
        }
    }

    links
}

#[cfg(test)]
mod tests {
    use reqwest::header::{HeaderMap, HeaderValue};

    use super::parse_response_metadata;

    #[test]
    fn parses_metadata_and_link_relations() {
        let mut headers = HeaderMap::new();
        headers.insert("Last-Modified-Version", HeaderValue::from_static("14"));
        headers.insert("Zotero-Library-Version", HeaderValue::from_static("15"));
        headers.insert("Backoff", HeaderValue::from_static("30"));
        headers.insert("Retry-After", HeaderValue::from_static("12"));
        headers.insert(
            "Link",
            HeaderValue::from_static(
                "<https://api.zotero.org/users/1/items?start=0>; rel=\"first\", <https://api.zotero.org/users/1/items?start=25>; rel=\"next\"",
            ),
        );

        let metadata = parse_response_metadata(&headers);
        assert_eq!(metadata.last_modified_version, Some(14));
        assert_eq!(metadata.library_version, Some(15));
        assert_eq!(metadata.backoff_seconds, Some(30));
        assert_eq!(metadata.retry_after_seconds, Some(12));
        assert_eq!(
            metadata.links.first.as_deref(),
            Some("https://api.zotero.org/users/1/items?start=0")
        );
        assert_eq!(
            metadata.links.next.as_deref(),
            Some("https://api.zotero.org/users/1/items?start=25")
        );
    }
}