open-library-api-rs 0.1.0

Async Rust client for the Open Library API
Documentation
// v0.0.1
use crate::client::OpenLibraryClient;
use crate::error::{Error, Result};
use crate::models::search::{
    AuthorDoc, AuthorSearchParams, BookDoc, InsideDoc, ListDoc, SearchParams, SearchResponse,
    SubjectDoc,
};
use crate::validation::{validate_limit, validate_search_query};

impl OpenLibraryClient {
    /// Search for books and works across the Open Library catalogue.
    ///
    /// At least one of `q`, `title`, `author`, `isbn`, or `subject` must be set.
    /// All other [`SearchParams`] fields are optional filters or pagination controls.
    ///
    /// Returns a [`SearchResponse<BookDoc>`] where `num_found` is the total match
    /// count and `docs` is the current page.
    pub async fn search(&self, params: SearchParams) -> Result<SearchResponse<BookDoc>> {
        // At least one of q, title, author, isbn, or subject must be provided.
        let q = params.q.as_deref().unwrap_or_default();
        let title = params.title.as_deref().unwrap_or_default();
        let author = params.author.as_deref().unwrap_or_default();
        let isbn = params.isbn.as_deref().unwrap_or_default();
        let subject = params.subject.as_deref().unwrap_or_default();

        if q.is_empty() && title.is_empty() && author.is_empty() && isbn.is_empty() && subject.is_empty() {
            return Err(Error::InvalidInput(
                "at least one of q, title, author, isbn, or subject must be provided".into(),
            ));
        }
        if !q.is_empty() {
            validate_search_query(q)?;
        }
        if let Some(ref limit) = params.limit {
            validate_limit(*limit)?;
        }

        let mut url = self.base_url.join("search.json")?;
        {
            let mut qp = url.query_pairs_mut();
            if !q.is_empty() { qp.append_pair("q", q); }
            if !title.is_empty() { qp.append_pair("title", title); }
            if !author.is_empty() { qp.append_pair("author", author); }
            if !isbn.is_empty() { qp.append_pair("isbn", isbn); }
            if !subject.is_empty() { qp.append_pair("subject", subject); }
            if let Some(v) = params.place.as_deref() { qp.append_pair("place", v); }
            if let Some(v) = params.person.as_deref() { qp.append_pair("person", v); }
            if let Some(v) = params.language.as_deref() { qp.append_pair("language", v); }
            if let Some(v) = params.publisher.as_deref() { qp.append_pair("publisher", v); }
            if let Some(v) = params.page { qp.append_pair("page", &v.to_string()); }
            if let Some(v) = params.limit { qp.append_pair("limit", &v.to_string()); }
            if let Some(v) = params.offset { qp.append_pair("offset", &v.to_string()); }
            if let Some(v) = params.sort.as_deref() { qp.append_pair("sort", v); }
            if let Some(v) = params.fields.as_deref() { qp.append_pair("fields", v); }
            if let Some(v) = params.lang.as_deref() { qp.append_pair("lang", v); }
        }
        self.get_json(url).await
    }

    /// Search the Open Library author index.
    ///
    /// `params.q` is required and must be non-empty.
    pub async fn search_authors(
        &self,
        params: AuthorSearchParams,
    ) -> Result<SearchResponse<AuthorDoc>> {
        let q = params.q.as_deref().unwrap_or_default();
        if q.is_empty() {
            return Err(Error::InvalidInput("author search query must not be empty".into()));
        }
        validate_search_query(q)?;
        if let Some(ref limit) = params.limit {
            validate_limit(*limit)?;
        }

        let mut url = self.base_url.join("search/authors.json")?;
        {
            let mut qp = url.query_pairs_mut();
            qp.append_pair("q", q);
            if let Some(v) = params.limit { qp.append_pair("limit", &v.to_string()); }
            if let Some(v) = params.offset { qp.append_pair("offset", &v.to_string()); }
        }
        self.get_json(url).await
    }

    /// Search the Open Library subject index.
    ///
    /// `query` is a free-text search term (non-empty, ≤ 1000 chars).
    pub async fn search_subjects(&self, query: &str) -> Result<SearchResponse<SubjectDoc>> {
        validate_search_query(query)?;
        let mut url = self.base_url.join("search/subjects.json")?;
        url.query_pairs_mut().append_pair("q", query);
        self.get_json(url).await
    }

    /// Search public user-created reading lists.
    ///
    /// `query` is a free-text search term. `limit` is clamped to 1–1000.
    pub async fn search_lists(&self, query: &str, limit: Option<u32>) -> Result<SearchResponse<ListDoc>> {
        validate_search_query(query)?;
        if let Some(l) = limit { validate_limit(l)?; }
        let mut url = self.base_url.join("search/lists.json")?;
        {
            let mut qp = url.query_pairs_mut();
            qp.append_pair("q", query);
            if let Some(l) = limit { qp.append_pair("limit", &l.to_string()); }
        }
        self.get_json(url).await
    }

    /// Full-text search inside the text of scanned books (Internet Archive).
    ///
    /// `query` is a phrase or keyword. `limit` is clamped to 1–1000.
    pub async fn search_inside(
        &self,
        query: &str,
        limit: Option<u32>,
    ) -> Result<SearchResponse<InsideDoc>> {
        validate_search_query(query)?;
        if let Some(l) = limit { validate_limit(l)?; }
        let mut url = self.base_url.join("search/inside.json")?;
        {
            let mut qp = url.query_pairs_mut();
            qp.append_pair("q", query);
            if let Some(l) = limit { qp.append_pair("limit", &l.to_string()); }
        }
        self.get_json(url).await
    }
}