simkl 0.1.0

Library to build queries for SIMKL and decoding JSON responses using Serde
Documentation
use serde::de::DeserializeOwned;
use serde_json::Value;

use crate::pagination::PaginationInfo;

pub struct SimklResponse {
    pub status_code: u16,
    pub headers: std::collections::HashMap<String, String>,
    pub body: String,
}

impl SimklResponse {
    pub fn new(
        status_code: u16,
        headers: std::collections::HashMap<String, String>,
        body: String,
    ) -> Self {
        Self {
            status_code,
            headers,
            body,
        }
    }

    pub fn json<T: DeserializeOwned>(&self) -> crate::error::Result<T> {
        Ok(serde_json::from_str(&self.body)?)
    }

    pub fn json_value(&self) -> crate::error::Result<Value> {
        Ok(serde_json::from_str(&self.body)?)
    }

    pub fn is_success(&self) -> bool {
        (200..300).contains(&self.status_code)
    }

    pub fn is_client_error(&self) -> bool {
        (400..500).contains(&self.status_code)
    }

    pub fn is_server_error(&self) -> bool {
        (500..600).contains(&self.status_code)
    }

    /// Parse pagination metadata from the `X-Pagination-*` response headers.
    ///
    /// The Simkl API returns pagination as HTTP headers, **not** in the body:
    ///
    /// | Header                   | Meaning              |
    /// |--------------------------|----------------------|
    /// | `X-Pagination-Page`      | Current page (1-based)|
    /// | `X-Pagination-Limit`     | Items per page       |
    /// | `X-Pagination-Page-Count`| Total pages          |
    /// | `X-Pagination-Item-Count`| Total items          |
    ///
    /// Returns `None` when none of the headers are present (non-paginated endpoint).
    pub fn pagination_info(&self) -> Option<PaginationInfo> {
        let get = |key: &str| -> Option<u32> { self.headers.get(key).and_then(|v| v.parse().ok()) };

        let current_page = get("X-Pagination-Page")?;
        let items_per_page = get("X-Pagination-Limit").unwrap_or(10);
        let total_pages = get("X-Pagination-Page-Count").unwrap_or(1);
        let total_items = get("X-Pagination-Item-Count").unwrap_or(0);

        Some(PaginationInfo {
            current_page,
            total_pages,
            total_items,
            items_per_page,
            has_next: current_page < total_pages,
            has_prev: current_page > 1,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    fn make_response(headers: Vec<(&str, &str)>) -> SimklResponse {
        let map: HashMap<String, String> = headers
            .into_iter()
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect();
        SimklResponse::new(200, map, "[]".to_string())
    }

    #[test]
    fn test_pagination_info_present() {
        let resp = make_response(vec![
            ("X-Pagination-Page", "2"),
            ("X-Pagination-Limit", "10"),
            ("X-Pagination-Page-Count", "5"),
            ("X-Pagination-Item-Count", "47"),
        ]);
        let info = resp.pagination_info().expect("should have pagination");
        assert_eq!(info.current_page, 2);
        assert_eq!(info.items_per_page, 10);
        assert_eq!(info.total_pages, 5);
        assert_eq!(info.total_items, 47);
        assert!(info.has_next);
        assert!(info.has_prev);
    }

    #[test]
    fn test_pagination_info_first_page() {
        let resp = make_response(vec![
            ("X-Pagination-Page", "1"),
            ("X-Pagination-Limit", "10"),
            ("X-Pagination-Page-Count", "3"),
            ("X-Pagination-Item-Count", "25"),
        ]);
        let info = resp.pagination_info().unwrap();
        assert!(!info.has_prev);
        assert!(info.has_next);
    }

    #[test]
    fn test_pagination_info_last_page() {
        let resp = make_response(vec![
            ("X-Pagination-Page", "3"),
            ("X-Pagination-Limit", "10"),
            ("X-Pagination-Page-Count", "3"),
            ("X-Pagination-Item-Count", "25"),
        ]);
        let info = resp.pagination_info().unwrap();
        assert!(info.has_prev);
        assert!(!info.has_next);
    }

    #[test]
    fn test_pagination_info_absent() {
        let resp = make_response(vec![]);
        assert!(resp.pagination_info().is_none());
    }
}