// ============================================================
// Blackboard schema and validation
//
// Real integrations usually have a contract for the keys that a tree reads.
// Schema keeps that contract executable: missing context and type mismatches
// become explicit diagnostics instead of silent fallback behavior.
// ============================================================
///|
/// The four value categories supported by Blackboard.
pub(all) enum ValueKind {
BoolKind
IntKind
DoubleKind
StringKind
} derive(Eq)
///|
/// Describe one required or optional blackboard field.
pub struct FieldSpec {
key : String
kind : ValueKind
required : Bool
default_value : Value?
}
///|
/// Create a required field specification.
pub fn FieldSpec::required(key : String, kind : ValueKind) -> FieldSpec {
{ key, kind, required: true, default_value: None }
}
///|
/// Create an optional field specification.
pub fn FieldSpec::optional(key : String, kind : ValueKind) -> FieldSpec {
{ key, kind, required: false, default_value: None }
}
///|
/// Create a field with a value to install when absent.
pub fn FieldSpec::with_default(
key : String,
kind : ValueKind,
default_value : Value,
) -> FieldSpec {
{ key, kind, required: false, default_value: Some(default_value) }
}
///|
/// Return a stable name for a value category.
pub fn ValueKind::to_string(self : ValueKind) -> String {
match self {
ValueKind::BoolKind => "bool"
ValueKind::IntKind => "int"
ValueKind::DoubleKind => "double"
ValueKind::StringKind => "string"
}
}
///|
/// Return the category of a concrete value.
pub fn Value::kind(self : Value) -> ValueKind {
match self {
Value::Bool(_) => ValueKind::BoolKind
Value::Int(_) => ValueKind::IntKind
Value::Double(_) => ValueKind::DoubleKind
Value::Str(_) => ValueKind::StringKind
}
}
///|
/// A single schema validation finding.
pub enum ValidationIssue {
MissingField(String)
WrongType(String, ValueKind, ValueKind)
} derive(Eq)
///|
/// Format a validation finding for logs and test reports.
pub fn ValidationIssue::to_string(self : ValidationIssue) -> String {
match self {
ValidationIssue::MissingField(key) => "missing field: \{key}"
ValidationIssue::WrongType(key, expected, actual) =>
"wrong type for \{key}: expected \{expected.to_string()}, got \{actual.to_string()}"
}
}
///|
/// A collection of field contracts for a behavior tree.
pub struct BlackboardSchema {
fields : Array[FieldSpec]
}
///|
/// Create an empty schema.
pub fn BlackboardSchema::new() -> BlackboardSchema {
{ fields: [] }
}
///|
/// Add a field contract and return the same schema for fluent setup.
pub fn BlackboardSchema::add(
self : BlackboardSchema,
field : FieldSpec,
) -> BlackboardSchema {
self.fields.push(field)
self
}
///|
/// Number of fields in the schema.
pub fn BlackboardSchema::size(self : BlackboardSchema) -> Int {
self.fields.length()
}
///|
/// Validate all fields without mutating the blackboard.
pub fn BlackboardSchema::validate(
self : BlackboardSchema,
bb : Blackboard,
) -> Array[ValidationIssue] {
let issues : Array[ValidationIssue] = []
for field in self.fields {
match bb.get_value(field.key) {
None =>
if field.required {
issues.push(ValidationIssue::MissingField(field.key))
}
Some(value) =>
if value.kind() != field.kind {
issues.push(
ValidationIssue::WrongType(field.key, field.kind, value.kind()),
)
}
}
}
issues
}
///|
/// Return whether the blackboard satisfies every schema field.
pub fn BlackboardSchema::is_valid(
self : BlackboardSchema,
bb : Blackboard,
) -> Bool {
self.validate(bb).length() == 0
}
///|
/// Install declared defaults without overwriting caller state.
pub fn BlackboardSchema::apply_defaults(
self : BlackboardSchema,
bb : Blackboard,
) -> Int {
let mut applied = 0
for field in self.fields {
match field.default_value {
None => ()
Some(value) =>
if bb.set_if_absent(field.key, value) {
applied = applied + 1
}
}
}
applied
}