Skip to main content

distri_types/
http_request.rs

1//! Typed request/response structs for the HTTP request proxy.
2//!
3//! Used by the `POST /request` server endpoint and the client-side
4//! `http_request` tool handler. Serializes cleanly across the wire.
5
6use std::collections::HashMap;
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11/// HTTP method.
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13#[serde(rename_all = "UPPERCASE")]
14pub enum HttpMethod {
15    #[default]
16    #[serde(alias = "get")]
17    GET,
18    #[serde(alias = "post")]
19    POST,
20    #[serde(alias = "put")]
21    PUT,
22    #[serde(alias = "patch")]
23    PATCH,
24    #[serde(alias = "delete")]
25    DELETE,
26}
27
28impl std::fmt::Display for HttpMethod {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::GET => write!(f, "GET"),
32            Self::POST => write!(f, "POST"),
33            Self::PUT => write!(f, "PUT"),
34            Self::PATCH => write!(f, "PATCH"),
35            Self::DELETE => write!(f, "DELETE"),
36        }
37    }
38}
39
40/// Input for an HTTP request — matches the tool parameter schema.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct HttpRequestInput {
43    /// Absolute URL. May contain `$VAR_NAME` for variable substitution.
44    pub url: String,
45    /// HTTP method.
46    #[serde(default)]
47    pub method: HttpMethod,
48    /// Request headers. May contain `$VAR_NAME` for variable substitution.
49    /// Set `x-connection-id` to inject an OAuth Bearer token.
50    #[serde(default)]
51    pub headers: HashMap<String, String>,
52    /// Request body. Sent as JSON if no Content-Type is set.
53    /// May contain `$VAR_NAME` for variable substitution.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub body: Option<serde_json::Value>,
56}
57
58/// Configuration for an HTTP request factory (type = "http").
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60pub struct HttpFactoryConfig {
61    /// Base URL for all requests. May contain $VAR_NAME.
62    pub base_url: String,
63    /// Default headers merged into every request. May contain $VAR_NAME.
64    #[serde(default)]
65    pub headers: HashMap<String, String>,
66}
67
68/// Input for a factory-created HTTP tool.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct HttpFactoryToolInput {
71    /// Request path (appended to base_url). Used for platform API calls.
72    #[serde(default)]
73    pub path: Option<String>,
74    /// Absolute URL. When set, `path` is ignored and `base_url` is NOT prepended.
75    /// Use this for external API calls (e.g., googleapis.com, slack.com).
76    /// Set `x-connection-id` header to auto-inject OAuth Bearer token.
77    #[serde(default)]
78    pub url: Option<String>,
79    /// HTTP method. Defaults to GET.
80    #[serde(default)]
81    pub method: HttpMethod,
82    /// Additional headers (merged with factory defaults, per-call wins).
83    #[serde(default)]
84    pub headers: HashMap<String, String>,
85    /// Request body.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub body: Option<serde_json::Value>,
88}
89
90impl HttpFactoryConfig {
91    /// Build an HttpRequestInput from factory defaults + per-call input.
92    ///
93    /// If `url` is set, use it as-is (external API call — base_url is NOT prepended).
94    /// If `path` is set, prepend base_url (platform API call).
95    /// Factory default headers are merged, but per-call headers win on conflict.
96    pub fn build_request(&self, input: &HttpFactoryToolInput) -> HttpRequestInput {
97        let url = if let Some(ref absolute_url) = input.url {
98            // External API call — use absolute URL directly, skip base_url
99            absolute_url.clone()
100        } else if let Some(ref path) = input.path {
101            // Platform API call — prepend base_url
102            format!("{}{}", self.base_url.trim_end_matches('/'), path)
103        } else {
104            // Fallback: just use base_url (shouldn't normally happen)
105            self.base_url.clone()
106        };
107
108        // For external URLs (url field), don't inject factory default headers
109        // (they contain platform auth like x-api-key which shouldn't leak to external APIs)
110        let headers = if input.url.is_some() {
111            input.headers.clone()
112        } else {
113            let mut headers = self.headers.clone();
114            headers.extend(input.headers.clone());
115            headers
116        };
117
118        HttpRequestInput {
119            url,
120            method: input.method.clone(),
121            headers,
122            body: input.body.clone(),
123        }
124    }
125}
126
127/// Response from an HTTP request.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct HttpRequestResponse {
130    /// HTTP status code.
131    pub status: u16,
132    /// Whether the status is 2xx.
133    pub ok: bool,
134    /// Filtered response headers (content-type, location, ratelimit, etc.)
135    pub headers: HashMap<String, String>,
136    /// Response body — parsed as JSON if content-type is application/json,
137    /// otherwise a string.
138    pub body: serde_json::Value,
139}