Skip to main content

switchback_protocols/
http.rs

1//! HTTP protocol implementation.
2
3use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::{
4    HttpContractMeta, HttpErrorMeta, HttpOperationMeta, HttpParameterMeta, HttpPayload,
5    HttpResponseMeta,
6};
7use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::__buffa::oneof::http_payload::Kind;
8use switchback_traits::{ProtocolAttachment, ResponseSeverity, Result};
9
10use crate::severity::{http_status_code_severity, http_status_severity};
11use crate::traits::{
12    ErrorProtocol, FieldCarrierProtocol, OperationProtocol, Protocol, ProtocolWire,
13    ResponseProtocol,
14};
15use crate::wire::{decode_message, encode_message};
16
17/// Built-in HTTP protocol (`"http"`).
18#[derive(Clone, Copy, Debug, Default)]
19pub struct HttpProtocol;
20
21impl Protocol for HttpProtocol {
22    fn id(&self) -> &'static str {
23        "http"
24    }
25}
26
27impl OperationProtocol for HttpProtocol {
28    type OperationMeta = HttpOperationMeta;
29
30    fn format_signature(&self, meta: &Self::OperationMeta) -> String {
31        format!(
32            "{} {}",
33            meta.method.to_ascii_uppercase(),
34            meta.path_template
35        )
36    }
37}
38
39impl ResponseProtocol for HttpProtocol {
40    fn response_severity(&self, status_key: &str) -> ResponseSeverity {
41        http_status_severity(status_key)
42    }
43}
44
45impl ErrorProtocol for HttpProtocol {
46    type ErrorMeta = HttpErrorMeta;
47
48    fn error_severity(&self, error_key: &str) -> ResponseSeverity {
49        http_status_severity(error_key)
50    }
51
52    fn format_error_label(&self, error_key: &str) -> String {
53        format!("HTTP {error_key}")
54    }
55}
56
57impl FieldCarrierProtocol for HttpProtocol {
58    fn field_carrier_kinds(&self) -> &'static [&'static str] {
59        &["header", "trailer", "cookie"]
60    }
61
62    fn valid_parameter_locations(&self) -> &'static [&'static str] {
63        &["query", "path", "header", "cookie"]
64    }
65}
66
67impl HttpProtocol {
68    /// Attach contract-level HTTP metadata.
69    pub fn attach_contract(&self, meta: &HttpContractMeta) -> ProtocolAttachment {
70        attachment_from_payload(HttpPayload {
71            kind: Some(Kind::Contract(Box::new(meta.clone()))),
72            ..Default::default()
73        })
74    }
75
76    /// Attach operation-level HTTP metadata.
77    pub fn attach_operation(&self, meta: &HttpOperationMeta) -> ProtocolAttachment {
78        attachment_from_payload(HttpPayload {
79            kind: Some(Kind::Operation(Box::new(meta.clone()))),
80            ..Default::default()
81        })
82    }
83
84    /// Attach success response HTTP metadata.
85    pub fn attach_response(&self, meta: &HttpResponseMeta) -> ProtocolAttachment {
86        attachment_from_payload(HttpPayload {
87            kind: Some(Kind::Response(Box::new(meta.clone()))),
88            ..Default::default()
89        })
90    }
91
92    /// Attach error response HTTP metadata.
93    pub fn attach_error(&self, meta: &HttpErrorMeta) -> ProtocolAttachment {
94        attachment_from_payload(HttpPayload {
95            kind: Some(Kind::Error(Box::new(meta.clone()))),
96            ..Default::default()
97        })
98    }
99
100    /// Attach parameter HTTP metadata.
101    pub fn attach_parameter(&self, meta: &HttpParameterMeta) -> ProtocolAttachment {
102        attachment_from_payload(HttpPayload {
103            kind: Some(Kind::Parameter(Box::new(meta.clone()))),
104            ..Default::default()
105        })
106    }
107
108    /// Build [`HttpOperationMeta`] from method, path, and streaming flags.
109    pub fn operation_meta(
110        method: &str,
111        path: &str,
112        request_streaming: bool,
113        response_streaming: bool,
114    ) -> HttpOperationMeta {
115        HttpOperationMeta {
116            method: method.to_ascii_uppercase(),
117            path_template: path.to_string(),
118            request_streaming,
119            response_streaming,
120            ..Default::default()
121        }
122    }
123
124    /// Build [`HttpResponseMeta`] from a status key and optional media type.
125    pub fn response_meta(status: &str, media_type: &str) -> HttpResponseMeta {
126        let status_code = status.parse::<u32>().unwrap_or(0);
127        HttpResponseMeta {
128            status_code,
129            media_type: media_type.to_string(),
130            ..Default::default()
131        }
132    }
133
134    /// Build [`HttpErrorMeta`] from a status key.
135    pub fn error_meta(status: &str, description: &str) -> HttpErrorMeta {
136        let status_code = status.parse::<u32>().unwrap_or(0);
137        HttpErrorMeta {
138            status_code,
139            detail: description.to_string(),
140            ..Default::default()
141        }
142    }
143
144    /// Build [`HttpParameterMeta`] from OpenAPI parameter fields.
145    pub fn parameter_meta(name: &str, location: &str, required: bool) -> HttpParameterMeta {
146        HttpParameterMeta {
147            name: name.to_string(),
148            location: location.to_string(),
149            required,
150            ..Default::default()
151        }
152    }
153
154    /// Build [`HttpContractMeta`] from server URLs.
155    pub fn contract_meta(server_urls: &[String], default: Option<&str>) -> HttpContractMeta {
156        HttpContractMeta {
157            server_urls: server_urls.to_vec(),
158            default_server_url: default.unwrap_or_default().to_string(),
159            ..Default::default()
160        }
161    }
162
163    /// Classify numeric HTTP status for populate.
164    pub fn severity_for_status_code(code: u16) -> ResponseSeverity {
165        http_status_code_severity(code)
166    }
167
168    /// True when the status code represents an error outcome.
169    pub fn is_error_status(status: &str) -> bool {
170        matches!(
171            http_status_severity(status),
172            ResponseSeverity::ClientError | ResponseSeverity::ServerError
173        )
174    }
175}
176
177impl ProtocolWire for HttpOperationMeta {
178    const PROTOCOL_ID: &'static str = "http";
179
180    fn encode_to_vec(&self) -> Vec<u8> {
181        encode_message(&HttpPayload {
182            kind: Some(Kind::Operation(Box::new(self.clone()))),
183            ..Default::default()
184        })
185    }
186
187    fn decode_from_bytes(bytes: &[u8]) -> Result<Self> {
188        let payload: HttpPayload = decode_message(bytes)?;
189        match payload.kind {
190            Some(Kind::Operation(meta)) => Ok(*meta),
191            _ => Err(switchback_traits::SwitchbackError::codec(
192                "expected HttpOperationMeta payload",
193            )),
194        }
195    }
196}
197
198impl ProtocolWire for HttpErrorMeta {
199    const PROTOCOL_ID: &'static str = "http";
200
201    fn encode_to_vec(&self) -> Vec<u8> {
202        encode_message(&HttpPayload {
203            kind: Some(Kind::Error(Box::new(self.clone()))),
204            ..Default::default()
205        })
206    }
207
208    fn decode_from_bytes(bytes: &[u8]) -> Result<Self> {
209        let payload: HttpPayload = decode_message(bytes)?;
210        match payload.kind {
211            Some(Kind::Error(meta)) => Ok(*meta),
212            _ => Err(switchback_traits::SwitchbackError::codec(
213                "expected HttpErrorMeta payload",
214            )),
215        }
216    }
217}
218
219fn attachment_from_payload(payload: HttpPayload) -> ProtocolAttachment {
220    let protocol = HttpProtocol;
221    ProtocolAttachment {
222        protocol_id: protocol.id().to_string(),
223        payload: encode_message(&payload),
224    }
225}