use std::{error::Error as StdError, future::Future, time::Duration};
use futures::Stream;
use crate::{Broker, ConnectedBroker, IncomingMessage, OutgoingMessage, Publisher, Subscriber};
pub trait BatchSubscriber: Subscriber {
type Batch: IntoIterator<Item = <Self as Subscriber>::Message> + Send;
fn batches(
&mut self,
) -> impl Stream<Item = Result<Self::Batch, <Self as Subscriber>::Error>> + Send + '_;
}
pub trait Seekable: Subscriber {
type Seeker: Seeker;
fn seeker(&self) -> Self::Seeker;
}
pub trait Seeker: Clone + Send + Sync + 'static {
type Position: Send;
type Error: StdError + Send + Sync + 'static;
fn seek(&self, to: Self::Position) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
pub trait Positioned: IncomingMessage {
type Position: Send;
fn position(&self) -> Self::Position;
}
pub trait TransactionalPublisher: Publisher {
fn begin_transaction(&self) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn commit(&self) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn abort(&self) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[must_use = "a transaction does nothing until settled with commit() or abort()"]
pub trait Transaction: Send {
type Error: StdError + Send + Sync + 'static;
fn publish(
&mut self,
msg: OutgoingMessage<'_>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn commit(self) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn abort(self) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
pub trait OwnedTransactions: Publisher {
type Transaction: Transaction;
fn transaction(&self) -> impl Future<Output = Result<Self::Transaction, Self::Error>> + Send;
}
pub trait RequestReply: Publisher {
type Reply: IncomingMessage;
fn request(
&self,
msg: OutgoingMessage<'_>,
timeout: Duration,
) -> impl Future<Output = Result<Self::Reply, Self::Error>> + Send;
}
pub trait Partitioned {
fn partition_key(&self) -> Option<&[u8]>;
}
pub trait Subscribe: ConnectedBroker {
type Subscriber: Subscriber;
fn subscribe(
&self,
name: &str,
) -> impl Future<Output = Result<Self::Subscriber, Self::Error>> + Send;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServerSpec {
pub host: Option<String>,
pub protocol: String,
pub description: Option<String>,
pub security: Vec<SecurityScheme>,
}
impl ServerSpec {
#[must_use]
pub fn new(host: impl Into<String>, protocol: impl Into<String>) -> Self {
Self {
host: Some(host.into()),
protocol: protocol.into(),
description: None,
security: Vec::new(),
}
}
#[must_use]
pub fn in_process(protocol: impl Into<String>) -> Self {
Self {
host: None,
protocol: protocol.into(),
description: None,
security: Vec::new(),
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_security(mut self, scheme: SecurityScheme) -> Self {
self.security.push(scheme);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecurityScheme {
pub(crate) kind: SecuritySchemeKind,
pub(crate) description: Option<String>,
}
#[cfg_attr(not(feature = "asyncapi"), allow(dead_code))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SecuritySchemeKind {
UserPassword,
ApiKey {
location: ApiKeyLocation,
},
X509,
Plain,
ScramSha256,
ScramSha512,
Gssapi,
Http {
scheme: String,
},
HttpApiKey {
name: String,
location: HttpApiKeyLocation,
},
OpenIdConnect {
url: String,
},
Oauth2 {
flows: String,
},
Custom {
object: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiKeyLocation {
User,
Password,
}
impl ApiKeyLocation {
#[cfg_attr(not(feature = "asyncapi"), allow(dead_code))]
pub(crate) fn as_api(self) -> &'static str {
match self {
Self::User => "user",
Self::Password => "password",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpApiKeyLocation {
Query,
Header,
Cookie,
}
impl HttpApiKeyLocation {
#[cfg_attr(not(feature = "asyncapi"), allow(dead_code))]
pub(crate) fn as_api(self) -> &'static str {
match self {
Self::Query => "query",
Self::Header => "header",
Self::Cookie => "cookie",
}
}
}
impl SecurityScheme {
fn of(kind: SecuritySchemeKind) -> Self {
Self {
kind,
description: None,
}
}
#[must_use]
pub fn user_password() -> Self {
Self::of(SecuritySchemeKind::UserPassword)
}
#[must_use]
pub fn api_key(location: ApiKeyLocation) -> Self {
Self::of(SecuritySchemeKind::ApiKey { location })
}
#[must_use]
pub fn x509() -> Self {
Self::of(SecuritySchemeKind::X509)
}
#[must_use]
pub fn plain() -> Self {
Self::of(SecuritySchemeKind::Plain)
}
#[must_use]
pub fn scram_sha256() -> Self {
Self::of(SecuritySchemeKind::ScramSha256)
}
#[must_use]
pub fn scram_sha512() -> Self {
Self::of(SecuritySchemeKind::ScramSha512)
}
#[must_use]
pub fn gssapi() -> Self {
Self::of(SecuritySchemeKind::Gssapi)
}
#[must_use]
pub fn http(scheme: impl Into<String>) -> Self {
Self::of(SecuritySchemeKind::Http {
scheme: scheme.into(),
})
}
#[must_use]
pub fn http_api_key(name: impl Into<String>, location: HttpApiKeyLocation) -> Self {
Self::of(SecuritySchemeKind::HttpApiKey {
name: name.into(),
location,
})
}
#[must_use]
pub fn open_id_connect(url: impl Into<String>) -> Self {
Self::of(SecuritySchemeKind::OpenIdConnect { url: url.into() })
}
#[cfg(feature = "json")]
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn oauth2(flows: serde_json::Value) -> Self {
Self::of(SecuritySchemeKind::Oauth2 {
flows: flows.to_string(),
})
}
#[cfg(feature = "json")]
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn custom(object: serde_json::Value) -> Self {
Self::of(SecuritySchemeKind::Custom {
object: object.to_string(),
})
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
pub trait DescribeServer: Broker {
fn describe_server(&self) -> ServerSpec;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_security_accumulates_schemes_in_order() {
let spec = ServerSpec::new("kafka.example.com:9093", "kafka")
.with_security(SecurityScheme::scram_sha512())
.with_security(SecurityScheme::x509());
assert_eq!(spec.security.len(), 2);
assert_eq!(spec.security[0].kind, SecuritySchemeKind::ScramSha512);
assert_eq!(spec.security[1].kind, SecuritySchemeKind::X509);
}
#[test]
fn in_process_spec_starts_without_security() {
let spec = ServerSpec::in_process("memory");
assert!(spec.security.is_empty());
}
#[test]
fn constructors_produce_their_kind() {
let cases = [
(
SecurityScheme::user_password(),
SecuritySchemeKind::UserPassword,
),
(
SecurityScheme::api_key(ApiKeyLocation::User),
SecuritySchemeKind::ApiKey {
location: ApiKeyLocation::User,
},
),
(SecurityScheme::x509(), SecuritySchemeKind::X509),
(SecurityScheme::plain(), SecuritySchemeKind::Plain),
(
SecurityScheme::scram_sha256(),
SecuritySchemeKind::ScramSha256,
),
(
SecurityScheme::scram_sha512(),
SecuritySchemeKind::ScramSha512,
),
(SecurityScheme::gssapi(), SecuritySchemeKind::Gssapi),
(
SecurityScheme::http("bearer"),
SecuritySchemeKind::Http {
scheme: "bearer".into(),
},
),
(
SecurityScheme::http_api_key("X-Api-Key", HttpApiKeyLocation::Header),
SecuritySchemeKind::HttpApiKey {
name: "X-Api-Key".into(),
location: HttpApiKeyLocation::Header,
},
),
(
SecurityScheme::open_id_connect("https://idp.example.com/.well-known"),
SecuritySchemeKind::OpenIdConnect {
url: "https://idp.example.com/.well-known".into(),
},
),
];
for (scheme, kind) in cases {
assert_eq!(scheme.kind, kind);
assert_eq!(scheme.description, None);
}
}
#[cfg(feature = "json")]
#[test]
fn json_backed_constructors_serialize_their_payload() {
let oauth2 = SecurityScheme::oauth2(serde_json::json!({ "clientCredentials": {} }));
assert_eq!(
oauth2.kind,
SecuritySchemeKind::Oauth2 {
flows: r#"{"clientCredentials":{}}"#.into(),
}
);
let custom = SecurityScheme::custom(serde_json::json!({ "type": "symmetricEncryption" }));
assert_eq!(
custom.kind,
SecuritySchemeKind::Custom {
object: r#"{"type":"symmetricEncryption"}"#.into(),
}
);
}
#[test]
fn with_description_sets_the_description() {
let scheme = SecurityScheme::plain().with_description("SASL over TLS");
assert_eq!(scheme.description.as_deref(), Some("SASL over TLS"));
}
#[test]
fn api_key_locations_map_to_document_values() {
assert_eq!(ApiKeyLocation::User.as_api(), "user");
assert_eq!(ApiKeyLocation::Password.as_api(), "password");
}
#[test]
fn http_api_key_locations_map_to_document_values() {
assert_eq!(HttpApiKeyLocation::Query.as_api(), "query");
assert_eq!(HttpApiKeyLocation::Header.as_api(), "header");
assert_eq!(HttpApiKeyLocation::Cookie.as_api(), "cookie");
}
}