use std::collections::BTreeMap;
use type_bridge_contract::capability::CapabilitySet;
use type_bridge_contract::codec::FormatVersion;
use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
use type_bridge_contract::fingerprint::SemanticProfileId;
use type_bridge_contract::schema::{
AnnotationKindId, DeclaredIdentityFingerprint, DeclaredSchema, DocumentId,
SchemaAnnotationValue, SchemaDiagnostic, SchemaDiagnostics, SchemaFact, SchemaFactId,
SourceSpan, SourcedSchemaFact,
};
use type_bridge_contract::semantic_profile::{InterfaceKind, SemanticProfile};
use crate::ManagedSchemaScope;
pub const OBSERVED_SCHEMA_CANONICALIZATION_VERSION: &str = "typebridge.observed-schema/v1";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ObservedFactProvenance {
Direct,
Inherited {
declared_fact: SchemaFactId,
},
ServerDefault,
Ambiguous,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ObservedFactScope {
Managed,
TypeBridgeInternal,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ObservedSchemaFact {
fact: SchemaFact,
provenance: ObservedFactProvenance,
scope: ObservedFactScope,
}
impl ObservedSchemaFact {
pub const fn new(
fact: SchemaFact,
provenance: ObservedFactProvenance,
scope: ObservedFactScope,
) -> Self {
Self {
fact,
provenance,
scope,
}
}
pub const fn fact(&self) -> &SchemaFact {
&self.fact
}
pub const fn provenance(&self) -> &ObservedFactProvenance {
&self.provenance
}
pub const fn scope(&self) -> ObservedFactScope {
self.scope
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ObservedSchema {
format: FormatVersion,
required_capabilities: CapabilitySet,
facts: Vec<ObservedSchemaFact>,
}
impl ObservedSchema {
#[must_use]
pub fn new(
format: FormatVersion,
required_capabilities: CapabilitySet,
facts: impl IntoIterator<Item = ObservedSchemaFact>,
) -> Self {
Self {
format,
required_capabilities,
facts: facts.into_iter().collect(),
}
}
pub const fn format(&self) -> FormatVersion {
self.format
}
pub const fn required_capabilities(&self) -> &CapabilitySet {
&self.required_capabilities
}
pub fn facts(&self) -> impl ExactSizeIterator<Item = &ObservedSchemaFact> {
self.facts.iter()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CanonicalObservedSchema {
direct_schema: DeclaredSchema,
managed_scope: ManagedSchemaScope,
semantic_profile: SemanticProfileId,
}
impl CanonicalObservedSchema {
pub const fn canonicalization_version(&self) -> &'static str {
OBSERVED_SCHEMA_CANONICALIZATION_VERSION
}
pub const fn direct_schema(&self) -> &DeclaredSchema {
&self.direct_schema
}
pub const fn managed_scope(&self) -> &ManagedSchemaScope {
&self.managed_scope
}
pub const fn semantic_profile(&self) -> &SemanticProfileId {
&self.semantic_profile
}
pub fn canonical_identity_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
self.direct_schema.canonical_identity_bytes()
}
pub const fn declared_identity_fingerprint(&self) -> &DeclaredIdentityFingerprint {
self.direct_schema.declared_identity_fingerprint()
}
}
pub fn canonicalize_observed_schema(
observed: &ObservedSchema,
semantic_profile: &SemanticProfile,
) -> Result<CanonicalObservedSchema, SchemaDiagnostics> {
let mut captures = BTreeMap::<SchemaFactId, Vec<&ObservedSchemaFact>>::new();
for captured in observed.facts() {
captures
.entry(captured.fact().id())
.or_default()
.push(captured);
}
let mut diagnostics = Vec::new();
let mut direct = BTreeMap::<SchemaFactId, (&SchemaFact, ObservedFactScope)>::new();
let mut inherited_origins = Vec::<SchemaFactId>::new();
for (id, entries) in captures {
if entries.len() != 1 {
diagnostics.push(observed_diagnostic(
"duplicate_observed_fact",
"provider introspection returned one fact identity more than once",
));
continue;
}
let captured = entries[0];
match captured.provenance() {
ObservedFactProvenance::Direct => {
direct.insert(id, (captured.fact(), captured.scope()));
}
ObservedFactProvenance::Inherited { declared_fact } => {
inherited_origins.push(declared_fact.clone());
}
ObservedFactProvenance::ServerDefault => {
if !is_server_default_cardinality(captured.fact(), semantic_profile) {
diagnostics.push(observed_diagnostic(
"invalid_observed_server_default",
"a server default must exactly match the selected profile's omitted interface cardinality",
));
}
}
ObservedFactProvenance::Ambiguous => diagnostics.push(observed_diagnostic(
"ambiguous_observed_provenance",
"direct, inherited, and synthesized provenance could not be recovered unambiguously",
)),
}
}
for declared_fact in inherited_origins {
if !direct.contains_key(&declared_fact) {
diagnostics.push(observed_diagnostic(
"invalid_observed_inheritance_origin",
"an inherited fact must identify an existing direct declaration",
));
}
}
if !diagnostics.is_empty() {
return Err(SchemaDiagnostics::from_vec(diagnostics));
}
let synthetic_source = synthetic_observed_source()?;
let managed_scope = ManagedSchemaScope::new(
direct
.iter()
.filter(|&(_, (_, scope))| *scope == ObservedFactScope::Managed)
.map(|(id, _)| id.clone()),
);
let direct_schema = DeclaredSchema::from_facts(
observed.format(),
observed.required_capabilities().clone(),
direct
.into_values()
.map(|(fact, _)| SourcedSchemaFact::new(fact.clone(), synthetic_source.clone())),
)?;
Ok(CanonicalObservedSchema {
direct_schema,
managed_scope,
semantic_profile: semantic_profile.id().clone(),
})
}
fn is_server_default_cardinality(fact: &SchemaFact, profile: &SemanticProfile) -> bool {
let SchemaFact::Annotation(annotation) = fact else {
return false;
};
if annotation.id().kind() != &AnnotationKindId::Card {
return false;
}
let kind = match annotation.id().subject() {
type_bridge_contract::schema::AnnotationSubjectId::Owns(_) => InterfaceKind::Owns,
type_bridge_contract::schema::AnnotationSubjectId::Relates(_) => InterfaceKind::Relates,
type_bridge_contract::schema::AnnotationSubjectId::Plays(_) => InterfaceKind::Plays,
_ => return false,
};
matches!(
annotation.value(),
SchemaAnnotationValue::Cardinality(cardinality)
if *cardinality == profile.default_cardinality(kind)
)
}
fn synthetic_observed_source() -> Result<SourceSpan, SchemaDiagnostics> {
let document = DocumentId::new("observed.schema").map_err(schema_diagnostics)?;
SourceSpan::new(document, 0, 1, 1, 1, 1, 2).map_err(schema_diagnostics)
}
fn observed_diagnostic(code: &'static str, message: &'static str) -> SchemaDiagnostic {
SchemaDiagnostic::new(
Diagnostic::new(
DiagnosticCategory::InvalidContract,
DiagnosticCode::new(code).expect("static observed-schema diagnostic code is valid"),
message,
),
None,
)
}
fn schema_diagnostics(diagnostic: Diagnostic) -> SchemaDiagnostics {
SchemaDiagnostics::one(SchemaDiagnostic::new(diagnostic, None))
}