use serde::{Deserialize, Serialize};
use crate::identity::ParticipantArtifactId;
use crate::version::{BusAbi, LaunchAbi, RobotApiVersion, RuntimeSchema};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ParticipantSchemas {
pub bus: BusAbi,
pub launch: LaunchAbi,
pub runtime: RuntimeSchema,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ParticipantContract {
pub id: ParticipantArtifactId,
pub kind: ParticipantKind,
pub api: RobotApiVersion,
pub schemas: ParticipantSchemas,
pub requirement: Option<ParticipantRequirement>,
pub config_schema: serde_json::Value,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ParticipantKind {
Service,
Driver,
Simulator,
Brain,
}
impl ParticipantKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
ParticipantKind::Service => "service",
ParticipantKind::Driver => "driver",
ParticipantKind::Simulator => "simulator",
ParticipantKind::Brain => "brain",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ParticipantRequirement {
DifferentialDriveVelocity,
}
impl ParticipantRequirement {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::DifferentialDriveVelocity => "differential_drive_velocity",
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(tag = "schema", deny_unknown_fields)]
pub enum ParticipantMetadata {
#[serde(rename = "phoxal/participant-metadata/v0")]
V0 {
#[serde(flatten)]
contract: ParticipantContract,
},
}
impl ParticipantMetadata {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, MetadataError> {
serde_json::from_slice(bytes).map_err(MetadataError)
}
#[must_use]
pub const fn contract(&self) -> &ParticipantContract {
match self {
Self::V0 { contract } => contract,
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("participant metadata is not a readable phoxal document: {0}")]
pub struct MetadataError(#[from] serde_json::Error);
#[cfg(test)]
mod tests {
use super::*;
const SCHEMAS: &str = r#"{"bus":"phoxal/bus-abi/v0","launch":"phoxal/participant-launch/v0","runtime":"phoxal/runtime-bundle/v0"}"#;
fn record(fields: &str) -> Vec<u8> {
format!(
r#"{{"schema":"phoxal/participant-metadata/v0","api":"phoxal/robot-api/v0.1","schemas":{SCHEMAS},"requirement":null,{fields}}}"#
)
.into_bytes()
}
#[test]
fn a_v0_record_parses_into_the_canonical_artifact_contract() {
let ParticipantMetadata::V0 { contract } = ParticipantMetadata::from_bytes(&record(
r#""id":"drive","kind":"service","config_schema":{"type":"null"}"#,
))
.expect("the exact document a role macro embeds must parse");
assert_eq!(contract.api, RobotApiVersion::new(0, 1));
assert_eq!(contract.schemas.bus, BusAbi::V0);
assert_eq!(contract.schemas.launch, LaunchAbi::V0);
assert_eq!(contract.schemas.runtime, RuntimeSchema::V0);
assert_eq!(contract.id.as_str(), "drive");
assert_eq!(contract.kind, ParticipantKind::Service);
assert_eq!(contract.requirement, None);
assert_eq!(contract.config_schema, serde_json::json!({"type": "null"}));
}
#[test]
fn the_root_brain_kind_is_distinct_from_a_service() {
let metadata = ParticipantMetadata::from_bytes(&record(
r#""id":"brain","kind":"brain","config_schema":{"type":"null"}"#,
))
.expect("the exact document `#[phoxal::brain]` embeds must parse");
let contract = metadata.contract();
assert_eq!(contract.id.as_str(), "brain");
assert_eq!(contract.kind, ParticipantKind::Brain);
assert_ne!(contract.kind, ParticipantKind::Service);
}
#[test]
fn the_kind_wire_token_is_the_serde_rename() {
for kind in [
ParticipantKind::Service,
ParticipantKind::Driver,
ParticipantKind::Simulator,
ParticipantKind::Brain,
] {
let json = serde_json::to_string(&kind).expect("a unit variant serializes");
assert_eq!(json, format!("\"{}\"", kind.as_str()));
}
}
#[test]
fn an_unknown_schema_tag_is_rejected() {
let bytes = br#"{"schema":"phoxal/participant-metadata/v1","api":"phoxal/robot-api/v0.1","schemas":{"bus":"phoxal/bus-abi/v0","launch":"phoxal/participant-launch/v0","runtime":"phoxal/runtime-bundle/v0"},"id":"drive","kind":"service","config_schema":null}"#;
assert!(ParticipantMetadata::from_bytes(bytes).is_err());
}
#[test]
fn a_future_robot_api_identity_is_preserved() {
let bytes = format!(
r#"{{"schema":"phoxal/participant-metadata/v0","api":"phoxal/robot-api/v0.3","schemas":{SCHEMAS},"id":"drive","kind":"service","config_schema":null}}"#
)
.into_bytes();
let metadata = ParticipantMetadata::from_bytes(&bytes)
.expect("the process boundary keeps a validated API identity open");
assert_eq!(metadata.contract().api, RobotApiVersion::new(0, 3));
}
#[test]
fn an_unknown_field_is_rejected() {
assert!(
ParticipantMetadata::from_bytes(&record(
r#""id":"drive","kind":"service","config_schema":null,"extra":true"#,
))
.is_err()
);
}
#[test]
fn a_record_missing_a_runtime_schema_is_rejected() {
let bytes = br#"{"schema":"phoxal/participant-metadata/v0","api":"phoxal/robot-api/v0.1","schemas":{"bus":"phoxal/bus-abi/v0","launch":"phoxal/participant-launch/v0"},"id":"drive","kind":"service","config_schema":null}"#;
assert!(ParticipantMetadata::from_bytes(bytes).is_err());
}
#[test]
fn requirement_tokens_round_trip() {
let requirement = ParticipantRequirement::DifferentialDriveVelocity;
let json = serde_json::to_string(&requirement).expect("requirement serializes");
assert_eq!(json, format!("\"{}\"", requirement.as_str()));
assert_eq!(
serde_json::from_str::<ParticipantRequirement>(&json).expect("requirement parses"),
requirement
);
}
}