use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const LOCAL_ONLY: &str = "local-only";
const ORG_TENANT: &str = "org-tenant";
const THIRD_PARTY_INDEX: &str = "third-party-index";
const THIRD_PARTY_MODEL: &str = "third-party-model";
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum EgressScope {
LocalOnly,
OrgTenant,
ThirdPartyIndex,
ThirdPartyModel,
Custom(String),
}
impl EgressScope {
pub fn as_str(&self) -> &str {
match self {
Self::LocalOnly => LOCAL_ONLY,
Self::OrgTenant => ORG_TENANT,
Self::ThirdPartyIndex => THIRD_PARTY_INDEX,
Self::ThirdPartyModel => THIRD_PARTY_MODEL,
Self::Custom(scope) => scope,
}
}
pub fn from_wire(scope: impl Into<String>) -> Self {
let scope = scope.into();
match scope.as_str() {
LOCAL_ONLY => Self::LocalOnly,
ORG_TENANT => Self::OrgTenant,
THIRD_PARTY_INDEX => Self::ThirdPartyIndex,
THIRD_PARTY_MODEL => Self::ThirdPartyModel,
_ => Self::Custom(scope),
}
}
pub fn is_base(&self) -> bool {
!matches!(self, Self::Custom(_))
}
pub fn is_off_machine(&self) -> bool {
!matches!(self, Self::LocalOnly)
}
pub fn is_valid(&self) -> bool {
match self {
Self::Custom(scope) => match scope.split_once(':') {
Some((namespace, name)) => !namespace.is_empty() && !name.is_empty(),
None => false,
},
_ => true,
}
}
}
impl fmt::Display for EgressScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for EgressScope {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for EgressScope {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let scope = String::deserialize(deserializer)?;
Ok(Self::from_wire(scope))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_scopes_round_trip_through_their_canonical_strings() {
for (scope, wire) in [
(EgressScope::LocalOnly, "local-only"),
(EgressScope::OrgTenant, "org-tenant"),
(EgressScope::ThirdPartyIndex, "third-party-index"),
(EgressScope::ThirdPartyModel, "third-party-model"),
] {
assert_eq!(scope.as_str(), wire);
let json = serde_json::to_string(&scope).unwrap();
assert_eq!(json, format!("\"{wire}\""));
let back: EgressScope = serde_json::from_str(&json).unwrap();
assert_eq!(back, scope);
assert!(scope.is_base() && scope.is_valid());
}
}
#[test]
fn a_custom_scope_round_trips_as_a_flat_string() {
let scope = EgressScope::Custom("acme:vector-store".into());
let json = serde_json::to_string(&scope).unwrap();
assert_eq!(json, "\"acme:vector-store\"");
let back: EgressScope = serde_json::from_str(&json).unwrap();
assert_eq!(back, scope);
assert!(!scope.is_base());
assert!(scope.is_valid());
}
#[test]
fn an_unknown_base_like_string_deserializes_to_custom_not_a_base_class() {
let back: EgressScope = serde_json::from_str("\"acme:special\"").unwrap();
assert_eq!(back, EgressScope::Custom("acme:special".into()));
}
#[test]
fn only_local_only_is_on_machine() {
assert!(!EgressScope::LocalOnly.is_off_machine());
assert!(EgressScope::OrgTenant.is_off_machine());
assert!(EgressScope::ThirdPartyIndex.is_off_machine());
assert!(EgressScope::ThirdPartyModel.is_off_machine());
assert!(EgressScope::Custom("acme:sink".into()).is_off_machine());
}
#[test]
fn a_non_namespaced_custom_scope_is_invalid() {
assert!(!EgressScope::Custom("notnamespaced".into()).is_valid());
assert!(!EgressScope::Custom(":no-namespace".into()).is_valid());
assert!(!EgressScope::Custom("no-name:".into()).is_valid());
assert!(EgressScope::Custom("ns:name".into()).is_valid());
}
}