use std::collections::BTreeMap;
use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
use type_bridge_contract::managed_scope::SemanticProfileBinding;
use type_bridge_contract::migration::MigrationPlanFingerprint;
use type_bridge_contract::migration::{MigrationId, MigrationManifestDigest};
use type_bridge_contract::schema::ManagedSchemaState;
use type_bridge_contract::schema_lowering::SchemaLoweringProfileBinding;
use type_bridge_schema::SafetyClass;
use crate::manifest::{VerifiedSchemaMigrationManifest, verified_manifest_digest};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SafetyPolicyDecision {
Allow,
RequireApproval,
Reject,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MigrationSafetyPolicy {
decisions: BTreeMap<SafetyClass, SafetyPolicyDecision>,
}
impl MigrationSafetyPolicy {
pub fn default_policy() -> Self {
Self {
decisions: BTreeMap::from([
(SafetyClass::FormalOnly, SafetyPolicyDecision::Allow),
(SafetyClass::SchemaMetadata, SafetyPolicyDecision::Allow),
(SafetyClass::Additive, SafetyPolicyDecision::Allow),
(SafetyClass::Conditional, SafetyPolicyDecision::Allow),
(
SafetyClass::Destructive,
SafetyPolicyDecision::RequireApproval,
),
(SafetyClass::Opaque, SafetyPolicyDecision::RequireApproval),
(SafetyClass::BackfillRequired, SafetyPolicyDecision::Reject),
(SafetyClass::Unsupported, SafetyPolicyDecision::Reject),
]),
}
}
pub fn with_decision(
mut self,
class: SafetyClass,
decision: SafetyPolicyDecision,
) -> Result<Self, Diagnostic> {
match (class, decision) {
(SafetyClass::Destructive | SafetyClass::Opaque, SafetyPolicyDecision::Allow) => {
return Err(failure(
"migration_policy_forbidden_allow",
"destructive and opaque work cannot carry a standing allowance",
));
}
(SafetyClass::BackfillRequired | SafetyClass::Unsupported, decision)
if decision != SafetyPolicyDecision::Reject =>
{
return Err(failure(
"migration_policy_unresolvable_class",
"classes the manifest verifier refuses cannot be admitted by policy",
));
}
_ => {}
}
self.decisions.insert(class, decision);
Ok(self)
}
pub fn decision(&self, class: SafetyClass) -> SafetyPolicyDecision {
self.decisions
.get(&class)
.copied()
.unwrap_or(SafetyPolicyDecision::Reject)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MigrationApplyApproval {
id: MigrationId,
lowering_profile: SchemaLoweringProfileBinding,
manifest_digest: MigrationManifestDigest,
plan_fingerprint: MigrationPlanFingerprint,
safety: SafetyClass,
semantic_profile: SemanticProfileBinding,
source_state: ManagedSchemaState,
target_state: ManagedSchemaState,
}
impl MigrationApplyApproval {
pub fn for_manifest(manifest: &VerifiedSchemaMigrationManifest) -> Result<Self, Diagnostic> {
Self::for_transition(manifest, manifest.safety(), false)
}
pub fn for_rollback(
manifest: &VerifiedSchemaMigrationManifest,
rollback_safety: SafetyClass,
) -> Result<Self, Diagnostic> {
Self::for_transition(manifest, rollback_safety, true)
}
fn for_transition(
manifest: &VerifiedSchemaMigrationManifest,
safety: SafetyClass,
rollback: bool,
) -> Result<Self, Diagnostic> {
let (source_state, target_state) = if rollback {
(
manifest.target_state().clone(),
manifest.source_state().clone(),
)
} else {
(
manifest.source_state().clone(),
manifest.target_state().clone(),
)
};
Ok(Self {
id: manifest.id().clone(),
lowering_profile: manifest.lowering_profile().clone(),
manifest_digest: verified_manifest_digest(manifest)?,
plan_fingerprint: manifest.plan_fingerprint().clone(),
safety,
semantic_profile: manifest.semantic_profile().clone(),
source_state,
target_state,
})
}
pub const fn id(&self) -> &MigrationId {
&self.id
}
pub const fn safety(&self) -> SafetyClass {
self.safety
}
pub fn binds(&self, manifest: &VerifiedSchemaMigrationManifest) -> Result<bool, Diagnostic> {
Ok(self.safety == manifest.safety()
&& self.source_state == *manifest.source_state()
&& self.target_state == *manifest.target_state()
&& self.binds_identity(manifest)?)
}
pub fn binds_rollback(
&self,
manifest: &VerifiedSchemaMigrationManifest,
rollback_safety: SafetyClass,
) -> Result<bool, Diagnostic> {
Ok(self.safety == rollback_safety
&& self.source_state == *manifest.target_state()
&& self.target_state == *manifest.source_state()
&& self.binds_identity(manifest)?)
}
fn binds_identity(
&self,
manifest: &VerifiedSchemaMigrationManifest,
) -> Result<bool, Diagnostic> {
Ok(self.id == *manifest.id()
&& self.plan_fingerprint == *manifest.plan_fingerprint()
&& self.semantic_profile == *manifest.semantic_profile()
&& self.lowering_profile == *manifest.lowering_profile()
&& self.manifest_digest == verified_manifest_digest(manifest)?)
}
}
fn failure(code: &'static str, message: &'static str) -> Diagnostic {
Diagnostic::new(
DiagnosticCategory::InvalidContract,
DiagnosticCode::new(code).expect("static policy diagnostic code is canonical"),
message,
)
}