use crate::router::MethodKind;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StreamType {
Unary,
ClientStream,
ServerStream,
BidiStream,
}
impl From<MethodKind> for StreamType {
fn from(kind: MethodKind) -> Self {
match kind {
MethodKind::Unary => Self::Unary,
MethodKind::ClientStreaming => Self::ClientStream,
MethodKind::ServerStreaming => Self::ServerStream,
MethodKind::BidiStreaming => Self::BidiStream,
}
}
}
impl From<StreamType> for MethodKind {
fn from(st: StreamType) -> Self {
match st {
StreamType::Unary => Self::Unary,
StreamType::ClientStream => Self::ClientStreaming,
StreamType::ServerStream => Self::ServerStreaming,
StreamType::BidiStream => Self::BidiStreaming,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum IdempotencyLevel {
#[default]
Unknown,
NoSideEffects,
Idempotent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SpecOrigin {
Server,
Client,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Spec {
pub procedure: &'static str,
pub stream_type: StreamType,
pub origin: SpecOrigin,
pub idempotency_level: IdempotencyLevel,
}
impl Spec {
pub const fn server(procedure: &'static str, stream_type: StreamType) -> Self {
debug_assert_well_formed(procedure);
Self {
procedure,
stream_type,
origin: SpecOrigin::Server,
idempotency_level: IdempotencyLevel::Unknown,
}
}
pub const fn client(procedure: &'static str, stream_type: StreamType) -> Self {
debug_assert_well_formed(procedure);
Self {
procedure,
stream_type,
origin: SpecOrigin::Client,
idempotency_level: IdempotencyLevel::Unknown,
}
}
#[must_use]
pub const fn with_idempotency_level(mut self, idempotency_level: IdempotencyLevel) -> Self {
self.idempotency_level = idempotency_level;
self
}
#[must_use]
pub const fn with_origin(mut self, origin: SpecOrigin) -> Self {
self.origin = origin;
self
}
#[must_use]
pub fn same_method(self, other: Spec) -> bool {
self.procedure == other.procedure
}
pub fn service(&self) -> &'static str {
let p = self.procedure.trim_start_matches('/');
p.rsplit_once('/').map(|(svc, _)| svc).unwrap_or(p)
}
pub fn method(&self) -> &'static str {
let p = self.procedure.trim_start_matches('/');
p.rsplit_once('/').map(|(_, m)| m).unwrap_or(p)
}
}
const fn debug_assert_well_formed(procedure: &str) {
if cfg!(debug_assertions) {
assert!(
procedure_is_well_formed(procedure),
"Spec procedure must start with '/' and contain a '/Service/Method' separator (e.g. \"/pkg.Service/Method\")"
);
}
}
pub(crate) const fn procedure_is_well_formed(procedure: &str) -> bool {
let bytes = procedure.as_bytes();
if bytes.is_empty() || bytes[0] != b'/' {
return false;
}
let mut i = 1;
while i < bytes.len() {
if bytes[i] == b'/' {
return true;
}
i += 1;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_type_round_trips_method_kind() {
for kind in [
MethodKind::Unary,
MethodKind::ServerStreaming,
MethodKind::ClientStreaming,
MethodKind::BidiStreaming,
] {
assert_eq!(MethodKind::from(StreamType::from(kind)), kind);
}
}
#[test]
fn spec_const_construction_and_accessors() {
const SPEC: Spec = Spec::server("/pkg.Greet/Say", StreamType::Unary)
.with_idempotency_level(IdempotencyLevel::NoSideEffects);
assert_eq!(SPEC.procedure, "/pkg.Greet/Say");
assert_eq!(SPEC.service(), "pkg.Greet");
assert_eq!(SPEC.method(), "Say");
assert_eq!(SPEC.stream_type, StreamType::Unary);
assert_eq!(SPEC.idempotency_level, IdempotencyLevel::NoSideEffects);
const { assert!(matches!(SPEC.origin, SpecOrigin::Server)) };
}
#[test]
fn procedure_well_formedness() {
assert!(procedure_is_well_formed("/pkg.Svc/M"));
assert!(procedure_is_well_formed("/Svc/M"));
assert!(!procedure_is_well_formed("pkg.Svc/M"), "no leading slash");
assert!(
!procedure_is_well_formed("/pkg.SvcM"),
"no method separator"
);
assert!(!procedure_is_well_formed(""));
assert!(!procedure_is_well_formed("/"));
}
#[test]
fn same_method_ignores_origin() {
const SERVER: Spec = Spec::server("/pkg.Greet/Say", StreamType::Unary)
.with_idempotency_level(IdempotencyLevel::NoSideEffects);
const CLIENT: Spec = SERVER.with_origin(SpecOrigin::Client);
const OTHER: Spec = Spec::client("/pkg.Greet/Shout", StreamType::Unary);
assert_eq!(
CLIENT,
Spec::client("/pkg.Greet/Say", StreamType::Unary)
.with_idempotency_level(IdempotencyLevel::NoSideEffects)
);
assert_ne!(SERVER, CLIENT, "PartialEq includes origin");
assert!(SERVER.same_method(CLIENT));
assert!(CLIENT.same_method(SERVER));
assert!(!CLIENT.same_method(OTHER));
}
#[test]
fn spec_client_const_construction() {
const SPEC: Spec = Spec::client("/pkg.Greet/Say", StreamType::Unary);
assert_eq!(SPEC.origin, SpecOrigin::Client);
assert_eq!(SPEC.idempotency_level, IdempotencyLevel::Unknown);
}
#[test]
fn spec_defaults() {
let s = Spec::server("/a.B/C", StreamType::BidiStream);
assert_eq!(s.idempotency_level, IdempotencyLevel::Unknown);
assert_eq!(s.origin, SpecOrigin::Server);
}
#[test]
#[cfg_attr(
debug_assertions,
should_panic(expected = "contain a '/Service/Method' separator")
)]
fn spec_malformed_path_no_method_separator_debug_asserts() {
let _ = Spec::server("/nopath", StreamType::Unary);
}
#[test]
#[cfg_attr(
debug_assertions,
should_panic(expected = "Spec procedure must start with '/'")
)]
fn spec_malformed_path_no_leading_slash_debug_asserts() {
let _ = Spec::server("pkg.Service/Method", StreamType::Unary);
}
#[test]
#[cfg(not(debug_assertions))]
fn spec_service_method_no_separator_release_fallback() {
let s = Spec::server("/nopath", StreamType::Unary);
assert_eq!(s.service(), "nopath");
assert_eq!(s.method(), "nopath");
}
}