use serde::Serialize;
use super::diff::ScopeView;
use super::error::AdminError;
use crate::desired_state::{
LoadedRevision, ModelAlias, ModelEnablement, ModelError, ModelLifecycle, ModelOwner, Models,
OfferingId, ProjectId, ResourceScope, TenantId, WireFamily,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CatalogueRequest {
pub tenant: TenantId,
pub project: Option<ProjectId>,
pub filters: CatalogueFilters,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CatalogueFilters {
pub state: Option<ModelLifecycle>,
pub wire_family: Option<WireFamily>,
pub offering: Option<OfferingId>,
pub billable: Option<bool>,
}
impl CatalogueRequest {
pub const fn scope(&self) -> ResourceScope {
match self.project {
None => ResourceScope::Tenant(self.tenant),
Some(project) => ResourceScope::Project {
tenant: self.tenant,
project,
},
}
}
fn covers(&self, owner: ModelOwner) -> bool {
if owner.tenant != self.tenant {
return false;
}
match self.project {
None => owner.project.is_none(),
Some(project) => owner.project.is_none() || owner.project == Some(project),
}
}
fn admits(&self, entry: &ModelEnablement) -> bool {
let filters = &self.filters;
filters
.state
.is_none_or(|state| state == entry.body.state())
&& filters
.wire_family
.is_none_or(|family| family == entry.body.wire_family())
&& filters
.offering
.is_none_or(|offering| offering == entry.body.offering().offering)
&& filters
.billable
.is_none_or(|billable| billable == entry.body.billable_price().is_some())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum UnavailableReason {
Disabled,
Shadowed,
Unpriced,
Unaliased,
}
impl UnavailableReason {
pub const ALL: &'static [Self] = &[
Self::Disabled,
Self::Shadowed,
Self::Unpriced,
Self::Unaliased,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Disabled => "disabled",
Self::Shadowed => "shadowed",
Self::Unpriced => "unpriced",
Self::Unaliased => "unaliased",
}
}
}
impl Serialize for UnavailableReason {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PendingFact {
OfferingMetadata,
Availability,
}
impl PendingFact {
pub const ALL: &'static [Self] = &[Self::OfferingMetadata, Self::Availability];
pub const fn as_str(self) -> &'static str {
match self {
Self::OfferingMetadata => "offering-metadata",
Self::Availability => "availability",
}
}
}
impl Serialize for PendingFact {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CatalogueEntry {
pub offering: String,
pub catalog_snapshot: String,
pub enablement: String,
pub version: u64,
pub slug: String,
pub scope: ScopeView,
pub wire_family: &'static str,
pub state: &'static str,
pub effective: bool,
pub routable: bool,
pub billable: bool,
pub aliases: Vec<String>,
pub unavailable: Vec<UnavailableReason>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CatalogueAlias {
pub alias: String,
pub version: u64,
pub slug: String,
pub scope: ScopeView,
pub wire_family: &'static str,
pub state: &'static str,
pub targets: Vec<CatalogueAliasTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CatalogueAliasTarget {
pub enablement: String,
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CatalogueView {
#[serde(skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
pub scope: ScopeView,
pub entries: Vec<CatalogueEntry>,
pub aliases: Vec<CatalogueAlias>,
pub pending: Vec<PendingFact>,
}
impl CatalogueView {
pub fn of(
revision: Option<&LoadedRevision>,
request: &CatalogueRequest,
) -> Result<Self, AdminError> {
let Some(revision) = revision else {
return Ok(Self::empty(None, request));
};
let models = Models::of(revision.state()).map_err(|error| unreadable(revision, &error))?;
let mut entries = Vec::new();
for enablement in models.enablements() {
let owner = enablement.body.owner();
if !request.covers(owner) || !request.admits(enablement) {
continue;
}
let aliases = aliases_naming(&models, request, enablement);
let shadowed = request.project.is_some_and(|project| {
owner.project.is_none()
&& models
.override_for(request.tenant, project, enablement.body.offering().offering)
.is_some()
});
let mut unavailable = Vec::new();
if !enablement.body.is_enabled() {
unavailable.push(UnavailableReason::Disabled);
}
if shadowed {
unavailable.push(UnavailableReason::Shadowed);
}
if enablement.body.billable_price().is_none() {
unavailable.push(UnavailableReason::Unpriced);
}
if aliases.is_empty() {
unavailable.push(UnavailableReason::Unaliased);
}
entries.push(CatalogueEntry {
offering: enablement.body.offering().offering.to_string(),
catalog_snapshot: enablement.body.offering().snapshot.to_string(),
enablement: enablement.reference.id.to_string(),
version: enablement.reference.version.get(),
slug: enablement.slug.as_str().to_owned(),
scope: ScopeView::of(&enablement.body.scope()),
wire_family: enablement.body.wire_family().as_str(),
state: enablement.body.state().as_str(),
effective: !shadowed,
routable: unavailable.is_empty(),
billable: enablement.body.billable_price().is_some(),
aliases,
unavailable,
});
}
let aliases = models
.aliases()
.filter(|alias| in_scope(request, alias))
.map(|alias| CatalogueAlias {
alias: alias.reference.id.to_string(),
version: alias.reference.version.get(),
slug: alias.slug.as_str().to_owned(),
scope: ScopeView::of(&alias.body.scope()),
wire_family: alias.body.wire_family().as_str(),
state: alias.body.state().as_str(),
targets: alias
.body
.targets()
.iter()
.map(|target| CatalogueAliasTarget {
enablement: target.enablement.to_string(),
version: target.version.get(),
})
.collect(),
})
.collect();
Ok(Self {
revision: Some(revision.id().to_string()),
scope: ScopeView::of(&request.scope()),
entries,
aliases,
pending: PendingFact::ALL.to_vec(),
})
}
fn empty(revision: Option<String>, request: &CatalogueRequest) -> Self {
Self {
revision,
scope: ScopeView::of(&request.scope()),
entries: Vec::new(),
aliases: Vec::new(),
pending: PendingFact::ALL.to_vec(),
}
}
}
fn aliases_naming(
models: &Models,
request: &CatalogueRequest,
enablement: &ModelEnablement,
) -> Vec<String> {
let mut names: Vec<String> = models
.aliases()
.filter(|alias| in_scope(request, alias) && alias.body.is_enabled())
.filter(|alias| {
alias
.body
.targets()
.iter()
.any(|target| target.enablement == enablement.reference.id)
})
.map(|alias| alias.slug.as_str().to_owned())
.collect();
names.sort_unstable();
names.dedup();
names
}
fn in_scope(request: &CatalogueRequest, alias: &ModelAlias) -> bool {
alias.body.tenant() == request.tenant
&& request
.project
.is_none_or(|project| alias.body.project() == project)
}
fn unreadable(revision: &LoadedRevision, error: &ModelError) -> AdminError {
AdminError::RevisionUnreadable {
revision: Some(revision.id()),
detail: error.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::desired_state::fixtures::{
approved_price, blob_backed_catalog, candidate, catalog_reference, enablement_body,
offering_id, price, project, project_enablement, project_id, revision_id, tenant,
tenant_id, typed_alias,
};
use crate::desired_state::{
DesiredState, ExpectedRevision, ProjectId, RevisionManifest, Slug, TenantId,
};
fn published() -> LoadedRevision {
let acme = tenant_id(1);
let core = project_id(2);
let globex = tenant_id(3);
let their_core = project_id(4);
let catalog = blob_backed_catalog(5);
let rate = price(&acme, 6, "gpt-4o-rate");
let default = enablement_body(30, ModelOwner::tenant(acme), "gpt-4o")
.approving(approved_price(6))
.version(slug("gpt-4o"), catalog_reference());
let over = project_enablement(&acme, &core, 31, "gpt-4o");
let withdrawn = enablement_body(32, ModelOwner::tenant(acme), "gpt-4o-mini")
.transitioned(ModelLifecycle::Disabled)
.version(slug("gpt-4o-mini"), catalog_reference());
let alias = typed_alias(&acme, &core, 33, "fast", &[default.reference]);
let theirs = enablement_body(34, ModelOwner::tenant(globex), "gpt-4o")
.version(slug("gpt-4o"), catalog_reference());
let their_alias = typed_alias(&globex, &their_core, 35, "fast", &[theirs.reference]);
let mut state = DesiredState::new();
state.declare_blob(*catalog.body.blob().expect("a blob body"));
state
.insert(tenant(1, "acme"))
.and_then(|state| state.insert(project(&acme, 2, "core")))
.and_then(|state| state.insert(tenant(3, "globex")))
.and_then(|state| state.insert(project(&globex, 4, "core")))
.and_then(|state| state.insert(catalog))
.and_then(|state| state.insert(rate))
.and_then(|state| state.insert(default))
.and_then(|state| state.insert(over))
.and_then(|state| state.insert(withdrawn))
.and_then(|state| state.insert(alias))
.and_then(|state| state.insert(theirs))
.and_then(|state| state.insert(their_alias))
.expect("the fixture state is publishable");
loaded(state)
}
fn loaded(state: DesiredState) -> LoadedRevision {
let candidate = candidate(ExpectedRevision::Empty, "catalogue", state);
let manifest = RevisionManifest::of(
revision_id(9),
None,
std::time::SystemTime::UNIX_EPOCH,
&candidate,
)
.expect("the fixture state is publishable");
LoadedRevision::assemble(manifest, candidate.state).expect("a consistent revision")
}
fn slug(slug: &str) -> Slug {
Slug::parse(slug).expect("fixture slug")
}
fn read(tenant: TenantId, project: Option<ProjectId>) -> CatalogueView {
filtered(tenant, project, CatalogueFilters::default())
}
fn filtered(
tenant: TenantId,
project: Option<ProjectId>,
filters: CatalogueFilters,
) -> CatalogueView {
CatalogueView::of(
Some(&published()),
&CatalogueRequest {
tenant,
project,
filters,
},
)
.expect("the fixture revision is readable")
}
fn entry<'a>(view: &'a CatalogueView, slug: &str) -> &'a CatalogueEntry {
view.entries
.iter()
.find(|entry| entry.slug == slug)
.unwrap_or_else(|| panic!("an entry named {slug}, in {:?}", view.entries))
}
#[test]
fn a_tenant_read_is_isolated_and_explains_each_entry() {
let view = read(tenant_id(1), None);
assert_eq!(view.entries.len(), 2, "{:?}", view.entries);
assert!(
view.entries
.iter()
.all(|entry| entry.scope.tenant.as_deref() == Some(&tenant_id(1).to_string())),
"another tenant's enablement reached a tenant-scoped read: {:?}",
view.entries
);
let priced = entry(&view, "gpt-4o");
assert!(priced.routable);
assert!(priced.billable);
assert_eq!(priced.aliases, vec!["fast".to_owned()]);
assert!(priced.unavailable.is_empty());
let withdrawn = entry(&view, "gpt-4o-mini");
assert!(!withdrawn.routable);
assert_eq!(withdrawn.state, "disabled");
assert_eq!(
withdrawn.unavailable,
vec![
UnavailableReason::Disabled,
UnavailableReason::Unpriced,
UnavailableReason::Unaliased,
]
);
}
#[test]
fn a_tenant_read_does_not_enumerate_a_projects_overrides() {
let view = read(tenant_id(1), None);
assert!(
view.entries
.iter()
.all(|entry| entry.scope.kind == "tenant"),
"{:?}",
view.entries
);
}
#[test]
fn a_project_read_reports_the_override_beside_the_default_it_shadows() {
let view = read(tenant_id(1), Some(project_id(2)));
let shadowed = view
.entries
.iter()
.find(|entry| entry.scope.kind == "tenant" && entry.slug == "gpt-4o")
.expect("the tenant default");
assert!(!shadowed.effective);
assert!(!shadowed.routable);
assert_eq!(shadowed.unavailable, vec![UnavailableReason::Shadowed]);
let over = view
.entries
.iter()
.find(|entry| entry.scope.kind == "project")
.expect("the project override");
assert!(over.effective);
assert!(!over.billable);
assert_eq!(
over.unavailable,
vec![UnavailableReason::Unpriced, UnavailableReason::Unaliased]
);
}
#[test]
fn a_project_of_another_tenant_yields_nothing() {
let view = read(tenant_id(1), Some(project_id(4)));
assert!(
view.entries
.iter()
.all(|entry| entry.scope.kind == "tenant"),
"{:?}",
view.entries
);
}
#[test]
fn a_read_filters_by_lifecycle_offering_and_billability() {
let enabled = filtered(
tenant_id(1),
None,
CatalogueFilters {
state: Some(ModelLifecycle::Enabled),
..CatalogueFilters::default()
},
);
assert_eq!(
enabled
.entries
.iter()
.map(|entry| entry.slug.as_str())
.collect::<Vec<_>>(),
vec!["gpt-4o"]
);
let unbillable = filtered(
tenant_id(1),
None,
CatalogueFilters {
billable: Some(false),
..CatalogueFilters::default()
},
);
assert_eq!(
unbillable
.entries
.iter()
.map(|entry| entry.slug.as_str())
.collect::<Vec<_>>(),
vec!["gpt-4o-mini"]
);
let one = filtered(
tenant_id(1),
None,
CatalogueFilters {
offering: Some(offering_id("gpt-4o")),
..CatalogueFilters::default()
},
);
assert_eq!(one.entries.len(), 1);
assert_eq!(one.entries[0].slug, "gpt-4o");
assert_eq!(
one.entries[0].wire_family,
WireFamily::OpenaiChat.as_str(),
"an entry carries the wire family its targets are held to"
);
}
#[test]
fn a_read_projects_aliases_as_first_class_ordered_resources() {
let view = read(tenant_id(1), None);
assert_eq!(view.aliases.len(), 1);
let alias = &view.aliases[0];
assert_eq!(
alias.alias,
crate::desired_state::fixtures::resource_id(33).to_string()
);
assert_eq!(alias.version, 1);
assert_eq!(alias.slug, "fast");
assert_eq!(alias.scope.kind, "project");
assert_eq!(
alias.scope.tenant.as_deref(),
Some(tenant_id(1).to_string().as_str())
);
assert_eq!(
alias.scope.project.as_deref(),
Some(project_id(2).to_string().as_str())
);
assert_eq!(alias.wire_family, WireFamily::OpenaiChat.as_str());
assert_eq!(alias.state, ModelLifecycle::Enabled.as_str());
assert_eq!(
alias.targets,
vec![CatalogueAliasTarget {
enablement: crate::desired_state::fixtures::resource_id(30).to_string(),
version: 1,
}]
);
}
#[test]
fn a_project_read_only_projects_aliases_owned_by_that_project() {
let view = read(tenant_id(1), Some(project_id(2)));
assert_eq!(view.aliases.len(), 1);
assert_eq!(view.aliases[0].slug, "fast");
let other = read(tenant_id(1), Some(project_id(4)));
assert!(other.aliases.is_empty());
}
#[test]
fn an_entry_carries_the_catalogue_coordinates_it_was_enabled_against() {
let view = read(tenant_id(1), None);
let entry = entry(&view, "gpt-4o");
assert_eq!(entry.offering, offering_id("gpt-4o").to_string());
assert_eq!(
entry.catalog_snapshot,
crate::desired_state::fixtures::catalog_snapshot().to_string()
);
}
#[test]
fn a_read_names_the_facts_it_could_not_consult() {
assert_eq!(read(tenant_id(1), None).pending, PendingFact::ALL.to_vec());
}
#[test]
fn an_unpublished_control_plane_is_an_empty_catalogue() {
let request = CatalogueRequest {
tenant: tenant_id(1),
project: None,
filters: CatalogueFilters::default(),
};
let view = CatalogueView::of(None, &request).expect("an empty control plane is readable");
assert!(view.revision.is_none());
assert!(view.entries.is_empty());
assert!(view.aliases.is_empty());
assert_eq!(view.scope.kind, "tenant");
assert_eq!(view.pending, PendingFact::ALL.to_vec());
let stranger = read(tenant_id(7), None);
assert!(stranger.revision.is_some());
assert!(stranger.entries.is_empty());
}
#[test]
fn a_reason_and_a_pending_fact_serialize_as_their_wire_spelling() {
assert_eq!(
serde_json::to_value(UnavailableReason::ALL).expect("serializable"),
serde_json::json!(["disabled", "shadowed", "unpriced", "unaliased"])
);
assert_eq!(
serde_json::to_value(PendingFact::ALL).expect("serializable"),
serde_json::json!(["offering-metadata", "availability"])
);
}
}