use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaslMechanism {
SaslPlain,
SaslAnonymous,
SaslExternal,
SaslScramSha256,
}
impl SaslMechanism {
#[must_use]
pub const fn as_idl(self) -> &'static str {
match self {
Self::SaslPlain => "SASL_PLAIN",
Self::SaslAnonymous => "SASL_ANONYMOUS",
Self::SaslExternal => "SASL_EXTERNAL",
Self::SaslScramSha256 => "SASL_SCRAM_SHA_256",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"SASL_PLAIN" | "PLAIN" => Some(Self::SaslPlain),
"SASL_ANONYMOUS" | "ANONYMOUS" => Some(Self::SaslAnonymous),
"SASL_EXTERNAL" | "EXTERNAL" => Some(Self::SaslExternal),
"SASL_SCRAM_SHA_256" | "SCRAM-SHA-256" | "SCRAM_SHA_256" => Some(Self::SaslScramSha256),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BodyEncodingMode {
#[default]
ModePassthrough,
ModeJson,
ModeAmqpNative,
}
impl BodyEncodingMode {
#[must_use]
pub const fn as_idl(self) -> &'static str {
match self {
Self::ModePassthrough => "MODE_PASSTHROUGH",
Self::ModeJson => "MODE_JSON",
Self::ModeAmqpNative => "MODE_AMQP_NATIVE",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"MODE_PASSTHROUGH" | "PASSTHROUGH" | "PASS_THROUGH" => Some(Self::ModePassthrough),
"MODE_JSON" | "JSON" => Some(Self::ModeJson),
"MODE_AMQP_NATIVE" | "AMQP_NATIVE" => Some(Self::ModeAmqpNative),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimeMapping {
#[default]
MappingStandard,
MappingComposite,
}
impl TimeMapping {
#[must_use]
pub const fn as_idl(self) -> &'static str {
match self {
Self::MappingStandard => "MAPPING_STANDARD",
Self::MappingComposite => "MAPPING_COMPOSITE",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"MAPPING_STANDARD" | "STANDARD" => Some(Self::MappingStandard),
"MAPPING_COMPOSITE" | "COMPOSITE" => Some(Self::MappingComposite),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DescriptorForm {
#[default]
DescTruncated,
DescFull,
}
impl DescriptorForm {
#[must_use]
pub const fn as_idl(self) -> &'static str {
match self {
Self::DescTruncated => "DESC_TRUNCATED",
Self::DescFull => "DESC_FULL",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"DESC_TRUNCATED" | "TRUNCATED" => Some(Self::DescTruncated),
"DESC_FULL" | "FULL" => Some(Self::DescFull),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LinkDirection {
DirProducerToDds,
DirDdsToConsumer,
#[default]
DirBoth,
}
impl LinkDirection {
#[must_use]
pub const fn as_idl(self) -> &'static str {
match self {
Self::DirProducerToDds => "DIR_PRODUCER_TO_DDS",
Self::DirDdsToConsumer => "DIR_DDS_TO_CONSUMER",
Self::DirBoth => "DIR_BOTH",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"DIR_PRODUCER_TO_DDS" | "PRODUCER_TO_DDS" | "producer-to-dds" => {
Some(Self::DirProducerToDds)
}
"DIR_DDS_TO_CONSUMER" | "DDS_TO_CONSUMER" | "dds-to-consumer" => {
Some(Self::DirDdsToConsumer)
}
"DIR_BOTH" | "BOTH" | "both" => Some(Self::DirBoth),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct TopicMapping {
pub amqp_address: String,
pub dds_topic: String,
pub dds_type_name: String,
pub dds_domain_id: u32,
pub dds_partition: Vec<String>,
pub mode: BodyEncodingMode,
pub time_mapping: TimeMapping,
pub descriptor_form: DescriptorForm,
pub rpc_aware: bool,
pub rpc_timeout_ms: u32,
pub direction: LinkDirection,
}
impl Default for TopicMapping {
fn default() -> Self {
Self {
amqp_address: String::new(),
dds_topic: String::new(),
dds_type_name: String::new(),
dds_domain_id: 0,
dds_partition: Vec::new(),
mode: BodyEncodingMode::default(),
time_mapping: TimeMapping::default(),
descriptor_form: DescriptorForm::default(),
rpc_aware: false,
rpc_timeout_ms: 30_000,
direction: LinkDirection::default(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TlsConfig {
pub enabled: bool,
pub cert_path: String,
pub key_path: String,
pub ca_path: String,
pub require_client_cert: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SaslConfig {
pub enabled_mechanisms: Vec<SaslMechanism>,
pub credential_store_uri: String,
}
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_connections: u32,
pub max_sessions_per_connection: u32,
pub max_links_per_session: u32,
pub max_frame_size: u32,
pub idle_timeout_ms: u64,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_connections: 1024,
max_sessions_per_connection: 8,
max_links_per_session: 16,
max_frame_size: 1_048_576, idle_timeout_ms: 60_000,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DynamicTopicConfig {
pub permit_dynamic_topics: bool,
pub dynamic_topic_default_type: String,
pub default_mode: BodyEncodingMode,
}
#[derive(Debug, Clone, Default)]
pub struct AmqpEndpointConfig {
pub endpoint_name: String,
pub listen_uri: String,
pub tls: TlsConfig,
pub sasl: SaslConfig,
pub topics: Vec<TopicMapping>,
pub dynamic: DynamicTopicConfig,
pub limits: ResourceLimits,
pub bridge_id: String,
pub bridge_hop_cap: u8,
}
#[derive(Debug, Clone, Default)]
pub struct AmqpBridgeConfig {
pub bridge_name: String,
pub upstream_uri: String,
pub tls: TlsConfig,
pub sasl: SaslConfig,
pub topics: Vec<TopicMapping>,
pub bridge_id: String,
pub bridge_hop_cap: u8,
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn sasl_mechanism_idl_round_trip() {
for m in [
SaslMechanism::SaslPlain,
SaslMechanism::SaslAnonymous,
SaslMechanism::SaslExternal,
SaslMechanism::SaslScramSha256,
] {
assert_eq!(SaslMechanism::parse(m.as_idl()), Some(m));
}
}
#[test]
fn sasl_mechanism_accepts_amqp_wire_aliases() {
assert_eq!(
SaslMechanism::parse("PLAIN"),
Some(SaslMechanism::SaslPlain)
);
assert_eq!(
SaslMechanism::parse("ANONYMOUS"),
Some(SaslMechanism::SaslAnonymous)
);
assert_eq!(
SaslMechanism::parse("EXTERNAL"),
Some(SaslMechanism::SaslExternal)
);
assert_eq!(
SaslMechanism::parse("SCRAM-SHA-256"),
Some(SaslMechanism::SaslScramSha256)
);
}
#[test]
fn body_encoding_mode_idl_round_trip() {
for m in [
BodyEncodingMode::ModePassthrough,
BodyEncodingMode::ModeJson,
BodyEncodingMode::ModeAmqpNative,
] {
assert_eq!(BodyEncodingMode::parse(m.as_idl()), Some(m));
}
assert_eq!(
BodyEncodingMode::default(),
BodyEncodingMode::ModePassthrough
);
}
#[test]
fn descriptor_form_default_is_truncated() {
assert_eq!(DescriptorForm::default(), DescriptorForm::DescTruncated);
}
#[test]
fn time_mapping_default_is_standard() {
assert_eq!(TimeMapping::default(), TimeMapping::MappingStandard);
}
#[test]
fn link_direction_default_is_both() {
assert_eq!(LinkDirection::default(), LinkDirection::DirBoth);
}
#[test]
fn topic_mapping_defaults_match_spec() {
let t = TopicMapping::default();
assert_eq!(t.dds_domain_id, 0);
assert_eq!(t.mode, BodyEncodingMode::ModePassthrough);
assert_eq!(t.time_mapping, TimeMapping::MappingStandard);
assert_eq!(t.descriptor_form, DescriptorForm::DescTruncated);
assert!(!t.rpc_aware);
assert_eq!(t.rpc_timeout_ms, 30_000);
assert_eq!(t.direction, LinkDirection::DirBoth);
}
#[test]
fn endpoint_config_default_is_empty() {
let c = AmqpEndpointConfig::default();
assert!(c.endpoint_name.is_empty());
assert!(c.topics.is_empty());
assert_eq!(c.bridge_hop_cap, 0);
}
#[test]
fn resource_limits_default_has_dos_caps() {
let l = ResourceLimits::default();
assert!(l.max_connections > 0);
assert!(l.max_frame_size >= 65_536);
assert!(l.idle_timeout_ms > 0);
}
#[test]
fn link_direction_amqp_aliases() {
assert_eq!(
LinkDirection::parse("producer-to-dds"),
Some(LinkDirection::DirProducerToDds)
);
assert_eq!(
LinkDirection::parse("dds-to-consumer"),
Some(LinkDirection::DirDdsToConsumer)
);
assert_eq!(LinkDirection::parse("both"), Some(LinkDirection::DirBoth));
}
}