use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "runtime")]
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[derive(Default)]
pub enum Scope {
#[default]
Global,
Namespace(String),
Service { namespace: String, service: String },
}
impl Scope {
pub fn is_global(&self) -> bool {
matches!(self, Scope::Global)
}
pub fn is_namespace(&self) -> bool {
matches!(self, Scope::Namespace(_))
}
pub fn is_service(&self) -> bool {
matches!(self, Scope::Service { .. })
}
pub fn namespace(&self) -> Option<&str> {
match self {
Scope::Global => None,
Scope::Namespace(ns) => Some(ns),
Scope::Service { namespace, .. } => Some(namespace),
}
}
pub fn service(&self) -> Option<&str> {
match self {
Scope::Service { service, .. } => Some(service),
_ => None,
}
}
pub fn parent(&self) -> Option<Scope> {
match self {
Scope::Global => None,
Scope::Namespace(_) => Some(Scope::Global),
Scope::Service { namespace, .. } => Some(Scope::Namespace(namespace.clone())),
}
}
pub fn chain(&self) -> Vec<Scope> {
let mut chain = vec![self.clone()];
let mut current = self.clone();
while let Some(parent) = current.parent() {
chain.push(parent.clone());
current = parent;
}
chain
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Scope::Global => write!(f, "global"),
Scope::Namespace(ns) => write!(f, "namespace:{}", ns),
Scope::Service { namespace, service } => {
write!(f, "service:{}:{}", namespace, service)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QualifiedId {
pub name: String,
pub scope: Scope,
}
impl QualifiedId {
pub fn new(name: impl Into<String>, scope: Scope) -> Self {
Self {
name: name.into(),
scope,
}
}
pub fn global(name: impl Into<String>) -> Self {
Self {
name: name.into(),
scope: Scope::Global,
}
}
pub fn namespaced(namespace: impl Into<String>, name: impl Into<String>) -> Self {
Self {
name: name.into(),
scope: Scope::Namespace(namespace.into()),
}
}
pub fn in_service(
namespace: impl Into<String>,
service: impl Into<String>,
name: impl Into<String>,
) -> Self {
Self {
name: name.into(),
scope: Scope::Service {
namespace: namespace.into(),
service: service.into(),
},
}
}
pub fn canonical(&self) -> String {
match &self.scope {
Scope::Global => self.name.clone(),
Scope::Namespace(ns) => format!("{}:{}", ns, self.name),
Scope::Service { namespace, service } => {
format!("{}:{}:{}", namespace, service, self.name)
}
}
}
pub fn parse(s: &str) -> Self {
let parts: Vec<&str> = s.splitn(3, ':').collect();
match parts.as_slice() {
[name] => Self::global(*name),
[namespace, name] => Self::namespaced(*namespace, *name),
[namespace, service, name] => Self::in_service(*namespace, *service, *name),
_ => Self::global(s), }
}
pub fn is_global(&self) -> bool {
self.scope.is_global()
}
pub fn is_qualified(&self) -> bool {
!self.scope.is_global()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn scope(&self) -> &Scope {
&self.scope
}
}
impl fmt::Display for QualifiedId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.canonical())
}
}
impl From<&str> for QualifiedId {
fn from(s: &str) -> Self {
Self::parse(s)
}
}
impl From<String> for QualifiedId {
fn from(s: String) -> Self {
Self::parse(&s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CorrelationId(String);
impl CorrelationId {
#[cfg(feature = "runtime")]
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
#[cfg(feature = "runtime")]
impl Default for CorrelationId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CorrelationId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for CorrelationId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for CorrelationId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RequestId(String);
impl RequestId {
#[cfg(feature = "runtime")]
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(feature = "runtime")]
impl Default for RequestId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for RequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RouteId(String);
impl RouteId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RouteId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UpstreamId(String);
impl UpstreamId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for UpstreamId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AgentId(String);
impl AgentId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AgentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_global() {
let scope = Scope::Global;
assert!(scope.is_global());
assert!(!scope.is_namespace());
assert!(!scope.is_service());
assert_eq!(scope.namespace(), None);
assert_eq!(scope.service(), None);
assert_eq!(scope.parent(), None);
}
#[test]
fn test_scope_namespace() {
let scope = Scope::Namespace("api".to_string());
assert!(!scope.is_global());
assert!(scope.is_namespace());
assert!(!scope.is_service());
assert_eq!(scope.namespace(), Some("api"));
assert_eq!(scope.service(), None);
assert_eq!(scope.parent(), Some(Scope::Global));
}
#[test]
fn test_scope_service() {
let scope = Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string(),
};
assert!(!scope.is_global());
assert!(!scope.is_namespace());
assert!(scope.is_service());
assert_eq!(scope.namespace(), Some("api"));
assert_eq!(scope.service(), Some("payments"));
assert_eq!(scope.parent(), Some(Scope::Namespace("api".to_string())));
}
#[test]
fn test_scope_chain() {
let service_scope = Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string(),
};
let chain = service_scope.chain();
assert_eq!(chain.len(), 3);
assert_eq!(
chain[0],
Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string()
}
);
assert_eq!(chain[1], Scope::Namespace("api".to_string()));
assert_eq!(chain[2], Scope::Global);
}
#[test]
fn test_scope_display() {
assert_eq!(Scope::Global.to_string(), "global");
assert_eq!(
Scope::Namespace("api".to_string()).to_string(),
"namespace:api"
);
assert_eq!(
Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string()
}
.to_string(),
"service:api:payments"
);
}
#[test]
fn test_qualified_id_global() {
let qid = QualifiedId::global("backend");
assert_eq!(qid.name(), "backend");
assert_eq!(qid.scope(), &Scope::Global);
assert_eq!(qid.canonical(), "backend");
assert!(qid.is_global());
assert!(!qid.is_qualified());
}
#[test]
fn test_qualified_id_namespaced() {
let qid = QualifiedId::namespaced("api", "backend");
assert_eq!(qid.name(), "backend");
assert_eq!(qid.scope(), &Scope::Namespace("api".to_string()));
assert_eq!(qid.canonical(), "api:backend");
assert!(!qid.is_global());
assert!(qid.is_qualified());
}
#[test]
fn test_qualified_id_service() {
let qid = QualifiedId::in_service("api", "payments", "checkout");
assert_eq!(qid.name(), "checkout");
assert_eq!(
qid.scope(),
&Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string()
}
);
assert_eq!(qid.canonical(), "api:payments:checkout");
assert!(!qid.is_global());
assert!(qid.is_qualified());
}
#[test]
fn test_qualified_id_parse_global() {
let qid = QualifiedId::parse("backend");
assert_eq!(qid.name(), "backend");
assert_eq!(qid.scope(), &Scope::Global);
}
#[test]
fn test_qualified_id_parse_namespaced() {
let qid = QualifiedId::parse("api:backend");
assert_eq!(qid.name(), "backend");
assert_eq!(qid.scope(), &Scope::Namespace("api".to_string()));
}
#[test]
fn test_qualified_id_parse_service() {
let qid = QualifiedId::parse("api:payments:checkout");
assert_eq!(qid.name(), "checkout");
assert_eq!(
qid.scope(),
&Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string()
}
);
}
#[test]
fn test_qualified_id_parse_with_extra_colons() {
let qid = QualifiedId::parse("api:payments:item:with:colons");
assert_eq!(qid.name(), "item:with:colons");
assert_eq!(
qid.scope(),
&Scope::Service {
namespace: "api".to_string(),
service: "payments".to_string()
}
);
}
#[test]
fn test_qualified_id_from_str() {
let qid: QualifiedId = "api:backend".into();
assert_eq!(qid.canonical(), "api:backend");
}
#[test]
fn test_qualified_id_display() {
let qid = QualifiedId::in_service("ns", "svc", "resource");
assert_eq!(qid.to_string(), "ns:svc:resource");
}
#[test]
fn test_qualified_id_equality() {
let qid1 = QualifiedId::namespaced("api", "backend");
let qid2 = QualifiedId::parse("api:backend");
assert_eq!(qid1, qid2);
}
#[test]
fn test_qualified_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(QualifiedId::global("backend"));
set.insert(QualifiedId::namespaced("api", "backend"));
set.insert(QualifiedId::in_service("api", "svc", "backend"));
assert_eq!(set.len(), 3);
assert!(set.contains(&QualifiedId::parse("api:backend")));
}
#[test]
#[cfg(feature = "runtime")]
fn test_correlation_id() {
let id1 = CorrelationId::new();
let id2 = CorrelationId::from_string("test-id");
assert_ne!(id1, id2);
assert_eq!(id2.as_str(), "test-id");
}
#[test]
fn test_route_id() {
let id = RouteId::new("my-route");
assert_eq!(id.as_str(), "my-route");
assert_eq!(id.to_string(), "my-route");
}
#[test]
fn test_upstream_id() {
let id = UpstreamId::new("backend-pool");
assert_eq!(id.as_str(), "backend-pool");
}
#[test]
fn test_agent_id() {
let id = AgentId::new("waf-agent");
assert_eq!(id.as_str(), "waf-agent");
}
}