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
}
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) {
let bytes = procedure.as_bytes();
assert!(
!bytes.is_empty() && bytes[0] == b'/',
"Spec procedure must start with '/' (e.g. \"/pkg.Service/Method\")"
);
let mut has_inner_slash = false;
let mut i = 1;
while i < bytes.len() {
if bytes[i] == b'/' {
has_inner_slash = true;
break;
}
i += 1;
}
assert!(
has_inner_slash,
"Spec procedure must contain a '/Service/Method' separator (e.g. \"/pkg.Service/Method\")"
);
}
}
#[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 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 = "Spec procedure must 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");
}
}