cratestack-client-rust 0.4.13

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
use cratestack_core::SelectionQuery;
use reqwest::Url;
use reqwest::header::HeaderMap;

use crate::error::{ClientError, QueryPair};
use crate::runtime::wire::RuntimeHeader;

pub(crate) fn canonical_query_from_selection(
    selection: &SelectionQuery,
    extra_query: &[QueryPair<'_>],
) -> Result<Option<String>, ClientError> {
    let mut query: Vec<(String, String)> = Vec::new();
    if !selection.fields.is_empty() {
        query.push(("fields".to_owned(), selection.fields.join(",")));
    }
    if !selection.includes.is_empty() {
        query.push(("include".to_owned(), selection.includes.join(",")));
    }
    for (include, fields) in &selection.include_fields {
        if !fields.is_empty() {
            query.push((format!("includeFields[{include}]"), fields.join(",")));
        }
    }
    for (key, value) in extra_query {
        if *key == "fields" || *key == "include" || key.starts_with("includeFields[") {
            return Err(ClientError::BadInput(format!(
                "projection query parameter '{key}' must come from SelectionQuery, not extra_query"
            )));
        }
        query.push(((*key).to_owned(), (*value).to_owned()));
    }
    if query.is_empty() {
        return Ok(None);
    }
    serde_urlencoded::to_string(&query)
        .map(Some)
        .map_err(|error| ClientError::BadInput(format!("invalid selection query: {error}")))
}

pub(crate) fn headers_to_runtime(headers: &HeaderMap) -> Vec<RuntimeHeader> {
    headers
        .iter()
        .filter_map(|(name, value)| {
            value.to_str().ok().map(|value| RuntimeHeader {
                name: name.as_str().to_owned(),
                value: value.to_owned(),
            })
        })
        .collect()
}

pub(crate) fn build_url(
    base_url: &Url,
    path: &str,
    canonical_query: Option<&str>,
) -> Result<Url, ClientError> {
    let mut base = base_url.clone();
    if !base.path().ends_with('/') {
        let next_path = format!("{}/", base.path());
        base.set_path(&next_path);
    }
    let mut url = base.join(path.trim_start_matches('/')).map_err(|error| {
        ClientError::InvalidResponse(format!(
            "failed to resolve path '{path}' against {}: {error}",
            base
        ))
    })?;
    match canonical_query {
        Some(query) if !query.is_empty() => url.set_query(Some(query)),
        _ => url.set_query(None),
    }
    Ok(url)
}