refactor(firmware): centralize configuration and diagnostics
This commit is contained in:
1 parent
7048ed89d0
commit
387fdaad23
140 files changed
+6891
-4541
No files matched your search
@@ -8,11 +8,6 @@ version.workspace = true
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# Explicitly opt-in host/SITL test authority. Production cannot construct an
|
||||
# authorization token from a CRC-valid raw MAVLink frame.
|
||||
host-authority = []
|
||||
|
||||
[dependencies]
|
||||
kaptan-config = { path = "../kaptan-config" }
|
||||
kaptan-log = { path = "../kaptan-log" }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Authorized, transport-neutral configuration commands.
|
||||
//! Transport-neutral configuration commands.
|
||||
|
||||
use kaptan_config::{ConfigValue, FieldId};
|
||||
|
||||
@@ -11,6 +11,30 @@ pub struct Target {
|
||||
component: u8,
|
||||
}
|
||||
|
||||
/// Exact MAVLink endpoint that owns a configuration transaction lease.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct Principal(u16);
|
||||
|
||||
impl Principal {
|
||||
/// Creates a principal from one exact source system/component pair.
|
||||
pub const fn new(system: u8, component: u8) -> Self {
|
||||
assert!(
|
||||
system != 0 && component != 0,
|
||||
"configuration principal must be exact"
|
||||
);
|
||||
Self(u16::from_be_bytes([system, component]))
|
||||
}
|
||||
|
||||
/// Parses an exact source principal from an untrusted transport.
|
||||
pub const fn try_new(system: u8, component: u8) -> Option<Self> {
|
||||
if system == 0 || component == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Self(u16::from_be_bytes([system, component])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the configuration snapshot read by a value request.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ValueSource {
|
||||
@@ -45,21 +69,30 @@ impl Target {
|
||||
/// One command accepted by the Kaptan configuration microservice.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ConfigCommand {
|
||||
/// Reads protocol/schema metadata before any descriptor or upload request.
|
||||
/// Reads protocol/schema metadata before descriptor and value requests.
|
||||
InfoRequest {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
/// Enumerates a bounded page of schema descriptors.
|
||||
/// Reads one schema descriptor by stable index.
|
||||
DescriptorRequest {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// First stable descriptor index.
|
||||
first: u16,
|
||||
/// Requested number of descriptors, clamped to the protocol bound.
|
||||
count: u16,
|
||||
/// Stable descriptor index.
|
||||
index: u16,
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
/// Reads one named choice for one descriptor.
|
||||
EnumOptionRequest {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// Stable index of the owning descriptor.
|
||||
descriptor_index: u16,
|
||||
/// Stable enum-option index.
|
||||
option_index: u8,
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
@@ -76,59 +109,10 @@ pub enum ConfigCommand {
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
/// Starts one bounded CBOR snapshot import into the caller's transaction.
|
||||
ImportBegin {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// Lease-bound transaction identity.
|
||||
session_id: u32,
|
||||
/// Optimistic transaction revision.
|
||||
expected_revision: u32,
|
||||
/// Complete CBOR byte length.
|
||||
length: u16,
|
||||
/// Exact schema identifier advertised by `KAPTAN_CONFIG_INFO`.
|
||||
schema_id: [u8; 16],
|
||||
/// CRC-32C over exactly `length` CBOR bytes.
|
||||
crc32c: u32,
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
/// Supplies one fixed-bounded CBOR import chunk.
|
||||
ImportChunk {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// Lease-bound transaction identity.
|
||||
session_id: u32,
|
||||
/// Server-issued ID returned by the matching import-begin result.
|
||||
transfer_id: u32,
|
||||
/// Byte offset into the complete import.
|
||||
offset: u16,
|
||||
/// Used bytes of `data`.
|
||||
length: u8,
|
||||
/// Fixed MAVLink payload data.
|
||||
data: [u8; 192],
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
/// Checks the completed import and replaces only the RAM transaction.
|
||||
ImportFinish {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// Lease-bound transaction identity.
|
||||
session_id: u32,
|
||||
/// Server-issued ID returned by the matching import-begin result.
|
||||
transfer_id: u32,
|
||||
/// Optimistic transaction revision.
|
||||
expected_revision: u32,
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
/// Starts an edit transaction from the expected committed generation.
|
||||
/// Starts an edit transaction from the latest committed configuration.
|
||||
Begin {
|
||||
/// Client correlation identity.
|
||||
request_id: u32,
|
||||
/// Optimistic concurrency generation.
|
||||
expected_generation: u64,
|
||||
/// Exact addressed endpoint.
|
||||
target: Target,
|
||||
},
|
||||
@@ -164,8 +148,6 @@ pub enum ConfigCommand {
|
||||
request_id: u32,
|
||||
/// Lease-bound transaction identity.
|
||||
session_id: u32,
|
||||
/// Optimistic committed generation.
|
||||
expected_generation: u64,
|
||||
/// Optimistic transaction revision.
|
||||
expected_revision: u32,
|
||||
/// Exact addressed endpoint.
|
||||
@@ -188,10 +170,8 @@ impl ConfigCommand {
|
||||
match self {
|
||||
Self::InfoRequest { request_id, .. }
|
||||
| Self::DescriptorRequest { request_id, .. }
|
||||
| Self::EnumOptionRequest { request_id, .. }
|
||||
| Self::ValueRequest { request_id, .. }
|
||||
| Self::ImportBegin { request_id, .. }
|
||||
| Self::ImportChunk { request_id, .. }
|
||||
| Self::ImportFinish { request_id, .. }
|
||||
| Self::Begin { request_id, .. }
|
||||
| Self::Set { request_id, .. }
|
||||
| Self::Validate { request_id, .. }
|
||||
@@ -205,10 +185,8 @@ impl ConfigCommand {
|
||||
match self {
|
||||
Self::InfoRequest { target, .. }
|
||||
| Self::DescriptorRequest { target, .. }
|
||||
| Self::EnumOptionRequest { target, .. }
|
||||
| Self::ValueRequest { target, .. }
|
||||
| Self::ImportBegin { target, .. }
|
||||
| Self::ImportChunk { target, .. }
|
||||
| Self::ImportFinish { target, .. }
|
||||
| Self::Begin { target, .. }
|
||||
| Self::Set { target, .. }
|
||||
| Self::Validate { target, .. }
|
||||
@@ -218,54 +196,25 @@ impl ConfigCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// A capability token produced only by the future signing/replay authority.
|
||||
///
|
||||
/// The token carries a stable principal identity so the service can bind a
|
||||
/// transaction lease to one authenticated source. It is intentionally opaque:
|
||||
/// an unverified raw MAVLink frame cannot create one.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct VerifiedAuthority {
|
||||
principal: u64,
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl VerifiedAuthority {
|
||||
/// Returns the authenticated principal identity for ownership checks.
|
||||
pub const fn principal(self) -> u64 {
|
||||
self.principal
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "host-authority"))]
|
||||
/// Creates an explicit host/SITL authority token.
|
||||
///
|
||||
/// This exists only for host integration and protocol tests. Flight code
|
||||
/// must receive the equivalent token from the reviewed signing/replay path.
|
||||
pub const fn host(principal: u64) -> Self {
|
||||
Self {
|
||||
principal,
|
||||
_private: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A command whose authority was verified before it reached this adapter.
|
||||
/// Test command paired with its lease principal.
|
||||
#[cfg(test)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Authorized<T> {
|
||||
authority: VerifiedAuthority,
|
||||
principal: Principal,
|
||||
command: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<T> Authorized<T> {
|
||||
#[cfg(any(test, feature = "host-authority"))]
|
||||
/// Wraps a host/SITL command in an explicit test-only authority token.
|
||||
pub const fn host(principal: u64, command: T) -> Self {
|
||||
/// Pairs a test command with component `principal` in test system 1.
|
||||
pub const fn host(principal: u8, command: T) -> Self {
|
||||
Self {
|
||||
authority: VerifiedAuthority::host(principal),
|
||||
principal: Principal::new(1, principal),
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_parts(self) -> (VerifiedAuthority, T) {
|
||||
(self.authority, self.command)
|
||||
pub(crate) fn into_parts(self) -> (Principal, T) {
|
||||
(self.principal, self.command)
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,34 @@
|
||||
//!
|
||||
//! This crate translates generated `kaptan.xml` records to the transport-
|
||||
//! neutral configuration lifecycle. It does not parse raw frames, authenticate
|
||||
//! peers, deserialize CBOR, access storage directly, or mutate active flight
|
||||
//! peers, access storage directly, or mutate active flight
|
||||
//! consumers.
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate std;
|
||||
|
||||
mod command;
|
||||
mod protocol;
|
||||
pub mod service;
|
||||
pub mod wire;
|
||||
|
||||
pub use command::{Authorized, ConfigCommand, Target, ValueSource, VerifiedAuthority};
|
||||
#[cfg(test)]
|
||||
pub use command::Authorized;
|
||||
pub use command::{ConfigCommand, Principal, Target, ValueSource};
|
||||
pub use protocol::{ConfigReply, Operation, ProtocolError, ResultKind};
|
||||
pub use service::{
|
||||
CAPABILITY_CBOR_IMPORT, ConfigInfo, ConfigProtocol, DescriptorPage, NoSnapshotImporter,
|
||||
ServiceResponse, SnapshotImporter, ValueResponse,
|
||||
ConfigInfo, ConfigProtocol, DescriptorResponse, EnumOptionResponse, ServiceResponse,
|
||||
ValueResponse,
|
||||
};
|
||||
pub use wire::{ResponseVisitor, visit_response};
|
||||
|
||||
/// Wire protocol implemented by the descriptor/value configuration service.
|
||||
pub const PROTOCOL_MAJOR: u8 = 2;
|
||||
pub const PROTOCOL_MINOR: u8 = 0;
|
||||
|
||||
/// `KAPTAN_CONFIG_DESCRIPTOR.flags` bit: `minimum_bits` is present.
|
||||
pub const DESCRIPTOR_HAS_MINIMUM: u8 = 1 << 0;
|
||||
/// `KAPTAN_CONFIG_DESCRIPTOR.flags` bit: `maximum_bits` is present.
|
||||
pub const DESCRIPTOR_HAS_MAXIMUM: u8 = 1 << 1;
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Stable typed outcomes for the Kaptan configuration microservice.
|
||||
|
||||
use kaptan_config::{ConfigState, ConfigViolation, FieldId};
|
||||
use kaptan_log::{EventHub, KaptanError, LogFlags, PublishOutcome};
|
||||
use kaptan_log::KaptanError;
|
||||
|
||||
/// Operation reported in an authoritative result record.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Operation {
|
||||
@@ -17,8 +18,6 @@ pub enum Operation {
|
||||
Commit,
|
||||
/// Transaction abort.
|
||||
Abort,
|
||||
/// CBOR snapshot import into the isolated transaction.
|
||||
Import,
|
||||
}
|
||||
|
||||
/// Stable result classification.
|
||||
@@ -38,11 +37,7 @@ pub enum ResultKind {
|
||||
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.
|
||||
/// The configuration lease or single transaction slot is occupied.
|
||||
SessionBusy,
|
||||
/// The session is unknown, expired, or belongs to a different principal.
|
||||
InvalidSession,
|
||||
@@ -54,22 +49,14 @@ pub enum ProtocolError {
|
||||
BadState,
|
||||
/// The field is not present in this schema.
|
||||
UnknownField(FieldId),
|
||||
/// A query requested an empty or out-of-range descriptor span.
|
||||
InvalidQuery,
|
||||
/// 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,
|
||||
}
|
||||
@@ -81,12 +68,8 @@ pub struct ConfigReply {
|
||||
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.
|
||||
@@ -130,66 +113,4 @@ impl ConfigReply {
|
||||
_ => 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);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
//! Transport-neutral façade for the custom configuration service.
|
||||
//!
|
||||
//! Public callers see one small protocol API here. Its bounded query,
|
||||
//! session, replay, and CBOR-import mechanics stay in private sibling modules
|
||||
//! session and replay mechanics stay in private sibling modules
|
||||
//! so no one concern becomes a second service owner.
|
||||
|
||||
mod import;
|
||||
mod query;
|
||||
mod replay;
|
||||
mod session;
|
||||
|
||||
pub use import::{CAPABILITY_CBOR_IMPORT, NoSnapshotImporter, SnapshotImporter};
|
||||
pub use query::{ConfigInfo, DescriptorPage, ServiceResponse, ValueResponse};
|
||||
pub use query::{
|
||||
ConfigInfo, DescriptorResponse, EnumOptionResponse, ServiceResponse, ValueResponse,
|
||||
};
|
||||
pub use session::ConfigProtocol;
|
||||
@@ -1,248 +0,0 @@
|
||||
//! Bounded CBOR snapshot transfer state.
|
||||
|
||||
use kaptan_config::{CommitError, ConfigManager, ConfigModel, ProfileStore};
|
||||
|
||||
use super::session::{ConfigProtocol, Session};
|
||||
use crate::{ConfigCommand, ConfigReply, Operation, ProtocolError, ResultKind};
|
||||
|
||||
/// Maximum complete CBOR snapshot accepted by the protocol.
|
||||
pub(super) const IMPORT_MAXIMUM_BYTES: usize = 4096;
|
||||
/// Payload bytes carried by each aligned import chunk.
|
||||
pub(super) const IMPORT_CHUNK_BYTES: usize = 192;
|
||||
pub(super) const IMPORT_CHUNK_COUNT: usize = IMPORT_MAXIMUM_BYTES.div_ceil(IMPORT_CHUNK_BYTES);
|
||||
|
||||
/// `KAPTAN_CONFIG_INFO.capability_flags`: bounded CBOR transaction import.
|
||||
pub const CAPABILITY_CBOR_IMPORT: u8 = 1;
|
||||
|
||||
/// Decodes a complete bounded CBOR snapshot into this service's candidate.
|
||||
///
|
||||
/// Vehicle-specific implementations live above this transport adapter so the
|
||||
/// generic MAVLink layer never owns a profile schema or board capabilities.
|
||||
pub trait SnapshotImporter<C> {
|
||||
/// Whether this concrete deployment exposes the bounded import path.
|
||||
const IMPORT_SUPPORTED: bool;
|
||||
/// Current format carried inside the deployment's raw CBOR profile blob.
|
||||
const PROFILE_FORMAT: u16;
|
||||
/// Converts one complete CBOR payload to a candidate. The service performs
|
||||
/// the subsequent complete-context validation before replacing the RAM
|
||||
/// transaction.
|
||||
fn decode(&self, bytes: &[u8]) -> Result<C, ProtocolError>;
|
||||
}
|
||||
|
||||
/// Default importer used when a deployment deliberately does not expose CBOR
|
||||
/// import yet. It keeps the service type usable for descriptor-only tests.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct NoSnapshotImporter;
|
||||
|
||||
impl<C> SnapshotImporter<C> for NoSnapshotImporter {
|
||||
const IMPORT_SUPPORTED: bool = false;
|
||||
const PROFILE_FORMAT: u16 = 0;
|
||||
|
||||
fn decode(&self, _: &[u8]) -> Result<C, ProtocolError> {
|
||||
Err(ProtocolError::CborMalformed)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct ImportBuffer {
|
||||
owner: u64,
|
||||
session_id: u32,
|
||||
transfer_id: u32,
|
||||
revision: u32,
|
||||
length: u16,
|
||||
crc32c: u32,
|
||||
received: [bool; IMPORT_CHUNK_COUNT],
|
||||
data: [u8; IMPORT_MAXIMUM_BYTES],
|
||||
}
|
||||
|
||||
impl<I> ConfigProtocol<I> {
|
||||
pub(super) fn begin_import<C>(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
command: ConfigCommand,
|
||||
session: Session,
|
||||
) -> ConfigReply
|
||||
where
|
||||
C: ConfigModel,
|
||||
I: SnapshotImporter<C>,
|
||||
{
|
||||
let ConfigCommand::ImportBegin {
|
||||
request_id,
|
||||
length,
|
||||
schema_id,
|
||||
crc32c,
|
||||
..
|
||||
} = command
|
||||
else {
|
||||
return self.rejected(command, ProtocolError::Internal);
|
||||
};
|
||||
if !I::IMPORT_SUPPORTED {
|
||||
return self.rejected(command, ProtocolError::ImportUnavailable);
|
||||
}
|
||||
if schema_id != C::SCHEMA_ID {
|
||||
return self.rejected(command, ProtocolError::SchemaMismatch);
|
||||
}
|
||||
if length == 0 || usize::from(length) > IMPORT_MAXIMUM_BYTES {
|
||||
return self.rejected(command, ProtocolError::CborTooLarge);
|
||||
}
|
||||
let Some(transfer_id) = self.allocate_transfer_id() else {
|
||||
return self.rejected(command, ProtocolError::Internal);
|
||||
};
|
||||
self.import = Some(ImportBuffer {
|
||||
owner,
|
||||
session_id: session.id,
|
||||
transfer_id,
|
||||
revision: session.revision,
|
||||
length,
|
||||
crc32c,
|
||||
received: [false; IMPORT_CHUNK_COUNT],
|
||||
data: [0; IMPORT_MAXIMUM_BYTES],
|
||||
});
|
||||
let mut reply = self.reply(
|
||||
request_id,
|
||||
session.id,
|
||||
session.revision,
|
||||
Operation::Import,
|
||||
ResultKind::Accepted,
|
||||
None,
|
||||
);
|
||||
reply.transfer_id = transfer_id;
|
||||
reply
|
||||
}
|
||||
|
||||
pub(super) fn import_chunk(&mut self, command: ConfigCommand, session: Session) -> ConfigReply {
|
||||
let ConfigCommand::ImportChunk {
|
||||
request_id,
|
||||
transfer_id,
|
||||
offset,
|
||||
length,
|
||||
data,
|
||||
..
|
||||
} = command
|
||||
else {
|
||||
return self.rejected(command, ProtocolError::Internal);
|
||||
};
|
||||
let offset = usize::from(offset);
|
||||
let length = usize::from(length);
|
||||
let Some(import) = self.import.as_mut() else {
|
||||
return self.rejected(command, ProtocolError::BadState);
|
||||
};
|
||||
if import.owner != session.owner
|
||||
|| import.session_id != session.id
|
||||
|| import.transfer_id != transfer_id
|
||||
{
|
||||
return self.rejected(command, ProtocolError::InvalidSession);
|
||||
}
|
||||
if import.revision != session.revision {
|
||||
return self.rejected(command, ProtocolError::StaleRevision);
|
||||
}
|
||||
let total = usize::from(import.length);
|
||||
let Some(end) = offset.checked_add(length) else {
|
||||
return self.rejected(command, ProtocolError::CborMalformed);
|
||||
};
|
||||
if length == 0
|
||||
|| offset % IMPORT_CHUNK_BYTES != 0
|
||||
|| end > total
|
||||
|| (length != IMPORT_CHUNK_BYTES && end != total)
|
||||
{
|
||||
return self.rejected(command, ProtocolError::CborMalformed);
|
||||
}
|
||||
let chunk = offset / IMPORT_CHUNK_BYTES;
|
||||
if import.received[chunk] {
|
||||
if import.data[offset..end] != data[..length] {
|
||||
return self.rejected(command, ProtocolError::RequestConflict);
|
||||
}
|
||||
} else {
|
||||
import.data[offset..end].copy_from_slice(&data[..length]);
|
||||
import.received[chunk] = true;
|
||||
}
|
||||
self.reply(
|
||||
request_id,
|
||||
session.id,
|
||||
session.revision,
|
||||
Operation::Import,
|
||||
ResultKind::Accepted,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn finish_import<C, S>(
|
||||
&mut self,
|
||||
manager: &mut ConfigManager<C, S>,
|
||||
command: ConfigCommand,
|
||||
session: Session,
|
||||
request_id: u32,
|
||||
transfer_id: u32,
|
||||
) -> ConfigReply
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
I: SnapshotImporter<C>,
|
||||
{
|
||||
let Some(import) = self.import else {
|
||||
return self.rejected(command, ProtocolError::BadState);
|
||||
};
|
||||
if import.owner != session.owner
|
||||
|| import.session_id != session.id
|
||||
|| import.transfer_id != transfer_id
|
||||
{
|
||||
return self.rejected(command, ProtocolError::InvalidSession);
|
||||
}
|
||||
if import.revision != session.revision {
|
||||
return self.rejected(command, ProtocolError::StaleRevision);
|
||||
}
|
||||
let total = usize::from(import.length);
|
||||
let chunks = total.div_ceil(IMPORT_CHUNK_BYTES);
|
||||
if import.received[..chunks].iter().any(|received| !received) {
|
||||
return self.rejected(command, ProtocolError::CborMalformed);
|
||||
}
|
||||
if crc32c(&import.data[..total]) != import.crc32c {
|
||||
return self.rejected(command, ProtocolError::BlobCrc);
|
||||
}
|
||||
let candidate = match self.importer.decode(&import.data[..total]) {
|
||||
Ok(candidate) => candidate,
|
||||
Err(error) => return self.rejected(command, error),
|
||||
};
|
||||
let Some(next_revision) = session.revision.checked_add(1) else {
|
||||
return self.rejected(command, ProtocolError::Internal);
|
||||
};
|
||||
match manager.replace_transaction(candidate) {
|
||||
Ok(()) => {
|
||||
let Some(session) = self.session.as_mut() else {
|
||||
return self.rejected(command, ProtocolError::Internal);
|
||||
};
|
||||
session.revision = next_revision;
|
||||
let (session_id, revision) = (session.id, session.revision);
|
||||
self.import = None;
|
||||
self.reply(
|
||||
request_id,
|
||||
session_id,
|
||||
revision,
|
||||
Operation::Import,
|
||||
ResultKind::Accepted,
|
||||
None,
|
||||
)
|
||||
}
|
||||
Err(CommitError::NoTransaction) => self.rejected(command, ProtocolError::BadState),
|
||||
Err(CommitError::InvalidCandidate(violation)) => {
|
||||
self.rejected(command, ProtocolError::Violation(violation))
|
||||
}
|
||||
Err(CommitError::Store(_)) => self.rejected(command, ProtocolError::Internal),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn crc32c(bytes: &[u8]) -> u32 {
|
||||
let mut crc = !0_u32;
|
||||
for byte in bytes {
|
||||
crc ^= u32::from(*byte);
|
||||
for _ in 0..8 {
|
||||
crc = if crc & 1 == 0 {
|
||||
crc >> 1
|
||||
} else {
|
||||
(crc >> 1) ^ 0x82f6_3b78
|
||||
};
|
||||
}
|
||||
}
|
||||
!crc
|
||||
}
|
||||
@@ -1,24 +1,39 @@
|
||||
//! Read-only protocol discovery and value-query responses.
|
||||
|
||||
use kaptan_config::{
|
||||
ConfigManager, ConfigModel, ConfigState, ConfigValue, FieldDescriptor, FieldId, Generation,
|
||||
ProfileStore,
|
||||
ConfigManager, ConfigModel, ConfigState, ConfigStore, ConfigValue, EnumOption, FieldDescriptor,
|
||||
FieldId,
|
||||
};
|
||||
|
||||
use super::{import::SnapshotImporter, session::ConfigProtocol};
|
||||
use crate::{ProtocolError, ValueSource};
|
||||
use super::session::ConfigProtocol;
|
||||
use crate::{Principal, ProtocolError, ValueSource};
|
||||
|
||||
/// Bounded descriptor page returned by a schema discovery request.
|
||||
/// One descriptor returned by a schema discovery request.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct DescriptorPage {
|
||||
pub struct DescriptorResponse {
|
||||
/// Request being answered.
|
||||
pub request_id: u32,
|
||||
/// First requested descriptor index.
|
||||
pub first: u16,
|
||||
/// Number of descriptors present in `descriptors`.
|
||||
pub len: u8,
|
||||
/// Fixed-capacity response page; `None` slots are unused.
|
||||
pub descriptors: [Option<&'static FieldDescriptor>; 4],
|
||||
/// Stable index of this descriptor in the advertised schema.
|
||||
pub index: u16,
|
||||
/// Self-describing field metadata.
|
||||
pub descriptor: FieldDescriptor,
|
||||
/// Factory value selected by the concrete board/image composition.
|
||||
pub default: ConfigValue,
|
||||
}
|
||||
|
||||
/// One named enum choice associated with a descriptor response.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct EnumOptionResponse {
|
||||
/// Request being answered.
|
||||
pub request_id: u32,
|
||||
/// Stable schema index of the owning descriptor.
|
||||
pub descriptor_index: u16,
|
||||
/// Stable field identity of the owning descriptor.
|
||||
pub field: FieldId,
|
||||
/// Stable zero-based order within the descriptor's choices.
|
||||
pub option_index: u8,
|
||||
/// Self-describing named choice.
|
||||
pub option: EnumOption,
|
||||
}
|
||||
|
||||
/// One typed value read from an explicitly selected configuration snapshot.
|
||||
@@ -41,20 +56,12 @@ pub struct ValueResponse {
|
||||
pub struct ConfigInfo {
|
||||
/// Request being answered.
|
||||
pub request_id: u32,
|
||||
/// Boot-active generation, or `None` for the factory profile.
|
||||
pub active_generation: Option<Generation>,
|
||||
/// Last committed generation, or `None` for the factory profile.
|
||||
pub committed_generation: Option<Generation>,
|
||||
/// Exact schema identifier expected by profile import.
|
||||
/// Stable schema identity for this target-described field registry.
|
||||
pub schema_id: [u8; 16],
|
||||
/// Version carried inside the one raw CBOR profile blob.
|
||||
pub profile_format: u16,
|
||||
/// Number of editable fields in the registry.
|
||||
pub field_count: u16,
|
||||
/// Current configuration lifecycle state.
|
||||
pub state: ConfigState,
|
||||
/// Deployment capabilities such as bounded CBOR import.
|
||||
pub capability_flags: u8,
|
||||
}
|
||||
|
||||
/// Exact response produced by the custom configuration service.
|
||||
@@ -64,13 +71,15 @@ pub enum ServiceResponse {
|
||||
Info(ConfigInfo),
|
||||
/// Completion for a mutating operation or rejected query.
|
||||
Result(crate::ConfigReply),
|
||||
/// Bounded self-describing schema page.
|
||||
Descriptors(DescriptorPage),
|
||||
/// One self-describing schema field.
|
||||
Descriptor(DescriptorResponse),
|
||||
/// One named choice for its explicitly identified enum descriptor.
|
||||
EnumOption(EnumOptionResponse),
|
||||
/// One typed scalar value.
|
||||
Value(ValueResponse),
|
||||
}
|
||||
|
||||
impl<I> ConfigProtocol<I> {
|
||||
impl ConfigProtocol {
|
||||
/// Returns static descriptor metadata by stable schema index.
|
||||
pub fn descriptor<C, S>(
|
||||
&self,
|
||||
@@ -79,7 +88,7 @@ impl<I> ConfigProtocol<I> {
|
||||
) -> Option<&'static FieldDescriptor>
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
manager.descriptor(index)
|
||||
}
|
||||
@@ -93,7 +102,7 @@ impl<I> ConfigProtocol<I> {
|
||||
) -> Option<ConfigValue>
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
match source {
|
||||
ValueSource::Active => manager.active_value(field),
|
||||
@@ -102,60 +111,65 @@ impl<I> ConfigProtocol<I> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn descriptors<C, S>(
|
||||
pub(super) fn descriptor_response<C, S>(
|
||||
&self,
|
||||
manager: &ConfigManager<C, S>,
|
||||
request_id: u32,
|
||||
first: u16,
|
||||
requested: u16,
|
||||
) -> DescriptorPage
|
||||
index: u16,
|
||||
) -> Option<DescriptorResponse>
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
let mut descriptors = [None; 4];
|
||||
let count = requested.min(descriptors.len() as u16);
|
||||
let mut len = 0;
|
||||
for offset in 0..count {
|
||||
let Some(index) = first.checked_add(offset) else {
|
||||
break;
|
||||
};
|
||||
let Some(descriptor) = manager.descriptor(index) else {
|
||||
break;
|
||||
};
|
||||
descriptors[usize::from(len)] = Some(descriptor);
|
||||
len += 1;
|
||||
}
|
||||
DescriptorPage {
|
||||
let descriptor = *manager.descriptor(index)?;
|
||||
let default = manager.factory_value(descriptor.id)?;
|
||||
Some(DescriptorResponse {
|
||||
request_id,
|
||||
first,
|
||||
len,
|
||||
descriptors,
|
||||
}
|
||||
index,
|
||||
descriptor,
|
||||
default,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn enum_option_response<C, S>(
|
||||
&self,
|
||||
manager: &ConfigManager<C, S>,
|
||||
request_id: u32,
|
||||
descriptor_index: u16,
|
||||
option_index: u8,
|
||||
) -> Option<EnumOptionResponse>
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
let descriptor = manager.descriptor(descriptor_index)?;
|
||||
let option = *descriptor.enum_options.get(usize::from(option_index))?;
|
||||
Some(EnumOptionResponse {
|
||||
request_id,
|
||||
descriptor_index,
|
||||
field: descriptor.id,
|
||||
option_index,
|
||||
option,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn info<C, S>(&self, manager: &ConfigManager<C, S>, request_id: u32) -> ConfigInfo
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
I: SnapshotImporter<C>,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
ConfigInfo {
|
||||
request_id,
|
||||
active_generation: manager.active_generation(),
|
||||
committed_generation: manager.committed_generation(),
|
||||
schema_id: C::SCHEMA_ID,
|
||||
profile_format: I::PROFILE_FORMAT,
|
||||
field_count: manager.field_count(),
|
||||
state: self.state,
|
||||
capability_flags: self.capability_flags_for::<C>(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn value_response<C, S>(
|
||||
&self,
|
||||
manager: &ConfigManager<C, S>,
|
||||
owner: u64,
|
||||
owner: Principal,
|
||||
request_id: u32,
|
||||
session_id: u32,
|
||||
field: FieldId,
|
||||
@@ -163,7 +177,7 @@ impl<I> ConfigProtocol<I> {
|
||||
) -> ServiceResponse
|
||||
where
|
||||
C: ConfigModel,
|
||||
S: ProfileStore<C>,
|
||||
S: ConfigStore<C>,
|
||||
{
|
||||
let (value, reply_session, revision) = match source {
|
||||
ValueSource::Active => (manager.active_value(field), 0, 0),
|
||||
@@ -176,7 +190,7 @@ impl<I> ConfigProtocol<I> {
|
||||
ProtocolError::InvalidSession,
|
||||
));
|
||||
};
|
||||
if session.id != session_id || session.owner != owner {
|
||||
if session.id != session_id || self.principal != Some(owner) {
|
||||
return ServiceResponse::Result(self.rejected_for(
|
||||
request_id,
|
||||
session_id,
|
||||
|
||||
@@ -3,34 +3,42 @@
|
||||
use kaptan_config::ConfigValue;
|
||||
|
||||
use super::session::ConfigProtocol;
|
||||
use crate::{ConfigCommand, ConfigReply, ProtocolError};
|
||||
|
||||
pub(super) const REQUEST_CACHE_CAPACITY: usize = 8;
|
||||
use crate::{ConfigCommand, ConfigReply, Operation, Principal, ProtocolError};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct RememberedResult {
|
||||
pub(super) owner: u64,
|
||||
pub(super) command: ConfigCommand,
|
||||
pub(super) reply: ConfigReply,
|
||||
pub(super) owner: Principal,
|
||||
}
|
||||
|
||||
impl<I> ConfigProtocol<I> {
|
||||
pub(super) fn cached(&self, owner: u64, command: ConfigCommand) -> Option<ConfigReply> {
|
||||
for cached in self.remembered.iter().flatten() {
|
||||
if cached.owner != owner || cached.command.request_id() != command.request_id() {
|
||||
continue;
|
||||
}
|
||||
if same_command(cached.command, command) {
|
||||
return Some(cached.reply);
|
||||
}
|
||||
return Some(self.rejected(command, ProtocolError::RequestConflict));
|
||||
impl ConfigProtocol {
|
||||
pub(super) fn cached(&self, owner: Principal, command: ConfigCommand) -> Option<ConfigReply> {
|
||||
let cached = self.remembered?;
|
||||
if cached.owner != owner || cached.command.request_id() != command.request_id() {
|
||||
return None;
|
||||
}
|
||||
if same_command(cached.command, command) {
|
||||
Some(cached.reply)
|
||||
} else {
|
||||
Some(self.rejected(command, ProtocolError::RequestConflict))
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn remember(&mut self, result: RememberedResult) {
|
||||
self.remembered[self.next_remembered] = Some(result);
|
||||
self.next_remembered = (self.next_remembered + 1) % REQUEST_CACHE_CAPACITY;
|
||||
pub(super) fn remember(
|
||||
&mut self,
|
||||
owner: Principal,
|
||||
command: ConfigCommand,
|
||||
reply: ConfigReply,
|
||||
) {
|
||||
if reply.operation == Operation::Query {
|
||||
return;
|
||||
}
|
||||
self.remembered = Some(RememberedResult {
|
||||
command,
|
||||
reply,
|
||||
owner,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,20 +57,36 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
|
||||
(
|
||||
ConfigCommand::DescriptorRequest {
|
||||
request_id: left_request,
|
||||
first: left_first,
|
||||
count: left_count,
|
||||
index: left_index,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::DescriptorRequest {
|
||||
request_id: right_request,
|
||||
first: right_first,
|
||||
count: right_count,
|
||||
index: right_index,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_first == right_first
|
||||
&& left_count == right_count
|
||||
&& left_index == right_index
|
||||
&& left_target == right_target
|
||||
}
|
||||
(
|
||||
ConfigCommand::EnumOptionRequest {
|
||||
request_id: left_request,
|
||||
descriptor_index: left_descriptor,
|
||||
option_index: left_option,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::EnumOptionRequest {
|
||||
request_id: right_request,
|
||||
descriptor_index: right_descriptor,
|
||||
option_index: right_option,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_descriptor == right_descriptor
|
||||
&& left_option == right_option
|
||||
&& left_target == right_target
|
||||
}
|
||||
(
|
||||
@@ -90,19 +114,13 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
|
||||
(
|
||||
ConfigCommand::Begin {
|
||||
request_id: left_request,
|
||||
expected_generation: left_generation,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::Begin {
|
||||
request_id: right_request,
|
||||
expected_generation: right_generation,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_generation == right_generation
|
||||
&& left_target == right_target
|
||||
}
|
||||
) => left_request == right_request && left_target == right_target,
|
||||
(
|
||||
ConfigCommand::Set {
|
||||
request_id: left_request,
|
||||
@@ -151,21 +169,18 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
|
||||
ConfigCommand::Commit {
|
||||
request_id: left_request,
|
||||
session_id: left_session,
|
||||
expected_generation: left_generation,
|
||||
expected_revision: left_revision,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::Commit {
|
||||
request_id: right_request,
|
||||
session_id: right_session,
|
||||
expected_generation: right_generation,
|
||||
expected_revision: right_revision,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_session == right_session
|
||||
&& left_generation == right_generation
|
||||
&& left_revision == right_revision
|
||||
&& left_target == right_target
|
||||
}
|
||||
@@ -185,84 +200,6 @@ fn same_command(left: ConfigCommand, right: ConfigCommand) -> bool {
|
||||
&& left_session == right_session
|
||||
&& left_target == right_target
|
||||
}
|
||||
(
|
||||
ConfigCommand::ImportBegin {
|
||||
request_id: left_request,
|
||||
session_id: left_session,
|
||||
expected_revision: left_revision,
|
||||
length: left_length,
|
||||
schema_id: left_schema,
|
||||
crc32c: left_crc,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::ImportBegin {
|
||||
request_id: right_request,
|
||||
session_id: right_session,
|
||||
expected_revision: right_revision,
|
||||
length: right_length,
|
||||
schema_id: right_schema,
|
||||
crc32c: right_crc,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_session == right_session
|
||||
&& left_revision == right_revision
|
||||
&& left_length == right_length
|
||||
&& left_schema == right_schema
|
||||
&& left_crc == right_crc
|
||||
&& left_target == right_target
|
||||
}
|
||||
(
|
||||
ConfigCommand::ImportChunk {
|
||||
request_id: left_request,
|
||||
session_id: left_session,
|
||||
transfer_id: left_transfer,
|
||||
offset: left_offset,
|
||||
length: left_length,
|
||||
data: left_data,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::ImportChunk {
|
||||
request_id: right_request,
|
||||
session_id: right_session,
|
||||
transfer_id: right_transfer,
|
||||
offset: right_offset,
|
||||
length: right_length,
|
||||
data: right_data,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_session == right_session
|
||||
&& left_transfer == right_transfer
|
||||
&& left_offset == right_offset
|
||||
&& left_length == right_length
|
||||
&& left_data == right_data
|
||||
&& left_target == right_target
|
||||
}
|
||||
(
|
||||
ConfigCommand::ImportFinish {
|
||||
request_id: left_request,
|
||||
session_id: left_session,
|
||||
transfer_id: left_transfer,
|
||||
expected_revision: left_revision,
|
||||
target: left_target,
|
||||
},
|
||||
ConfigCommand::ImportFinish {
|
||||
request_id: right_request,
|
||||
session_id: right_session,
|
||||
transfer_id: right_transfer,
|
||||
expected_revision: right_revision,
|
||||
target: right_target,
|
||||
},
|
||||
) => {
|
||||
left_request == right_request
|
||||
&& left_session == right_session
|
||||
&& left_transfer == right_transfer
|
||||
&& left_revision == right_revision
|
||||
&& left_target == right_target
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,23 +1,68 @@
|
||||
//! Generated Kaptan-dialect message translation.
|
||||
|
||||
use kaptan_config::{
|
||||
ApplyPolicy, ConfigState, ConfigValue, FieldDescriptor, FieldId, Generation, ValidationRule,
|
||||
ValueKind,
|
||||
ConfigState, ConfigValue, FieldDescriptor, FieldId, ValidationRule, ValueKind,
|
||||
};
|
||||
use kaptan_log::KaptanError;
|
||||
use kaptan_mavlink::message::Message;
|
||||
use kaptan_mavlink::message::kaptan::{
|
||||
KAPTAN_CONFIG_DESCRIPTOR_DATA, KAPTAN_CONFIG_INFO_DATA, KAPTAN_CONFIG_RESULT_DATA,
|
||||
KAPTAN_CONFIG_VALUE_DATA, KaptanConfigError, KaptanConfigOperation, KaptanConfigPolicy,
|
||||
KAPTAN_CONFIG_DESCRIPTOR_DATA, KAPTAN_CONFIG_ENUM_OPTION_DATA, KAPTAN_CONFIG_INFO_DATA,
|
||||
KAPTAN_CONFIG_RESULT_DATA, KAPTAN_CONFIG_VALUE_DATA, KaptanConfigError, KaptanConfigOperation,
|
||||
KaptanConfigResult, KaptanConfigState, KaptanConfigValueKind, KaptanConfigValueSource,
|
||||
MavMessage,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ConfigCommand, ConfigReply, Operation, ProtocolError, ResultKind, Target, ValueSource,
|
||||
service::{ConfigInfo, ValueResponse},
|
||||
ConfigCommand, ConfigReply, DESCRIPTOR_HAS_MAXIMUM, DESCRIPTOR_HAS_MINIMUM, Operation,
|
||||
PROTOCOL_MAJOR, PROTOCOL_MINOR, ProtocolError, ResultKind, Target, ValueSource,
|
||||
service::{ConfigInfo, EnumOptionResponse, ServiceResponse, ValueResponse},
|
||||
};
|
||||
|
||||
/// Transport-owned consumer of generated configuration response payloads.
|
||||
///
|
||||
/// Implementors retain ownership of MAVLink framing, sender sequence, queue
|
||||
/// classification, and physical transmission.
|
||||
pub trait ResponseVisitor {
|
||||
/// Transport-specific admission or serialization failure.
|
||||
type Error;
|
||||
|
||||
/// Visits one protocol/schema information response.
|
||||
fn info(&mut self, response: &KAPTAN_CONFIG_INFO_DATA) -> Result<(), Self::Error>;
|
||||
|
||||
/// Visits one authoritative operation result.
|
||||
fn result(&mut self, response: &KAPTAN_CONFIG_RESULT_DATA) -> Result<(), Self::Error>;
|
||||
|
||||
/// Visits one field descriptor.
|
||||
fn descriptor(&mut self, response: &KAPTAN_CONFIG_DESCRIPTOR_DATA) -> Result<(), Self::Error>;
|
||||
|
||||
/// Visits one named choice for an enum-backed field descriptor.
|
||||
fn enum_option(&mut self, response: &KAPTAN_CONFIG_ENUM_OPTION_DATA)
|
||||
-> Result<(), Self::Error>;
|
||||
|
||||
/// Visits one typed configuration value.
|
||||
fn value(&mut self, response: &KAPTAN_CONFIG_VALUE_DATA) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// Projects one transport-neutral service response into generated MAVLink
|
||||
/// payloads without allocating or assuming a board, schema, or field set.
|
||||
pub fn visit_response<V>(response: ServiceResponse, visitor: &mut V) -> Result<(), V::Error>
|
||||
where
|
||||
V: ResponseVisitor,
|
||||
{
|
||||
match response {
|
||||
ServiceResponse::Info(info) => visitor.info(&encode_info(info)),
|
||||
ServiceResponse::Result(result) => visitor.result(&encode_result(result)),
|
||||
ServiceResponse::Descriptor(response) => visitor.descriptor(&encode_descriptor(
|
||||
response.request_id,
|
||||
response.index,
|
||||
response.descriptor,
|
||||
response.default,
|
||||
)),
|
||||
ServiceResponse::EnumOption(response) => visitor.enum_option(&encode_enum_option(response)),
|
||||
ServiceResponse::Value(value) => visitor.value(&encode_value(value)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rejects a generated message that is not a Kaptan configuration command.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum DecodeError {
|
||||
@@ -35,8 +80,9 @@ pub enum DecodeError {
|
||||
|
||||
/// Converts one generated custom request into a transport-neutral command.
|
||||
///
|
||||
/// This only decodes a validated MAVLink payload. An authenticated ingress must
|
||||
/// still wrap the result in [`crate::Authorized`] before dispatch.
|
||||
/// This only decodes a validated MAVLink payload. CRC validity is not peer
|
||||
/// authentication; the platform composition must separately decide whether
|
||||
/// the ingress may dispatch the command and bind its transaction principal.
|
||||
pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError> {
|
||||
if matches!(message.target_system_id(), Some(0))
|
||||
|| matches!(message.target_component_id(), Some(0))
|
||||
@@ -51,8 +97,15 @@ pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError
|
||||
MavMessage::KAPTAN_CONFIG_DESCRIPTOR_REQUEST(data) => {
|
||||
Ok(ConfigCommand::DescriptorRequest {
|
||||
request_id: data.request_id,
|
||||
first: data.first,
|
||||
count: data.count,
|
||||
index: data.descriptor_index,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
})
|
||||
}
|
||||
MavMessage::KAPTAN_CONFIG_ENUM_OPTION_REQUEST(data) => {
|
||||
Ok(ConfigCommand::EnumOptionRequest {
|
||||
request_id: data.request_id,
|
||||
descriptor_index: data.descriptor_index,
|
||||
option_index: data.option_index,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
})
|
||||
}
|
||||
@@ -63,34 +116,8 @@ pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError
|
||||
source: value_source(data.source)?,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
}),
|
||||
MavMessage::KAPTAN_CONFIG_IMPORT_BEGIN(data) => Ok(ConfigCommand::ImportBegin {
|
||||
request_id: data.request_id,
|
||||
session_id: data.session_id,
|
||||
expected_revision: data.expected_revision,
|
||||
length: data.length,
|
||||
schema_id: data.schema_id,
|
||||
crc32c: data.crc32c,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
}),
|
||||
MavMessage::KAPTAN_CONFIG_IMPORT_CHUNK(data) => Ok(ConfigCommand::ImportChunk {
|
||||
request_id: data.request_id,
|
||||
session_id: data.session_id,
|
||||
transfer_id: data.transfer_id,
|
||||
offset: data.offset,
|
||||
length: data.length,
|
||||
data: data.data,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
}),
|
||||
MavMessage::KAPTAN_CONFIG_IMPORT_FINISH(data) => Ok(ConfigCommand::ImportFinish {
|
||||
request_id: data.request_id,
|
||||
session_id: data.session_id,
|
||||
transfer_id: data.transfer_id,
|
||||
expected_revision: data.expected_revision,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
}),
|
||||
MavMessage::KAPTAN_CONFIG_BEGIN(data) => Ok(ConfigCommand::Begin {
|
||||
request_id: data.request_id,
|
||||
expected_generation: data.expected_generation,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
}),
|
||||
MavMessage::KAPTAN_CONFIG_SET_F32(data) => Ok(ConfigCommand::Set {
|
||||
@@ -150,7 +177,6 @@ pub fn decode_command(message: &MavMessage) -> Result<ConfigCommand, DecodeError
|
||||
MavMessage::KAPTAN_CONFIG_COMMIT(data) => Ok(ConfigCommand::Commit {
|
||||
request_id: data.request_id,
|
||||
session_id: data.session_id,
|
||||
expected_generation: data.expected_generation,
|
||||
expected_revision: data.expected_revision,
|
||||
target: Target::new(data.target_system, data.target_component),
|
||||
}),
|
||||
@@ -191,10 +217,8 @@ const fn boolean(value: u8) -> Result<bool, DecodeError> {
|
||||
/// Encodes one authoritative service completion.
|
||||
pub fn encode_result(reply: ConfigReply) -> KAPTAN_CONFIG_RESULT_DATA {
|
||||
KAPTAN_CONFIG_RESULT_DATA {
|
||||
generation: reply.generation,
|
||||
request_id: reply.request_id,
|
||||
session_id: reply.session_id,
|
||||
transfer_id: reply.transfer_id,
|
||||
revision: reply.revision,
|
||||
primary_field: reply.primary_field(),
|
||||
related_field: reply.related_field(),
|
||||
@@ -209,16 +233,12 @@ pub fn encode_result(reply: ConfigReply) -> KAPTAN_CONFIG_RESULT_DATA {
|
||||
/// Encodes configuration protocol/schema status for a Kaptan control station.
|
||||
pub fn encode_info(info: ConfigInfo) -> KAPTAN_CONFIG_INFO_DATA {
|
||||
KAPTAN_CONFIG_INFO_DATA {
|
||||
active_generation: info.active_generation.map_or(0, Generation::get),
|
||||
committed_generation: info.committed_generation.map_or(0, Generation::get),
|
||||
request_id: info.request_id,
|
||||
schema_id: info.schema_id,
|
||||
profile_format: info.profile_format,
|
||||
field_count: info.field_count,
|
||||
protocol_major: 1,
|
||||
protocol_minor: 0,
|
||||
protocol_major: PROTOCOL_MAJOR,
|
||||
protocol_minor: PROTOCOL_MINOR,
|
||||
config_state: state(info.state),
|
||||
capability_flags: info.capability_flags,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,19 +247,34 @@ pub fn encode_descriptor(
|
||||
request_id: u32,
|
||||
descriptor_index: u16,
|
||||
descriptor: FieldDescriptor,
|
||||
default: ConfigValue,
|
||||
) -> KAPTAN_CONFIG_DESCRIPTOR_DATA {
|
||||
KAPTAN_CONFIG_DESCRIPTOR_DATA {
|
||||
minimum_bits: descriptor.minimum.map_or(0, ConfigValue::raw_bits),
|
||||
maximum_bits: descriptor.maximum.map_or(0, ConfigValue::raw_bits),
|
||||
default_bits: descriptor.default.raw_bits(),
|
||||
default_bits: default.raw_bits(),
|
||||
request_id,
|
||||
descriptor_index,
|
||||
field_id: descriptor.id.get(),
|
||||
enum_option_count: u8::try_from(descriptor.enum_options.len())
|
||||
.expect("validated enum option count fits u8"),
|
||||
display_key: descriptor.display_key.into(),
|
||||
unit: descriptor.unit.into(),
|
||||
value_kind: value_kind(descriptor.kind),
|
||||
apply_policy: policy(descriptor.apply_policy),
|
||||
flags: 0,
|
||||
flags: descriptor.minimum.map_or(0, |_| DESCRIPTOR_HAS_MINIMUM)
|
||||
| descriptor.maximum.map_or(0, |_| DESCRIPTOR_HAS_MAXIMUM),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes one generic named enum choice.
|
||||
pub fn encode_enum_option(response: EnumOptionResponse) -> KAPTAN_CONFIG_ENUM_OPTION_DATA {
|
||||
KAPTAN_CONFIG_ENUM_OPTION_DATA {
|
||||
value: response.option.0,
|
||||
request_id: response.request_id,
|
||||
descriptor_index: response.descriptor_index,
|
||||
field_id: response.field.get(),
|
||||
option_index: response.option_index,
|
||||
label: response.option.1.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,14 +301,6 @@ const fn value_kind(kind: ValueKind) -> KaptanConfigValueKind {
|
||||
}
|
||||
}
|
||||
|
||||
const fn policy(policy: ApplyPolicy) -> KaptanConfigPolicy {
|
||||
match policy {
|
||||
ApplyPolicy::Live => KaptanConfigPolicy::KAPTAN_CONFIG_POLICY_LIVE,
|
||||
ApplyPolicy::DisarmedReset => KaptanConfigPolicy::KAPTAN_CONFIG_POLICY_DISARMED_RESET,
|
||||
ApplyPolicy::RestartRequired => KaptanConfigPolicy::KAPTAN_CONFIG_POLICY_RESTART_REQUIRED,
|
||||
}
|
||||
}
|
||||
|
||||
const fn operation(operation: Operation) -> KaptanConfigOperation {
|
||||
match operation {
|
||||
Operation::Query => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_QUERY,
|
||||
@@ -282,7 +309,6 @@ const fn operation(operation: Operation) -> KaptanConfigOperation {
|
||||
Operation::Validate => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_VALIDATE,
|
||||
Operation::Commit => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_COMMIT,
|
||||
Operation::Abort => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_ABORT,
|
||||
Operation::Import => KaptanConfigOperation::KAPTAN_CONFIG_OPERATION_IMPORT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,12 +334,6 @@ const fn error(error: Option<ProtocolError>) -> KaptanConfigError {
|
||||
match error {
|
||||
None => KaptanConfigError::KAPTAN_CONFIG_ERROR_NONE,
|
||||
Some(ProtocolError::BadTarget) => KaptanConfigError::KAPTAN_CONFIG_ERROR_BAD_TARGET,
|
||||
Some(ProtocolError::AuthorizationDenied) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_AUTHORIZATION_DENIED
|
||||
}
|
||||
Some(ProtocolError::GenerationConflict) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_GENERATION_CONFLICT
|
||||
}
|
||||
Some(ProtocolError::SessionBusy) => KaptanConfigError::KAPTAN_CONFIG_ERROR_SESSION_BUSY,
|
||||
Some(ProtocolError::InvalidSession) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_INVALID_SESSION
|
||||
@@ -326,6 +346,7 @@ const fn error(error: Option<ProtocolError>) -> KaptanConfigError {
|
||||
Some(ProtocolError::UnknownField(_)) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNKNOWN_FIELD
|
||||
}
|
||||
Some(ProtocolError::InvalidQuery) => KaptanConfigError::KAPTAN_CONFIG_ERROR_INVALID_QUERY,
|
||||
Some(ProtocolError::TypeMismatch(_)) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_TYPE_MISMATCH
|
||||
}
|
||||
@@ -339,36 +360,88 @@ const fn error(error: Option<ProtocolError>) -> KaptanConfigError {
|
||||
ValidationRule::UnsupportedCapability => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNSUPPORTED_CAPABILITY
|
||||
}
|
||||
ValidationRule::UnsupportedFormat => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_UNSUPPORTED_FORMAT
|
||||
ValidationRule::UnsupportedFormat => KaptanConfigError::KAPTAN_CONFIG_ERROR_INTERNAL,
|
||||
ValidationRule::InvalidEnumValue => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_INVALID_ENUM_VALUE
|
||||
}
|
||||
},
|
||||
Some(ProtocolError::StorageFailure) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_STORAGE_FAILURE
|
||||
}
|
||||
Some(ProtocolError::CborMalformed) => KaptanConfigError::KAPTAN_CONFIG_ERROR_CBOR_MALFORMED,
|
||||
Some(ProtocolError::CborTooLarge) => KaptanConfigError::KAPTAN_CONFIG_ERROR_CBOR_TOO_LARGE,
|
||||
Some(ProtocolError::BlobCrc) => KaptanConfigError::KAPTAN_CONFIG_ERROR_BLOB_CRC,
|
||||
Some(ProtocolError::SchemaMismatch) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_SCHEMA_MISMATCH
|
||||
}
|
||||
Some(ProtocolError::ImportUnavailable) => {
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_IMPORT_UNAVAILABLE
|
||||
}
|
||||
Some(ProtocolError::Internal) => KaptanConfigError::KAPTAN_CONFIG_ERROR_INTERNAL,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kaptan_config::{ConfigState, ConfigViolation, ValidationRule};
|
||||
use kaptan_config::{
|
||||
ConfigState, ConfigValue, ConfigViolation, FieldDescriptor, ValidationRule, ValueKind,
|
||||
};
|
||||
use kaptan_mavlink::message::kaptan::{
|
||||
KAPTAN_CONFIG_INFO_REQUEST_DATA, KAPTAN_CONFIG_SET_F32_DATA, MavMessage,
|
||||
KAPTAN_CONFIG_ENUM_OPTION_REQUEST_DATA, KAPTAN_CONFIG_INFO_REQUEST_DATA,
|
||||
KAPTAN_CONFIG_SET_F32_DATA, MavMessage,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::CAPABILITY_CBOR_IMPORT;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Emitted {
|
||||
Info(KAPTAN_CONFIG_INFO_DATA),
|
||||
Result(KAPTAN_CONFIG_RESULT_DATA),
|
||||
Descriptor(KAPTAN_CONFIG_DESCRIPTOR_DATA),
|
||||
EnumOption(KAPTAN_CONFIG_ENUM_OPTION_DATA),
|
||||
Value(KAPTAN_CONFIG_VALUE_DATA),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RecordingVisitor {
|
||||
emitted: std::vec::Vec<Emitted>,
|
||||
}
|
||||
|
||||
impl ResponseVisitor for RecordingVisitor {
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn info(&mut self, response: &KAPTAN_CONFIG_INFO_DATA) -> Result<(), Self::Error> {
|
||||
self.emitted.push(Emitted::Info(response.clone()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn result(&mut self, response: &KAPTAN_CONFIG_RESULT_DATA) -> Result<(), Self::Error> {
|
||||
self.emitted.push(Emitted::Result(response.clone()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn descriptor(
|
||||
&mut self,
|
||||
response: &KAPTAN_CONFIG_DESCRIPTOR_DATA,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.emitted.push(Emitted::Descriptor(response.clone()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enum_option(
|
||||
&mut self,
|
||||
response: &KAPTAN_CONFIG_ENUM_OPTION_DATA,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.emitted.push(Emitted::EnumOption(response.clone()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn value(&mut self, response: &KAPTAN_CONFIG_VALUE_DATA) -> Result<(), Self::Error> {
|
||||
self.emitted.push(Emitted::Value(response.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const FIRST_DESCRIPTOR: FieldDescriptor = FieldDescriptor {
|
||||
id: FieldId::new(17),
|
||||
display_key: "future.sensor.rate_hz",
|
||||
kind: ValueKind::U32,
|
||||
minimum: Some(ConfigValue::U32(1)),
|
||||
maximum: Some(ConfigValue::U32(200)),
|
||||
enum_options: &[],
|
||||
unit: "Hz",
|
||||
};
|
||||
#[test]
|
||||
fn generated_set_message_preserves_typed_value_and_identifiers() {
|
||||
let message = MavMessage::KAPTAN_CONFIG_SET_F32(KAPTAN_CONFIG_SET_F32_DATA {
|
||||
@@ -409,6 +482,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_enum_option_request_preserves_the_explicit_index() {
|
||||
let message =
|
||||
MavMessage::KAPTAN_CONFIG_ENUM_OPTION_REQUEST(KAPTAN_CONFIG_ENUM_OPTION_REQUEST_DATA {
|
||||
request_id: 41,
|
||||
descriptor_index: 3,
|
||||
option_index: 6,
|
||||
target_system: 1,
|
||||
target_component: 2,
|
||||
});
|
||||
assert_eq!(
|
||||
decode_command(&message),
|
||||
Ok(ConfigCommand::EnumOptionRequest {
|
||||
request_id: 41,
|
||||
descriptor_index: 3,
|
||||
option_index: 6,
|
||||
target: Target::new(1, 2),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_config_requests_reject_broadcast_targets() {
|
||||
let zero_system = MavMessage::KAPTAN_CONFIG_SET_F32(KAPTAN_CONFIG_SET_F32_DATA {
|
||||
@@ -444,9 +538,7 @@ mod tests {
|
||||
let reply = ConfigReply {
|
||||
request_id: 1,
|
||||
session_id: 2,
|
||||
transfer_id: 0,
|
||||
revision: 3,
|
||||
generation: 4,
|
||||
operation: Operation::Set,
|
||||
result: ResultKind::Rejected,
|
||||
error: Some(ProtocolError::Violation(ConfigViolation::field(
|
||||
@@ -465,37 +557,118 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_import_has_its_own_structured_error_code() {
|
||||
let reply = ConfigReply {
|
||||
request_id: 1,
|
||||
session_id: 0,
|
||||
transfer_id: 0,
|
||||
revision: 0,
|
||||
generation: 0,
|
||||
operation: Operation::Import,
|
||||
result: ResultKind::Rejected,
|
||||
error: Some(ProtocolError::ImportUnavailable),
|
||||
fn info_preserves_schema_and_protocol_identity() {
|
||||
let info = encode_info(ConfigInfo {
|
||||
request_id: 7,
|
||||
schema_id: [0; 16],
|
||||
field_count: 23,
|
||||
state: ConfigState::Active,
|
||||
};
|
||||
});
|
||||
assert_eq!(info.request_id, 7);
|
||||
assert_eq!(info.schema_id, [0; 16]);
|
||||
assert_eq!(
|
||||
encode_result(reply).error,
|
||||
KaptanConfigError::KAPTAN_CONFIG_ERROR_IMPORT_UNAVAILABLE
|
||||
(info.protocol_major, info.protocol_minor),
|
||||
(PROTOCOL_MAJOR, PROTOCOL_MINOR)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_preserves_service_capabilities() {
|
||||
let info = encode_info(ConfigInfo {
|
||||
fn descriptor_flags_identify_present_bounds() {
|
||||
let descriptor = FieldDescriptor {
|
||||
id: FieldId::new(9),
|
||||
display_key: "test.value",
|
||||
kind: ValueKind::U32,
|
||||
minimum: Some(ConfigValue::U32(1)),
|
||||
maximum: Some(ConfigValue::U32(10)),
|
||||
enum_options: &[],
|
||||
unit: "count",
|
||||
};
|
||||
assert_eq!(
|
||||
encode_descriptor(1, 0, descriptor, ConfigValue::U32(2)).flags,
|
||||
DESCRIPTOR_HAS_MINIMUM | DESCRIPTOR_HAS_MAXIMUM
|
||||
);
|
||||
assert_eq!(
|
||||
encode_descriptor(1, 0, descriptor, ConfigValue::U32(2)).enum_option_count,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_visitor_projects_every_singleton_response_variant() {
|
||||
let info = ConfigInfo {
|
||||
request_id: 7,
|
||||
active_generation: None,
|
||||
committed_generation: None,
|
||||
schema_id: [0; 16],
|
||||
profile_format: 1,
|
||||
field_count: 23,
|
||||
state: ConfigState::Active,
|
||||
capability_flags: CAPABILITY_CBOR_IMPORT,
|
||||
});
|
||||
assert_eq!(info.request_id, 7);
|
||||
assert_eq!(info.capability_flags, CAPABILITY_CBOR_IMPORT);
|
||||
schema_id: *b"UNKNOWN-SCHEMA01",
|
||||
field_count: 2,
|
||||
state: ConfigState::RestartRequired,
|
||||
};
|
||||
let result = ConfigReply {
|
||||
request_id: 8,
|
||||
session_id: 9,
|
||||
revision: 2,
|
||||
operation: Operation::Commit,
|
||||
result: ResultKind::CommittedRestartRequired,
|
||||
error: None,
|
||||
state: ConfigState::RestartRequired,
|
||||
};
|
||||
let value = ValueResponse {
|
||||
request_id: 10,
|
||||
session_id: 0,
|
||||
revision: 0,
|
||||
field: FieldId::new(401),
|
||||
value: ConfigValue::Bool(true),
|
||||
};
|
||||
let enum_option = crate::EnumOptionResponse {
|
||||
request_id: 9,
|
||||
descriptor_index: 12,
|
||||
field: FieldId::new(17),
|
||||
option_index: 1,
|
||||
option: (3, "Error"),
|
||||
};
|
||||
let mut visitor = RecordingVisitor::default();
|
||||
|
||||
assert_eq!(
|
||||
visit_response(ServiceResponse::Info(info), &mut visitor),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
visit_response(ServiceResponse::Result(result), &mut visitor),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
visit_response(
|
||||
ServiceResponse::Descriptor(crate::DescriptorResponse {
|
||||
request_id: 9,
|
||||
index: 12,
|
||||
descriptor: FIRST_DESCRIPTOR,
|
||||
default: ConfigValue::U32(20),
|
||||
}),
|
||||
&mut visitor,
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
visit_response(ServiceResponse::EnumOption(enum_option), &mut visitor),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
visit_response(ServiceResponse::Value(value), &mut visitor),
|
||||
Ok(())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
visitor.emitted,
|
||||
std::vec![
|
||||
Emitted::Info(encode_info(info)),
|
||||
Emitted::Result(encode_result(result)),
|
||||
Emitted::Descriptor(encode_descriptor(
|
||||
9,
|
||||
12,
|
||||
FIRST_DESCRIPTOR,
|
||||
ConfigValue::U32(20),
|
||||
)),
|
||||
Emitted::EnumOption(encode_enum_option(enum_option)),
|
||||
Emitted::Value(encode_value(value)),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user