use switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcPayload;
use switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::__buffa::oneof::grpc_payload::Kind as GrpcKind;
use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpPayload;
use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::__buffa::oneof::http_payload::Kind as HttpKind;
use switchback_traits::{ProtocolAttachment, Result, SwitchbackError};
use crate::grpc::GrpcProtocol;
use crate::http::HttpProtocol;
use crate::wire::decode_message;
#[derive(Clone, Debug, PartialEq)]
pub enum HttpPayloadKind {
Contract(
switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpContractMeta,
),
Operation(
switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpOperationMeta,
),
Response(
switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpResponseMeta,
),
Error(switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpErrorMeta),
Parameter(
switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpParameterMeta,
),
}
#[derive(Clone, Debug, PartialEq)]
pub enum GrpcPayloadKind {
Contract(
switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcContractMeta,
),
Operation(
switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcOperationMeta,
),
Status(switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcStatusMeta),
Error(switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcErrorMeta),
Metadata(
switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcMetadataMeta,
),
}
#[derive(Clone, Debug, PartialEq)]
pub enum DecodedAttachment {
Http(HttpPayloadKind),
Grpc(GrpcPayloadKind),
Opaque {
protocol_id: String,
payload: Vec<u8>,
},
}
#[derive(Clone, Debug, Default)]
pub struct ProtocolRegistry {
http: HttpProtocol,
grpc: GrpcProtocol,
}
impl ProtocolRegistry {
pub fn with_builtins() -> Self {
Self::default()
}
pub fn http(&self) -> &HttpProtocol {
&self.http
}
pub fn grpc(&self) -> &GrpcProtocol {
&self.grpc
}
pub fn decode_attachment(&self, attachment: &ProtocolAttachment) -> Result<DecodedAttachment> {
match attachment.protocol_id.as_str() {
"http" => decode_http(&attachment.payload).map(DecodedAttachment::Http),
"grpc" => decode_grpc(&attachment.payload).map(DecodedAttachment::Grpc),
other => Ok(DecodedAttachment::Opaque {
protocol_id: other.to_string(),
payload: attachment.payload.clone(),
}),
}
}
pub fn http_operation_from_attachments(
&self,
protocols: &[ProtocolAttachment],
) -> Option<
switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::HttpOperationMeta,
> {
for attachment in protocols {
if let Ok(DecodedAttachment::Http(HttpPayloadKind::Operation(meta))) =
self.decode_attachment(attachment)
{
return Some(meta);
}
}
None
}
pub fn grpc_operation_from_attachments(
&self,
protocols: &[ProtocolAttachment],
) -> Option<
switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::GrpcOperationMeta,
> {
for attachment in protocols {
if let Ok(DecodedAttachment::Grpc(GrpcPayloadKind::Operation(meta))) =
self.decode_attachment(attachment)
{
return Some(meta);
}
}
None
}
}
fn decode_http(bytes: &[u8]) -> Result<HttpPayloadKind> {
let payload: HttpPayload = decode_message(bytes)?;
match payload.kind {
Some(HttpKind::Contract(v)) => Ok(HttpPayloadKind::Contract(*v)),
Some(HttpKind::Operation(v)) => Ok(HttpPayloadKind::Operation(*v)),
Some(HttpKind::Response(v)) => Ok(HttpPayloadKind::Response(*v)),
Some(HttpKind::Error(v)) => Ok(HttpPayloadKind::Error(*v)),
Some(HttpKind::Parameter(v)) => Ok(HttpPayloadKind::Parameter(*v)),
None => Err(SwitchbackError::codec("empty HttpPayload")),
}
}
fn decode_grpc(bytes: &[u8]) -> Result<GrpcPayloadKind> {
let payload: GrpcPayload = decode_message(bytes)?;
match payload.kind {
Some(GrpcKind::Contract(v)) => Ok(GrpcPayloadKind::Contract(*v)),
Some(GrpcKind::Operation(v)) => Ok(GrpcPayloadKind::Operation(*v)),
Some(GrpcKind::Status(v)) => Ok(GrpcPayloadKind::Status(*v)),
Some(GrpcKind::Error(v)) => Ok(GrpcPayloadKind::Error(*v)),
Some(GrpcKind::Metadata(v)) => Ok(GrpcPayloadKind::Metadata(*v)),
None => Err(SwitchbackError::codec("empty GrpcPayload")),
}
}
#[cfg(test)]
mod coverage_matrix {
use super::*;
use switchback_codec_pb::canardleteer::switchback::protocol::grpc::v1alpha1::{
GrpcContractMeta, GrpcErrorMeta, GrpcMetadataMeta, GrpcOperationMeta, GrpcStatusMeta,
};
use switchback_codec_pb::canardleteer::switchback::protocol::http::v1alpha1::{
HttpContractMeta, HttpErrorMeta, HttpOperationMeta, HttpParameterMeta, HttpResponseMeta,
};
#[test]
fn http_matrix_roundtrips() {
let registry = ProtocolRegistry::with_builtins();
let http = registry.http();
let cases: Vec<(HttpPayloadKind, ProtocolAttachment)> = vec![
(
HttpPayloadKind::Contract(HttpContractMeta {
default_server_url: "https://api.example.com".into(),
..Default::default()
}),
http.attach_contract(&HttpContractMeta {
default_server_url: "https://api.example.com".into(),
..Default::default()
}),
),
(
HttpPayloadKind::Operation(HttpOperationMeta {
method: "GET".into(),
path_template: "/pets".into(),
..Default::default()
}),
http.attach_operation(&HttpOperationMeta {
method: "GET".into(),
path_template: "/pets".into(),
..Default::default()
}),
),
(
HttpPayloadKind::Response(HttpResponseMeta {
status_code: 200,
..Default::default()
}),
http.attach_response(&HttpResponseMeta {
status_code: 200,
..Default::default()
}),
),
(
HttpPayloadKind::Error(HttpErrorMeta {
status_code: 404,
..Default::default()
}),
http.attach_error(&HttpErrorMeta {
status_code: 404,
..Default::default()
}),
),
(
HttpPayloadKind::Parameter(HttpParameterMeta {
name: "id".into(),
location: "path".into(),
required: true,
..Default::default()
}),
http.attach_parameter(&HttpParameterMeta {
name: "id".into(),
location: "path".into(),
required: true,
..Default::default()
}),
),
];
for (expected_kind, attachment) in cases {
match registry.decode_attachment(&attachment).unwrap() {
DecodedAttachment::Http(kind) => assert_eq!(kind, expected_kind),
other => panic!("expected http decode, got {other:?}"),
}
}
}
#[test]
fn grpc_matrix_roundtrips() {
let registry = ProtocolRegistry::with_builtins();
let grpc = registry.grpc();
let cases: Vec<(GrpcPayloadKind, ProtocolAttachment)> = vec![
(
GrpcPayloadKind::Contract(GrpcContractMeta {
package_name: "acme.v1".into(),
..Default::default()
}),
grpc.attach_contract(&GrpcContractMeta {
package_name: "acme.v1".into(),
..Default::default()
}),
),
(
GrpcPayloadKind::Operation(GrpcOperationMeta {
rpc_name: "GetPet".into(),
..Default::default()
}),
grpc.attach_operation(&GrpcOperationMeta {
rpc_name: "GetPet".into(),
..Default::default()
}),
),
(
GrpcPayloadKind::Status(GrpcStatusMeta {
code: 0,
message: "OK".into(),
..Default::default()
}),
grpc.attach_status(&GrpcStatusMeta {
code: 0,
message: "OK".into(),
..Default::default()
}),
),
(
GrpcPayloadKind::Error(GrpcErrorMeta {
code: 5,
message: "not found".into(),
..Default::default()
}),
grpc.attach_error(&GrpcErrorMeta {
code: 5,
message: "not found".into(),
..Default::default()
}),
),
(
GrpcPayloadKind::Metadata(GrpcMetadataMeta {
key: "x-request-id".into(),
required: false,
..Default::default()
}),
grpc.attach_metadata(&GrpcMetadataMeta {
key: "x-request-id".into(),
required: false,
..Default::default()
}),
),
];
for (expected_kind, attachment) in cases {
match registry.decode_attachment(&attachment).unwrap() {
DecodedAttachment::Grpc(kind) => assert_eq!(kind, expected_kind),
other => panic!("expected grpc decode, got {other:?}"),
}
}
}
#[test]
fn opaque_custom_protocol_passthrough() {
let registry = ProtocolRegistry::with_builtins();
let attachment = ProtocolAttachment {
protocol_id: "acme/kafka".into(),
payload: vec![1, 2, 3],
};
match registry.decode_attachment(&attachment).unwrap() {
DecodedAttachment::Opaque {
protocol_id,
payload,
} => {
assert_eq!(protocol_id, "acme/kafka");
assert_eq!(payload, vec![1, 2, 3]);
}
other => panic!("expected opaque, got {other:?}"),
}
}
}