You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
5.4 KiB
Rust
175 lines
5.4 KiB
Rust
//! Typed API client for the quotesdb backend.
|
|
//!
|
|
//! Provides async functions that wrap all quotesdb API endpoints using
|
|
//! `gloo::net::http::Request`. All functions return `Result<T, ApiError>`.
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```ignore
|
|
//! let quote = api::get_random_quote().await?;
|
|
//! ```
|
|
|
|
use quotesdb::{CreateQuoteInput, Quote, UpdateQuoteInput};
|
|
use serde::Deserialize;
|
|
|
|
/// Response type for `GET /api/quotes` (paginated list).
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct ListResponse {
|
|
/// The quotes on the current page.
|
|
pub quotes: Vec<Quote>,
|
|
/// Total number of pages available.
|
|
pub total_pages: u32,
|
|
}
|
|
|
|
/// Response type for `PUT /api/quotes` (create quote).
|
|
///
|
|
/// Includes the newly created quote and its auth code (only returned once).
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct CreateResponse {
|
|
/// The created quote record.
|
|
pub quote: Quote,
|
|
/// The auth code required for future edits/deletes. Store this securely.
|
|
pub auth_code: String,
|
|
}
|
|
|
|
/// Errors that can occur during API calls.
|
|
#[derive(Debug, thiserror::Error, Clone)]
|
|
pub enum ApiError {
|
|
/// A network-level error (e.g., connection refused, timeout).
|
|
#[error("network error: {0}")]
|
|
Network(String),
|
|
/// The server responded with an error status code.
|
|
#[error("server error {status}: {message}")]
|
|
Server { status: u16, message: String },
|
|
/// Failed to parse the response body as JSON.
|
|
#[error("parse error: {0}")]
|
|
Parse(String),
|
|
}
|
|
|
|
/// Fetch a paginated list of quotes.
|
|
///
|
|
/// # Arguments
|
|
/// - `page` — 1-indexed page number.
|
|
/// - `author` — Optional author name filter (URL-encoded automatically).
|
|
/// - `tag` — Optional tag filter (URL-encoded automatically).
|
|
pub async fn list_quotes(
|
|
page: u32,
|
|
author: Option<&str>,
|
|
tag: Option<&str>,
|
|
) -> Result<ListResponse, ApiError> {
|
|
let mut url = format!("/api/quotes?page={page}");
|
|
if let Some(a) = author {
|
|
url.push_str(&format!("&author={}", js_sys::encode_uri_component(a)));
|
|
}
|
|
if let Some(t) = tag {
|
|
url.push_str(&format!("&tag={}", js_sys::encode_uri_component(t)));
|
|
}
|
|
fetch_json(&url).await
|
|
}
|
|
|
|
/// Fetch a single quote by its ID.
|
|
///
|
|
/// Returns `ApiError::Server { status: 404, .. }` if the quote does not exist.
|
|
pub async fn get_quote(id: &str) -> Result<Quote, ApiError> {
|
|
fetch_json(&format!("/api/quotes/{id}")).await
|
|
}
|
|
|
|
/// Fetch a random quote from the database.
|
|
pub async fn get_random_quote() -> Result<Quote, ApiError> {
|
|
fetch_json("/api/quotes/random").await
|
|
}
|
|
|
|
/// Create a new quote.
|
|
///
|
|
/// Returns the created quote and its auth code on success (HTTP 201).
|
|
pub async fn create_quote(input: &CreateQuoteInput) -> Result<CreateResponse, ApiError> {
|
|
let resp = gloo::net::http::Request::put("/api/quotes")
|
|
.json(input)
|
|
.map_err(|e| ApiError::Network(e.to_string()))?
|
|
.send()
|
|
.await
|
|
.map_err(|e| ApiError::Network(e.to_string()))?;
|
|
if resp.status() == 201 {
|
|
resp.json()
|
|
.await
|
|
.map_err(|e| ApiError::Parse(e.to_string()))
|
|
} else {
|
|
let msg = resp.text().await.unwrap_or_default();
|
|
Err(ApiError::Server {
|
|
status: resp.status(),
|
|
message: msg,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Update an existing quote.
|
|
///
|
|
/// Requires the correct `auth_code` in the `X-Auth-Code` header.
|
|
/// Returns `ApiError::Server { status: 403, .. }` on wrong auth code.
|
|
pub async fn update_quote(
|
|
id: &str,
|
|
input: &UpdateQuoteInput,
|
|
auth_code: &str,
|
|
) -> Result<Quote, ApiError> {
|
|
let resp = gloo::net::http::Request::post(&format!("/api/quotes/{id}"))
|
|
.header("X-Auth-Code", auth_code)
|
|
.json(input)
|
|
.map_err(|e| ApiError::Network(e.to_string()))?
|
|
.send()
|
|
.await
|
|
.map_err(|e| ApiError::Network(e.to_string()))?;
|
|
if resp.status() == 200 {
|
|
resp.json()
|
|
.await
|
|
.map_err(|e| ApiError::Parse(e.to_string()))
|
|
} else {
|
|
let msg = resp.text().await.unwrap_or_default();
|
|
Err(ApiError::Server {
|
|
status: resp.status(),
|
|
message: msg,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Delete a quote by ID.
|
|
///
|
|
/// Requires the correct `auth_code` in the `X-Auth-Code` header.
|
|
/// Returns `Ok(())` on HTTP 204. Returns `ApiError::Server { status: 403, .. }` on wrong auth code.
|
|
pub async fn delete_quote(id: &str, auth_code: &str) -> Result<(), ApiError> {
|
|
let resp = gloo::net::http::Request::delete(&format!("/api/quotes/{id}"))
|
|
.header("X-Auth-Code", auth_code)
|
|
.send()
|
|
.await
|
|
.map_err(|e| ApiError::Network(e.to_string()))?;
|
|
if resp.status() == 204 {
|
|
Ok(())
|
|
} else {
|
|
let msg = resp.text().await.unwrap_or_default();
|
|
Err(ApiError::Server {
|
|
status: resp.status(),
|
|
message: msg,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Internal helper: GET a URL and deserialise the response body as JSON.
|
|
///
|
|
/// Returns `ApiError` on non-2xx status or deserialisation failure.
|
|
async fn fetch_json<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, ApiError> {
|
|
let resp = gloo::net::http::Request::get(url)
|
|
.send()
|
|
.await
|
|
.map_err(|e| ApiError::Network(e.to_string()))?;
|
|
if resp.status() >= 200 && resp.status() < 300 {
|
|
resp.json()
|
|
.await
|
|
.map_err(|e| ApiError::Parse(e.to_string()))
|
|
} else {
|
|
let msg = resp.text().await.unwrap_or_default();
|
|
Err(ApiError::Server {
|
|
status: resp.status(),
|
|
message: msg,
|
|
})
|
|
}
|
|
}
|