use std::collections::HashMap;
use crate::config::{Config, ConfigError};
use crate::desired_state::{DesiredState, LoadedRevision, ResourceRef, RevisionId};
use crate::state::{ConfigSnapshot, SnapshotError};
pub trait RevisionProjection: Send + Sync {
fn name(&self) -> &'static str;
fn project(&self, bootstrap: &Config, state: &DesiredState) -> Result<Config, ProjectionError>;
}
#[derive(Debug, thiserror::Error)]
pub enum ProjectionError {
#[error("desired state does not describe a servable deployment: {detail}")]
Incomplete { detail: String },
#[error("{reference} carries a body this build cannot read: {detail}")]
Body {
reference: ResourceRef,
detail: String,
},
#[error("secret `{reference}` referenced by {holder} could not be resolved: {detail}")]
Secret {
holder: ResourceRef,
reference: String,
detail: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum CompileError {
#[error("revision {revision} does not project onto a servable configuration: {source}")]
Projection {
revision: RevisionId,
#[source]
source: ProjectionError,
},
#[error("revision {revision} fails the configuration gate boot applies: {source}")]
Validation {
revision: RevisionId,
#[source]
source: ConfigError,
},
#[error("revision {revision} could not be compiled into a runtime snapshot: {source}")]
Snapshot {
revision: RevisionId,
#[source]
source: SnapshotError,
},
}
impl CompileError {
pub const fn revision(&self) -> RevisionId {
match self {
Self::Projection { revision, .. }
| Self::Validation { revision, .. }
| Self::Snapshot { revision, .. } => *revision,
}
}
pub const fn reason(&self) -> &'static str {
match self {
Self::Projection {
source: ProjectionError::Secret { .. },
..
} => "secret",
Self::Projection { .. } => "projection",
Self::Validation { .. } => "validation",
Self::Snapshot { .. } => "snapshot",
}
}
}
pub trait CandidateCompiler: Send + Sync {
fn compile(
&self,
revision: &LoadedRevision,
generation: u64,
) -> Result<ConfigSnapshot, CompileError>;
}
pub struct RevisionCompiler<P> {
bootstrap: Config,
env: HashMap<String, String>,
projection: P,
}
impl<P: RevisionProjection> RevisionCompiler<P> {
pub const fn new(bootstrap: Config, env: HashMap<String, String>, projection: P) -> Self {
Self {
bootstrap,
env,
projection,
}
}
pub fn projection_name(&self) -> &'static str {
self.projection.name()
}
}
impl<P: RevisionProjection> CandidateCompiler for RevisionCompiler<P> {
fn compile(
&self,
revision: &LoadedRevision,
generation: u64,
) -> Result<ConfigSnapshot, CompileError> {
let id = revision.id();
let config = self
.projection
.project(&self.bootstrap, revision.state())
.map_err(|source| CompileError::Projection {
revision: id,
source,
})?;
config
.validate_compiled()
.map_err(|source| CompileError::Validation {
revision: id,
source,
})?;
ConfigSnapshot::build(config, &self.env, generation).map_err(|source| {
CompileError::Snapshot {
revision: id,
source,
}
})
}
}
#[cfg(test)]
pub(crate) mod testing {
use super::*;
use crate::desired_state::fixtures;
pub(crate) struct AliasProjection {
pub(crate) provider: &'static str,
}
impl RevisionProjection for AliasProjection {
fn name(&self) -> &'static str {
"test-alias"
}
fn project(
&self,
bootstrap: &Config,
state: &DesiredState,
) -> Result<Config, ProjectionError> {
let mut config = bootstrap.clone();
for resource in state.resources() {
if resource.reference.kind != crate::desired_state::ResourceKind::Alias {
continue;
}
config.model.push(crate::config::Model {
name: resource.slug.as_str().to_owned(),
targets: vec![crate::config::Target {
provider: self.provider.to_owned(),
model: "gpt-4o".to_owned(),
price: gateway_core::catalog::ModelPrice {
input_microdollars_per_million: 1,
output_microdollars_per_million: 1,
reasoning_microdollars_per_million: None,
cache_read_microdollars_per_million: None,
cache_write_microdollars_per_million: None,
},
}],
});
}
Ok(config)
}
}
pub(crate) struct RefusingProjection;
impl RevisionProjection for RefusingProjection {
fn name(&self) -> &'static str {
"test-refusing"
}
fn project(&self, _: &Config, _: &DesiredState) -> Result<Config, ProjectionError> {
Err(ProjectionError::Incomplete {
detail: "no tenant is enabled".to_owned(),
})
}
}
pub(crate) fn bootstrap() -> Config {
Config::from_toml_str(
r#"
[[namespace]]
id = "platform"
default = true
[[provider]]
id = "openai"
kind = "openai"
base_url = "https://api.openai.com/v1"
[[gateway_key]]
env = "AXOND_KEY"
namespace = "platform"
"#,
)
.expect("a valid bootstrap config")
}
pub(crate) fn env() -> HashMap<String, String> {
HashMap::from([("AXOND_KEY".to_owned(), "inbound-secret".to_owned())])
}
pub(crate) fn revision() -> LoadedRevision {
let candidate = fixtures::candidate(
crate::desired_state::ExpectedRevision::Empty,
"first",
fixtures::state(),
);
let manifest = crate::desired_state::RevisionManifest::of(
fixtures::revision_id(9),
None,
std::time::SystemTime::UNIX_EPOCH,
&candidate,
)
.expect("a valid manifest");
LoadedRevision::assemble(manifest, candidate.state).expect("a consistent revision")
}
}
#[cfg(test)]
mod tests {
use super::testing::{AliasProjection, RefusingProjection, bootstrap, env, revision};
use super::*;
use crate::desired_state::fixtures;
#[test]
fn a_projected_revision_compiles_into_a_snapshot_at_the_requested_generation() {
let compiler =
RevisionCompiler::new(bootstrap(), env(), AliasProjection { provider: "openai" });
let snapshot = compiler
.compile(&revision(), 7)
.expect("the projected config is servable");
assert_eq!(snapshot.generation, 7);
assert!(
snapshot
.config
.model
.iter()
.any(|model| model.name == "fast")
);
}
#[test]
fn a_revision_whose_alias_targets_an_undefined_provider_is_refused_by_the_boot_gate() {
let compiler = RevisionCompiler::new(
bootstrap(),
env(),
AliasProjection {
provider: "nonexistent",
},
);
let error = compiler
.compile(&revision(), 1)
.err()
.expect("an undefined target cannot be served");
assert_eq!(error.reason(), "validation");
assert!(error.to_string().contains("undefined provider"), "{error}");
}
#[test]
fn an_unreadable_revision_is_refused_before_any_configuration_is_built() {
let compiler = RevisionCompiler::new(bootstrap(), env(), RefusingProjection);
let error = compiler
.compile(&revision(), 1)
.err()
.expect("the projection refuses");
assert_eq!(error.reason(), "projection");
assert_eq!(error.revision(), fixtures::revision_id(9));
}
#[test]
fn unresolvable_secret_material_refuses_the_candidate_without_disclosing_it() {
let compiler = RevisionCompiler::new(
bootstrap(),
HashMap::new(),
AliasProjection { provider: "openai" },
);
let error = compiler
.compile(&revision(), 1)
.err()
.expect("an unresolvable gateway key cannot be published");
assert_eq!(error.reason(), "snapshot");
assert!(error.to_string().contains("AXOND_KEY"), "{error}");
}
}