Files
kaptan/firmware/libs/kaptan-config/src/field.rs
T

301 lines
10 KiB
Rust

//! Transport-neutral typed configuration fields and descriptors.
use crate::{ConfigViolation, ValidationRule};
use kaptan_err_propagate::KaptanError;
/// Maximum choices representable by one enum-backed field.
///
/// Enum values and indexed discovery use their native `u8` domain. Options
/// are immutable flash metadata, so no smaller runtime resource limit is
/// necessary.
pub const MAX_ENUM_OPTIONS_PER_FIELD: usize = u8::MAX as usize;
/// Maximum UTF-8 bytes advertised for one enum variant name.
pub const MAX_ENUM_OPTION_LABEL_BYTES: usize = 32;
/// A stable, schema-local identity for one editable configuration field.
///
/// IDs are never reused within a schema version. They are compact wire
/// identities; display strings belong to metadata and diagnostics only.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
#[repr(transparent)]
pub struct FieldId(u16);
impl FieldId {
/// Creates a stable field identity.
pub const fn new(value: u16) -> Self {
assert!(
value != 0 && value != u16::MAX,
"reserved configuration field id"
);
Self(value)
}
/// Parses a field identity received from an untrusted transport.
pub const fn try_new(value: u16) -> Option<Self> {
if value == 0 || value == u16::MAX {
None
} else {
Some(Self(value))
}
}
/// Returns the transport representation.
pub const fn get(self) -> u16 {
self.0
}
}
/// The scalar representation of a configuration value.
///
/// The variants deliberately preserve native widths: durations, for example,
/// remain `u64` microseconds instead of being lossy display-unit conversions.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ConfigValue {
/// Boolean value.
Bool(bool),
/// Signed 32-bit integer.
I32(i32),
/// Unsigned 32-bit integer.
U32(u32),
/// Unsigned 64-bit integer.
U64(u64),
/// IEEE-754 single-precision value.
F32(f32),
/// IEEE-754 double-precision value.
F64(f64),
}
/// One named value advertised for an enum-backed configuration field.
///
/// Enum metadata belongs to the compiled schema. Generic transports can
/// therefore render choices without knowing field keys or firmware-specific
/// enum types. The tuple is `(stable u8 representation, display label)`.
pub type EnumOption = (u8, &'static str);
/// Failure to project a generic configuration scalar into its native type.
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScalarValueError {
/// The generic scalar kind does not match the native field type.
WrongKind(ValueKind),
/// The representation has the correct scalar kind but no native value.
OutOfDomain,
}
/// Native representation used by generated configuration fields.
///
/// The generic manager and transports retain their compact fixed scalar
/// protocol while board and runtime structures keep the narrow or enum type
/// that actually models the value.
#[doc(hidden)]
pub trait ConfigScalar: Copy {
/// Generic scalar kind used at schema and transport boundaries.
const KIND: ValueKind;
/// Projects the native value into the generic scalar boundary.
fn into_config_value(self) -> ConfigValue;
/// Validates and projects one generic scalar into the native type.
fn try_from_config_value(value: ConfigValue) -> Result<Self, ScalarValueError>;
}
macro_rules! direct_config_scalar {
($type:ty, $kind:ident, $variant:ident) => {
impl ConfigScalar for $type {
const KIND: ValueKind = ValueKind::$kind;
fn into_config_value(self) -> ConfigValue {
ConfigValue::$variant(self)
}
fn try_from_config_value(value: ConfigValue) -> Result<Self, ScalarValueError> {
match value {
ConfigValue::$variant(value) => Ok(value),
value => Err(ScalarValueError::WrongKind(value.kind())),
}
}
}
};
}
direct_config_scalar!(bool, Bool, Bool);
direct_config_scalar!(i32, I32, I32);
direct_config_scalar!(u32, U32, U32);
direct_config_scalar!(u64, U64, U64);
direct_config_scalar!(f32, F32, F32);
direct_config_scalar!(f64, F64, F64);
macro_rules! narrow_unsigned_config_scalar {
($type:ty) => {
impl ConfigScalar for $type {
const KIND: ValueKind = ValueKind::U32;
fn into_config_value(self) -> ConfigValue {
ConfigValue::U32(u32::from(self))
}
fn try_from_config_value(value: ConfigValue) -> Result<Self, ScalarValueError> {
match value {
ConfigValue::U32(value) => {
Self::try_from(value).map_err(|_| ScalarValueError::OutOfDomain)
}
value => Err(ScalarValueError::WrongKind(value.kind())),
}
}
}
};
}
narrow_unsigned_config_scalar!(u8);
narrow_unsigned_config_scalar!(u16);
impl ConfigValue {
/// The declared value kind.
pub const fn kind(self) -> ValueKind {
match self {
Self::Bool(_) => ValueKind::Bool,
Self::I32(_) => ValueKind::I32,
Self::U32(_) => ValueKind::U32,
Self::U64(_) => ValueKind::U64,
Self::F32(_) => ValueKind::F32,
Self::F64(_) => ValueKind::F64,
}
}
/// Canonical scalar bits for compact transport encodings.
pub const fn raw_bits(self) -> u64 {
match self {
Self::Bool(value) => value as u64,
Self::I32(value) => value as u32 as u64,
Self::U32(value) => value as u64,
Self::U64(value) => value,
Self::F32(value) => value.to_bits() as u64,
Self::F64(value) => value.to_bits(),
}
}
/// Decodes the canonical scalar representation used by snapshots and the
/// configuration wire protocol.
pub const fn try_from_raw_bits(kind: ValueKind, bits: u64) -> Option<Self> {
match kind {
ValueKind::Bool => match bits {
0 => Some(Self::Bool(false)),
1 => Some(Self::Bool(true)),
_ => None,
},
ValueKind::I32 if bits <= u32::MAX as u64 => Some(Self::I32(bits as u32 as i32)),
ValueKind::U32 if bits <= u32::MAX as u64 => Some(Self::U32(bits as u32)),
ValueKind::U64 => Some(Self::U64(bits)),
ValueKind::F32 if bits <= u32::MAX as u64 => {
Some(Self::F32(f32::from_bits(bits as u32)))
}
ValueKind::F64 => Some(Self::F64(f64::from_bits(bits))),
ValueKind::I32 | ValueKind::U32 | ValueKind::F32 => None,
}
}
}
/// The scalar kind carried in a descriptor or typed protocol message.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
pub enum ValueKind {
/// Boolean.
Bool = 1,
/// Signed 32-bit integer.
I32 = 2,
/// Unsigned 32-bit integer.
U32 = 3,
/// Unsigned 64-bit integer.
U64 = 4,
/// Single-precision float.
F32 = 5,
/// Double-precision float.
F64 = 6,
}
/// Static metadata for one configuration field.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FieldDescriptor {
/// Stable schema-local identity.
pub id: FieldId,
/// GCS display key; never used as a wire identity.
pub display_key: &'static str,
/// Scalar storage type.
pub kind: ValueKind,
/// Inclusive lower bound when the value is ordered.
pub minimum: Option<ConfigValue>,
/// Inclusive upper bound when the value is ordered.
pub maximum: Option<ConfigValue>,
/// Named choices for an enum-backed field, otherwise empty.
pub enum_options: &'static [EnumOption],
/// Human-readable unit for schema metadata.
pub unit: &'static str,
}
/// Failure while changing one transaction field.
#[derive(Clone, Copy, Debug, PartialEq, KaptanError)]
pub enum FieldWriteError {
/// No descriptor exposes the requested identity.
#[kaptan_error(id = 1001)]
UnknownField(FieldId),
/// The supplied scalar type differs from the descriptor type.
#[kaptan_error(id = 1002)]
WrongType {
/// Requested field.
field: FieldId,
/// Expected descriptor kind.
expected: ValueKind,
/// Supplied scalar kind.
actual: ValueKind,
},
/// A locally validated scalar violated a stable rule.
#[kaptan_error(id = 1003)]
Violation(ConfigViolation),
}
impl FieldWriteError {
/// Returns the typed local validation issue, if this was one.
pub const fn violation(self) -> Option<ConfigViolation> {
match self {
Self::Violation(violation) => Some(violation),
Self::UnknownField(_) | Self::WrongType { .. } => None,
}
}
/// Returns a field-local non-finite error.
pub const fn non_finite(field: FieldId) -> Self {
Self::Violation(ConfigViolation::field(field, ValidationRule::NonFinite))
}
}
/// Vehicle-specific editable candidate model.
///
/// Implementations expose every user-editable scalar through the static
/// descriptor registry. A field write performs only local validation; complete
/// relational and capability validation happens through [`Self::validate`].
pub trait ConfigModel: Clone {
/// Immutable configuration consumed by the runtime at boot.
type Active;
/// Immutable compiled board/vehicle capability contract.
type Context;
/// Exact identifier for the current externally transferable schema.
const SCHEMA_ID: [u8; 16];
/// Returns the descriptor at a stable zero-based schema index.
fn descriptor(index: u16) -> Option<&'static FieldDescriptor>;
/// Number of fields exposed by the compiled schema.
fn field_count() -> u16;
/// Returns a candidate value by stable ID.
fn field_value(&self, id: FieldId) -> Option<ConfigValue>;
/// Returns one value from the immutable boot-active view.
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue>;
/// Applies exactly one locally valid scalar to this candidate.
fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError>;
/// Fully validates and converts the candidate to an immutable active view.
fn validate(&self, context: &Self::Context) -> Result<Self::Active, ConfigViolation>;
}