octopust 0.2.0

A high-performance, idiomatic Rust client for the Octopus Energy API.
Documentation
use reqwest::Client as HttpClient;
use crate::error::{ApiError, OctopustError};
use crate::models::{Product, ProductDetail};

pub async fn list_products(
    http: &HttpClient,
    base_url: &str,
) -> Result<Vec<Product>, OctopustError> {
    let url = format!("{}/products/", base_url.trim_end_matches('/'));
    let resp = http.get(&url).send().await?;
    let status = resp.status();
    let body_bytes = resp.bytes().await?;
    let body_str = String::from_utf8_lossy(&body_bytes);

    if !status.is_success() {
        return Err(OctopustError::Api(ApiError {
            status,
            message: format!("API returned error status {}: {}", status, body_str),
        }));
    }

    let json: serde_json::Value = serde_json::from_slice(&body_bytes).map_err(|e| {
        OctopustError::Api(ApiError {
            status,
            message: format!(
                "Failed to parse products JSON: {}. Response body: {}",
                e, body_str
            ),
        })
    })?;

    if let Some(arr) = json.get("results").and_then(|v| v.as_array()) {
        let products: Vec<Product> = serde_json::from_value(arr.clone().into()).map_err(|e| {
            OctopustError::Api(ApiError {
                status,
                message: format!(
                    "Failed to parse products array: {}. Response body: {}",
                    e, body_str
                ),
            })
        })?;
        Ok(products)
    } else {
        Err(OctopustError::Api(ApiError {
            status,
            message: format!("Malformed products response: {}", body_str),
        }))
    }
}

pub async fn retrieve_product(
    http: &HttpClient,
    base_url: &str,
    product_code: &str,
) -> Result<ProductDetail, OctopustError> {
    let url = format!("{}/products/{}/", base_url.trim_end_matches('/'), product_code);
    let resp = http.get(&url).send().await?;
    let status = resp.status();
    let body_bytes = resp.bytes().await?;
    let body_str = String::from_utf8_lossy(&body_bytes);

    if !status.is_success() {
        return Err(OctopustError::Api(ApiError {
            status,
            message: format!(
                "API returned error status {}: {}",
                status, body_str
            ),
        }));
    }

    let product_detail: ProductDetail = serde_json::from_slice(&body_bytes).map_err(|e| {
        OctopustError::Api(ApiError {
            status,
            message: format!(
                "Failed to parse product JSON: {}. Response body: {}",
                e, body_str
            ),
        })
    })?;
    Ok(product_detail)
}