1use 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#[derive(Clone, Debug, PartialEq)]
15pub enum HttpPayloadKind {
16 Contract(
18 switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpContractMeta,
19 ),
20 Operation(
22 switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpOperationMeta,
23 ),
24 Response(
26 switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpResponseMeta,
27 ),
28 Error(switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpErrorMeta),
30 Parameter(
32 switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpParameterMeta,
33 ),
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub enum GrpcPayloadKind {
39 Contract(
41 switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcContractMeta,
42 ),
43 Operation(
45 switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcOperationMeta,
46 ),
47 Status(switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcStatusMeta),
49 Error(switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcErrorMeta),
51 Metadata(
53 switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcMetadataMeta,
54 ),
55}
56
57#[derive(Clone, Debug, PartialEq)]
59pub enum DecodedAttachment {
60 Http(HttpPayloadKind),
62 Grpc(GrpcPayloadKind),
64 Opaque {
66 protocol_id: String,
68 payload: Vec<u8>,
70 },
71}
72
73#[derive(Clone, Debug, Default)]
75pub struct ProtocolRegistry {
76 http: HttpProtocol,
77 grpc: GrpcProtocol,
78}
79
80impl ProtocolRegistry {
81 pub fn with_builtins() -> Self {
83 Self::default()
84 }
85
86 pub fn http(&self) -> &HttpProtocol {
88 &self.http
89 }
90
91 pub fn grpc(&self) -> &GrpcProtocol {
93 &self.grpc
94 }
95
96 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 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 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}