//! 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 { 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; } 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 { 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 { 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 { 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, /// Inclusive upper bound when the value is ordered. pub maximum: Option, /// 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 { 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; /// Returns one value from the immutable boot-active view. fn active_field_value(active: &Self::Active, id: FieldId) -> Option; /// 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; }