Skip to main content

switchback_protocols/
registry.rs

1//! Protocol registry for encode/decode of attachments.
2
3use switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcPayload;
4use switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::__buffa::oneof::grpc_payload::Kind as GrpcKind;
5use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpPayload;
6use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::__buffa::oneof::http_payload::Kind as HttpKind;
7use switchback_traits::{ProtocolAttachment, Result, SwitchbackError};
8
9use crate::grpc::GrpcProtocol;
10use crate::http::HttpProtocol;
11use crate::wire::decode_message;
12
13/// Decoded HTTP payload oneof arm.
14#[derive(Clone, Debug, PartialEq)]
15pub enum HttpPayloadKind {
16    /// Contract-level metadata.
17    Contract(
18        switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpContractMeta,
19    ),
20    /// Operation invocation metadata.
21    Operation(
22        switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpOperationMeta,
23    ),
24    /// Success response metadata.
25    Response(
26        switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpResponseMeta,
27    ),
28    /// Error response metadata.
29    Error(switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpErrorMeta),
30    /// Parameter metadata.
31    Parameter(
32        switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpParameterMeta,
33    ),
34}
35
36/// Decoded gRPC payload oneof arm.
37#[derive(Clone, Debug, PartialEq)]
38pub enum GrpcPayloadKind {
39    /// Contract-level metadata.
40    Contract(
41        switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcContractMeta,
42    ),
43    /// Operation invocation metadata.
44    Operation(
45        switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcOperationMeta,
46    ),
47    /// Success status metadata.
48    Status(switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcStatusMeta),
49    /// Error metadata.
50    Error(switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcErrorMeta),
51    /// Metadata key.
52    Metadata(
53        switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcMetadataMeta,
54    ),
55}
56
57/// Result of decoding a [`ProtocolAttachment`].
58#[derive(Clone, Debug, PartialEq)]
59pub enum DecodedAttachment {
60    /// Known HTTP payload arm.
61    Http(HttpPayloadKind),
62    /// Known gRPC payload arm.
63    Grpc(GrpcPayloadKind),
64    /// Unknown or custom protocol; bytes round-trip opaquely.
65    Opaque {
66        /// Protocol slug from the attachment envelope.
67        protocol_id: String,
68        /// Opaque payload bytes.
69        payload: Vec<u8>,
70    },
71}
72
73/// Registry of built-in protocol decoders.
74#[derive(Clone, Debug, Default)]
75pub struct ProtocolRegistry {
76    http: HttpProtocol,
77    grpc: GrpcProtocol,
78}
79
80impl ProtocolRegistry {
81    /// Built-in registry with `http` and `grpc` registered.
82    pub fn with_builtins() -> Self {
83        Self::default()
84    }
85
86    /// HTTP protocol implementation.
87    pub fn http(&self) -> &HttpProtocol {
88        &self.http
89    }
90
91    /// gRPC protocol implementation.
92    pub fn grpc(&self) -> &GrpcProtocol {
93        &self.grpc
94    }
95
96    /// Decode a protocol attachment envelope.
97    ///
98    /// Built-in ids `"http"` and `"grpc"` deserialize to [`DecodedAttachment::Http`]
99    /// or [`DecodedAttachment::Grpc`]; other ids return
100    /// [`DecodedAttachment::Opaque`] with bytes unchanged.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpOperationMeta;
106    /// use switchback_protocols::{DecodedAttachment, HttpPayloadKind, ProtocolRegistry};
107    ///
108    /// let registry = ProtocolRegistry::with_builtins();
109    /// let attachment = registry.http().attach_operation(&HttpOperationMeta {
110    ///     method: "GET".into(),
111    ///     path_template: "/pets".into(),
112    ///     ..Default::default()
113    /// });
114    /// match registry.decode_attachment(&attachment).unwrap() {
115    ///     DecodedAttachment::Http(HttpPayloadKind::Operation(m)) => assert_eq!(m.method, "GET"),
116    ///     _ => panic!("expected operation meta"),
117    /// }
118    /// ```
119    pub fn decode_attachment(&self, attachment: &ProtocolAttachment) -> Result<DecodedAttachment> {
120        match attachment.protocol_id.as_str() {
121            "http" => decode_http(&attachment.payload).map(DecodedAttachment::Http),
122            "grpc" => decode_grpc(&attachment.payload).map(DecodedAttachment::Grpc),
123            other => Ok(DecodedAttachment::Opaque {
124                protocol_id: other.to_string(),
125                payload: attachment.payload.clone(),
126            }),
127        }
128    }
129
130    /// Find the first HTTP operation meta on an operation body's attachments.
131    pub fn http_operation_from_attachments(
132        &self,
133        protocols: &[ProtocolAttachment],
134    ) -> Option<
135        switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpOperationMeta,
136    > {
137        for attachment in protocols {
138            if let Ok(DecodedAttachment::Http(HttpPayloadKind::Operation(meta))) =
139                self.decode_attachment(attachment)
140            {
141                return Some(meta);
142            }
143        }
144        None
145    }
146
147    /// Find the first gRPC operation meta on an operation body's attachments.
148    pub fn grpc_operation_from_attachments(
149        &self,
150        protocols: &[ProtocolAttachment],
151    ) -> Option<
152        switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcOperationMeta,
153    > {
154        for attachment in protocols {
155            if let Ok(DecodedAttachment::Grpc(GrpcPayloadKind::Operation(meta))) =
156                self.decode_attachment(attachment)
157            {
158                return Some(meta);
159            }
160        }
161        None
162    }
163}
164
165fn decode_http(bytes: &[u8]) -> Result<HttpPayloadKind> {
166    let payload: HttpPayload = decode_message(bytes)?;
167    match payload.kind {
168        Some(HttpKind::Contract(v)) => Ok(HttpPayloadKind::Contract(*v)),
169        Some(HttpKind::Operation(v)) => Ok(HttpPayloadKind::Operation(*v)),
170        Some(HttpKind::Response(v)) => Ok(HttpPayloadKind::Response(*v)),
171        Some(HttpKind::Error(v)) => Ok(HttpPayloadKind::Error(*v)),
172        Some(HttpKind::Parameter(v)) => Ok(HttpPayloadKind::Parameter(*v)),
173        None => Err(SwitchbackError::codec("empty HttpPayload")),
174    }
175}
176
177fn decode_grpc(bytes: &[u8]) -> Result<GrpcPayloadKind> {
178    let payload: GrpcPayload = decode_message(bytes)?;
179    match payload.kind {
180        Some(GrpcKind::Contract(v)) => Ok(GrpcPayloadKind::Contract(*v)),
181        Some(GrpcKind::Operation(v)) => Ok(GrpcPayloadKind::Operation(*v)),
182        Some(GrpcKind::Status(v)) => Ok(GrpcPayloadKind::Status(*v)),
183        Some(GrpcKind::Error(v)) => Ok(GrpcPayloadKind::Error(*v)),
184        Some(GrpcKind::Metadata(v)) => Ok(GrpcPayloadKind::Metadata(*v)),
185        None => Err(SwitchbackError::codec("empty GrpcPayload")),
186    }
187}
188
189#[cfg(test)]
190mod coverage_matrix {
191    use super::*;
192    use switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::{
193        GrpcContractMeta, GrpcErrorMeta, GrpcMetadataMeta, GrpcOperationMeta, GrpcStatusMeta,
194    };
195    use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::{
196        HttpContractMeta, HttpErrorMeta, HttpOperationMeta, HttpParameterMeta, HttpResponseMeta,
197    };
198
199    #[test]
200    fn http_matrix_roundtrips() {
201        let registry = ProtocolRegistry::with_builtins();
202        let http = registry.http();
203
204        let cases: Vec<(HttpPayloadKind, ProtocolAttachment)> = vec![
205            (
206                HttpPayloadKind::Contract(HttpContractMeta {
207                    default_server_url: "https://api.example.com".into(),
208                    ..Default::default()
209                }),
210                http.attach_contract(&HttpContractMeta {
211                    default_server_url: "https://api.example.com".into(),
212                    ..Default::default()
213                }),
214            ),
215            (
216                HttpPayloadKind::Operation(HttpOperationMeta {
217                    method: "GET".into(),
218                    path_template: "/pets".into(),
219                    ..Default::default()
220                }),
221                http.attach_operation(&HttpOperationMeta {
222                    method: "GET".into(),
223                    path_template: "/pets".into(),
224                    ..Default::default()
225                }),
226            ),
227            (
228                HttpPayloadKind::Response(HttpResponseMeta {
229                    status_code: 200,
230                    ..Default::default()
231                }),
232                http.attach_response(&HttpResponseMeta {
233                    status_code: 200,
234                    ..Default::default()
235                }),
236            ),
237            (
238                HttpPayloadKind::Error(HttpErrorMeta {
239                    status_code: 404,
240                    ..Default::default()
241                }),
242                http.attach_error(&HttpErrorMeta {
243                    status_code: 404,
244                    ..Default::default()
245                }),
246            ),
247            (
248                HttpPayloadKind::Parameter(HttpParameterMeta {
249                    name: "id".into(),
250                    location: "path".into(),
251                    required: true,
252                    ..Default::default()
253                }),
254                http.attach_parameter(&HttpParameterMeta {
255                    name: "id".into(),
256                    location: "path".into(),
257                    required: true,
258                    ..Default::default()
259                }),
260            ),
261        ];
262
263        for (expected_kind, attachment) in cases {
264            match registry.decode_attachment(&attachment).unwrap() {
265                DecodedAttachment::Http(kind) => assert_eq!(kind, expected_kind),
266                other => panic!("expected http decode, got {other:?}"),
267            }
268        }
269    }
270
271    #[test]
272    fn grpc_matrix_roundtrips() {
273        let registry = ProtocolRegistry::with_builtins();
274        let grpc = registry.grpc();
275
276        let cases: Vec<(GrpcPayloadKind, ProtocolAttachment)> = vec![
277            (
278                GrpcPayloadKind::Contract(GrpcContractMeta {
279                    package_name: "acme.v1".into(),
280                    ..Default::default()
281                }),
282                grpc.attach_contract(&GrpcContractMeta {
283                    package_name: "acme.v1".into(),
284                    ..Default::default()
285                }),
286            ),
287            (
288                GrpcPayloadKind::Operation(GrpcOperationMeta {
289                    rpc_name: "GetPet".into(),
290                    ..Default::default()
291                }),
292                grpc.attach_operation(&GrpcOperationMeta {
293                    rpc_name: "GetPet".into(),
294                    ..Default::default()
295                }),
296            ),
297            (
298                GrpcPayloadKind::Status(GrpcStatusMeta {
299                    code: 0,
300                    message: "OK".into(),
301                    ..Default::default()
302                }),
303                grpc.attach_status(&GrpcStatusMeta {
304                    code: 0,
305                    message: "OK".into(),
306                    ..Default::default()
307                }),
308            ),
309            (
310                GrpcPayloadKind::Error(GrpcErrorMeta {
311                    code: 5,
312                    message: "not found".into(),
313                    ..Default::default()
314                }),
315                grpc.attach_error(&GrpcErrorMeta {
316                    code: 5,
317                    message: "not found".into(),
318                    ..Default::default()
319                }),
320            ),
321            (
322                GrpcPayloadKind::Metadata(GrpcMetadataMeta {
323                    key: "x-request-id".into(),
324                    required: false,
325                    ..Default::default()
326                }),
327                grpc.attach_metadata(&GrpcMetadataMeta {
328                    key: "x-request-id".into(),
329                    required: false,
330                    ..Default::default()
331                }),
332            ),
333        ];
334
335        for (expected_kind, attachment) in cases {
336            match registry.decode_attachment(&attachment).unwrap() {
337                DecodedAttachment::Grpc(kind) => assert_eq!(kind, expected_kind),
338                other => panic!("expected grpc decode, got {other:?}"),
339            }
340        }
341    }
342
343    #[test]
344    fn opaque_custom_protocol_passthrough() {
345        let registry = ProtocolRegistry::with_builtins();
346        let attachment = ProtocolAttachment {
347            protocol_id: "acme/kafka".into(),
348            payload: vec![1, 2, 3],
349        };
350        match registry.decode_attachment(&attachment).unwrap() {
351            DecodedAttachment::Opaque {
352                protocol_id,
353                payload,
354            } => {
355                assert_eq!(protocol_id, "acme/kafka");
356                assert_eq!(payload, vec![1, 2, 3]);
357            }
358            other => panic!("expected opaque, got {other:?}"),
359        }
360    }
361}