use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct Scope {
pub namespace: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_id: Option<String>,
}
impl Scope {
pub fn new(namespace: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
domain: None,
workspace_id: None,
repo_id: None,
}
}
pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self
}
pub fn with_workspace(mut self, id: impl Into<String>) -> Self {
self.workspace_id = Some(id.into());
self
}
pub fn with_repo(mut self, id: impl Into<String>) -> Self {
self.repo_id = Some(id.into());
self
}
pub fn key(&self) -> ScopeKey {
ScopeKey {
namespace: self.namespace.clone(),
domain: self.domain.clone(),
workspace_id: self.workspace_id.clone(),
repo_id: self.repo_id.clone(),
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
pub struct ScopeKey {
pub namespace: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_id: Option<String>,
}
impl ScopeKey {
pub fn namespace_only(ns: impl Into<String>) -> Self {
Self {
namespace: ns.into(),
domain: None,
workspace_id: None,
repo_id: None,
}
}
pub fn from_legacy_namespace(namespace: impl Into<String>) -> Self {
Self::namespace_only(namespace)
}
pub fn to_legacy_namespace(&self) -> &str {
&self.namespace
}
pub fn is_namespace_only(&self) -> bool {
self.domain.is_none() && self.workspace_id.is_none() && self.repo_id.is_none()
}
}
impl std::fmt::Display for ScopeKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.namespace)?;
if let Some(d) = &self.domain {
write!(f, "/{d}")?;
}
if let Some(w) = &self.workspace_id {
write!(f, "@{w}")?;
}
if let Some(r) = &self.repo_id {
write!(f, "#{r}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PhaseStatus {
Current,
Compatibility,
PhaseGated,
}
impl PhaseStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Current => "current",
Self::Compatibility => "compatibility",
Self::PhaseGated => "phase_gated",
}
}
}
impl std::fmt::Display for PhaseStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scope_key_equality() {
let s1 = Scope::new("ns").with_repo("repo-a");
let s2 = Scope::new("ns").with_repo("repo-a");
assert_eq!(s1.key(), s2.key());
}
#[test]
fn scope_key_inequality_different_repo() {
let s1 = Scope::new("ns").with_repo("repo-a");
let s2 = Scope::new("ns").with_repo("repo-b");
assert_ne!(s1.key(), s2.key());
}
#[test]
fn scope_key_display() {
let s = Scope::new("prod")
.with_domain("code")
.with_workspace("ws1")
.with_repo("myrepo");
assert_eq!(s.key().to_string(), "prod/code@ws1#myrepo");
}
#[test]
fn scope_key_display_namespace_only() {
let sk = ScopeKey::namespace_only("default");
assert_eq!(sk.to_string(), "default");
}
#[test]
fn legacy_namespace_roundtrip() {
let sk = ScopeKey::from_legacy_namespace("my-namespace");
assert_eq!(sk.to_legacy_namespace(), "my-namespace");
assert!(sk.is_namespace_only());
}
#[test]
fn non_namespace_only_scope() {
let sk = Scope::new("ns").with_domain("code").key();
assert!(!sk.is_namespace_only());
}
#[test]
fn scope_key_ordering() {
let a = ScopeKey::namespace_only("aaa");
let b = ScopeKey::namespace_only("bbb");
assert!(a < b);
}
#[test]
fn scope_key_serde_roundtrip() {
let sk = Scope::new("ns")
.with_domain("code")
.with_workspace("ws")
.key();
let json = serde_json::to_string(&sk).unwrap();
let back: ScopeKey = serde_json::from_str(&json).unwrap();
assert_eq!(back, sk);
}
#[test]
fn scope_key_serde_skips_none() {
let sk = ScopeKey::namespace_only("ns");
let json = serde_json::to_string(&sk).unwrap();
assert!(!json.contains("domain"));
assert!(!json.contains("workspace_id"));
assert!(!json.contains("repo_id"));
}
}