use super::common::{Description, LifecycleStatus, ObjectRef};
use super::validation::{require_non_empty, require_some};
use crate::text::normalize_required_text;
use crate::{Confidence, CoreError, Id, Provenance, Result};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CriterionDirection {
Maximize,
Minimize,
Preserve,
Avoid,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ValuationCriterion {
pub criterion_id: String,
pub name: String,
pub direction: CriterionDirection,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
pub required: bool,
}
impl ValuationCriterion {
fn validate(&self) -> Result<()> {
normalize_required_text("criteria.criterion_id", &self.criterion_id)?;
normalize_required_text("criteria.name", &self.name)?;
if let Some(weight) = self.weight {
if !weight.is_finite() || weight < 0.0 {
return Err(CoreError::malformed_field(
"criteria.weight",
"weight must be finite and non-negative",
));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderType {
ScalarScore,
LexicographicOrder,
PartialOrder,
ParetoFrontier,
ThresholdAcceptance,
QualitativeRanking,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct CriterionValue {
pub criterion_id: String,
pub value: ValuationValue,
pub evidence: Id,
}
impl CriterionValue {
fn validate(&self) -> Result<()> {
normalize_required_text("values.criterion_id", &self.criterion_id)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
pub enum ValuationValue {
Text(String),
Number(f64),
Boolean(bool),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Tradeoff {
pub gains: String,
pub losses: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affected_invariants: Vec<Id>,
}
impl Tradeoff {
fn validate(&self) -> Result<()> {
normalize_required_text("tradeoffs.gains", &self.gains)?;
normalize_required_text("tradeoffs.losses", &self.losses)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Valuation {
pub id: Id,
pub target: ObjectRef,
#[serde(skip_serializing_if = "Option::is_none")]
pub valuation_context: Option<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub criteria: Vec<ValuationCriterion>,
pub order_type: OrderType,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub values: Vec<CriterionValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tradeoffs: Vec<Tradeoff>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub incomparable_with: Vec<Id>,
pub confidence: Confidence,
pub provenance: Provenance,
pub review_status: LifecycleStatus,
}
impl Valuation {
pub fn validate_for_decision(&self) -> Result<()> {
require_some("valuation_context", self.valuation_context.as_ref())?;
require_non_empty("criteria", &self.criteria)?;
require_non_empty("values", &self.values)?;
for criterion in &self.criteria {
criterion.validate()?;
}
for value in &self.values {
value.validate()?;
}
for tradeoff in &self.tradeoffs {
tradeoff.validate()?;
}
if !self.incomparable_with.is_empty()
&& matches!(
self.order_type,
OrderType::ScalarScore | OrderType::LexicographicOrder
)
{
return Err(CoreError::malformed_field(
"incomparable_with",
"incomparable valuations must not be projected as a single ranking",
));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SchemaMappingKind {
Rename,
Split,
Merge,
Refinement,
Abstraction,
Deprecation,
SemanticRedefinition,
Custom,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SchemaCompatibility {
BackwardCompatible,
ForwardCompatible,
Lossy,
Incompatible,
Unknown,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SchemaMapping {
pub source_ref: String,
pub target_ref: String,
pub mapping_rule: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preservation_claims: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub loss_claims: Vec<Description>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_reviews: Vec<Id>,
}
impl SchemaMapping {
fn validate(&self) -> Result<()> {
normalize_required_text("mappings.source_ref", &self.source_ref)?;
normalize_required_text("mappings.target_ref", &self.target_ref)?;
normalize_required_text("mappings.mapping_rule", &self.mapping_rule)?;
Ok(())
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SchemaVerification {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checks: Vec<Id>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub witnesses: Vec<Id>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SchemaMorphism {
pub id: Id,
pub source_schema: Id,
pub target_schema: Id,
pub source_interpretation_package: Id,
pub target_interpretation_package: Id,
pub mapping_kind: SchemaMappingKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mappings: Vec<SchemaMapping>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub affected_objects: Vec<ObjectRef>,
pub compatibility: SchemaCompatibility,
pub verification: SchemaVerification,
pub provenance: Provenance,
pub review_status: LifecycleStatus,
}
impl SchemaMorphism {
pub fn validate_application(&self) -> Result<()> {
require_non_empty("mappings", &self.mappings)?;
for mapping in &self.mappings {
mapping.validate()?;
}
if self.mapping_kind == SchemaMappingKind::SemanticRedefinition
&& self.review_status != LifecycleStatus::Accepted
{
return Err(CoreError::malformed_field(
"review_status",
"semantic redefinition requires explicit accepted review",
));
}
if matches!(
self.mapping_kind,
SchemaMappingKind::Split | SchemaMappingKind::Merge
) {
require_non_empty("affected_objects", &self.affected_objects)?;
if self
.mappings
.iter()
.all(|mapping| mapping.loss_claims.is_empty())
{
return Err(CoreError::malformed_field(
"mappings.loss_claims",
"split or merge schema mapping requires loss claims",
));
}
}
if self.compatibility == SchemaCompatibility::Incompatible {
return Err(CoreError::malformed_field(
"compatibility",
"incompatible schema morphism cannot be applied as a migration",
));
}
if self.compatibility == SchemaCompatibility::Lossy
&& self
.mappings
.iter()
.all(|mapping| mapping.loss_claims.is_empty())
{
return Err(CoreError::malformed_field(
"mappings.loss_claims",
"lossy schema morphism requires explicit loss claims",
));
}
Ok(())
}
}