refactor: move firmware workspace under firmware

This commit is contained in:
Onur Özkan committed 2026-08-10 16:58:55 +03:00
1 parent 153b0bf262
commit f03fc119a4
222 files changed
+56 -13

No files matched your search

@@ -0,0 +1,195 @@
//! Stable typed outcomes for the Kaptan configuration microservice.
use kaptan_config::{ConfigState, ConfigViolation, FieldId};
use kaptan_log::{EventHub, KaptanError, LogFlags, PublishOutcome};
/// Operation reported in an authoritative result record.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Operation {
/// Query/read operation.
Query,
/// Begin transaction.
Begin,
/// Scalar write.
Set,
/// Whole-candidate validation.
Validate,
/// Durable commit.
Commit,
/// Transaction abort.
Abort,
/// CBOR snapshot import into the isolated transaction.
Import,
}
/// Stable result classification.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResultKind {
/// The operation was accepted.
Accepted,
/// A full snapshot committed but requires restart before activation.
CommittedRestartRequired,
/// The operation was rejected without an active-state transition.
Rejected,
}
/// Compact protocol error code. Detail belongs in [`ConfigViolation`] when a
/// configuration invariant was responsible.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum ProtocolError {
/// The request target did not match this component.
BadTarget,
/// Authorization was absent or insufficient.
AuthorizationDenied,
/// The optimistic durable generation did not match.
GenerationConflict,
/// The single transaction slot is occupied.
SessionBusy,
/// The session is unknown, expired, or belongs to a different principal.
InvalidSession,
/// Reusing one request ID with different content is forbidden.
RequestConflict,
/// The request was not based on the current transaction revision.
StaleRevision,
/// A command is invalid in the current lifecycle state.
BadState,
/// The field is not present in this schema.
UnknownField(FieldId),
/// The typed setter does not match the field descriptor.
TypeMismatch(FieldId),
/// A local or complete validation issue.
Violation(ConfigViolation),
/// Persistence failed after a complete validation pass.
StorageFailure,
/// A CBOR snapshot could not be decoded into this schema.
CborMalformed,
/// A CBOR snapshot exceeded the fixed import budget.
CborTooLarge,
/// The complete CBOR snapshot did not match the supplied CRC-32C.
BlobCrc,
/// The uploaded CBOR blob was prepared for a different schema.
SchemaMismatch,
/// This deployment does not expose bounded CBOR transaction import.
ImportUnavailable,
/// The adapter reached an impossible internal state.
Internal,
}
/// Authoritative completion for one request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ConfigReply {
/// Client request identity.
pub request_id: u32,
/// The current session, or zero when none exists.
pub session_id: u32,
/// Server-assigned import transfer identity, or zero when not importing.
pub transfer_id: u32,
/// The transaction revision, or zero when no transaction exists.
pub revision: u32,
/// Latest committed generation, or zero for the factory profile.
pub generation: u64,
/// Completed operation.
pub operation: Operation,
/// High-level result.
pub result: ResultKind,
/// Exact typed failure, if rejected.
pub error: Option<ProtocolError>,
/// Current lifecycle state after handling the request.
pub state: ConfigState,
}
impl ConfigReply {
/// Stable primary field for the wire response, or the reserved sentinel.
pub const fn primary_field(self) -> u16 {
match self.error {
Some(ProtocolError::Violation(violation)) => match violation.primary() {
Some(field) => field.get(),
None => u16::MAX,
},
Some(ProtocolError::UnknownField(field)) | Some(ProtocolError::TypeMismatch(field)) => {
field.get()
}
_ => u16::MAX,
}
}
/// Stable related field for the wire response, or the reserved sentinel.
pub const fn related_field(self) -> u16 {
match self.error {
Some(ProtocolError::Violation(violation)) => match violation.related_field() {
Some(field) => field.get(),
None => u16::MAX,
},
_ => u16::MAX,
}
}
/// Stable validation-rule code, or zero when no validation rule applies.
pub const fn validation_rule(self) -> u16 {
match self.error {
Some(ProtocolError::Violation(violation)) => violation.rule().code(),
_ => 0,
}
}
/// Enqueues a critical diagnostic companion for a rejected structured
/// result. The `KAPTAN_CONFIG_RESULT` remains the correlated,
/// field/rule-precise authority; this sends only the derive-generated
/// high-level `ProtocolError` display identity through `KAPTAN_ERROR`.
///
/// The helper deliberately carries no request/session/generation fields
/// into the error channel. A composition owner may call it after it has
/// emitted or retained the authoritative result.
pub fn publish_rejection<const CRITICAL: usize, const BEST_EFFORT: usize>(
self,
hub: &mut EventHub<CRITICAL, BEST_EFFORT>,
) -> Option<PublishOutcome> {
if self.result != ResultKind::Rejected {
return None;
}
self.error
.as_ref()
.map(|error| hub.publish_error(LogFlags::NONE, error))
}
}
#[cfg(test)]
mod tests {
use kaptan_log::{EventHub, LogFilter, PublishOutcome, RecordChannel};
use super::{ConfigReply, Operation, ProtocolError, ResultKind};
use kaptan_config::ConfigState;
#[test]
fn rejected_result_enqueues_only_its_high_level_error_identity() {
let reply = ConfigReply {
request_id: 42,
session_id: 7,
transfer_id: 0,
revision: 3,
generation: 9,
operation: Operation::Set,
result: ResultKind::Rejected,
error: Some(ProtocolError::BadState),
state: ConfigState::Editing,
};
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
assert_eq!(
reply.publish_rejection(&mut hub),
Some(PublishOutcome::Queued)
);
let record = hub.peek().expect("rejection is retained");
assert_eq!(record.channel(), RecordChannel::Error);
assert_eq!(
record.error_path().unwrap().as_str(),
"kaptan_config_mavlink::protocol::ProtocolError::BadState"
);
assert!(record.additional_information().unwrap().is_empty());
let accepted = ConfigReply {
result: ResultKind::Accepted,
error: None,
..reply
};
assert_eq!(accepted.publish_rejection(&mut hub), None);
}
}