use serde::{Deserialize, Serialize};
use crate::identity::ParticipantArtifactId;
use crate::version::FrameworkVersion;
pub const MAX_RUNTIME_PARTICIPANTS: usize = 64;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ParticipantContract {
pub framework: FrameworkVersion,
pub id: ParticipantArtifactId,
pub kind: ParticipantKind,
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::*;
fn record(fields: &str) -> Vec<u8> {
format!(
r#"{{"schema":"phoxal/participant-metadata/v0","framework":"0.57.2","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.framework, FrameworkVersion::new(0, 57, 2));
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","framework":"0.57.2","id":"drive","kind":"service","requirement":null,"config_schema":null}"#;
assert!(ParticipantMetadata::from_bytes(bytes).is_err());
}
#[test]
fn a_non_canonical_framework_version_is_rejected() {
for framework in ["\"v0.57.2\"", "\"0.57\"", "\"0.57.2-rc.1\"", "null"] {
let bytes = format!(
r#"{{"schema":"phoxal/participant-metadata/v0","framework":{framework},"id":"drive","kind":"service","requirement":null,"config_schema":null}}"#
)
.into_bytes();
assert!(
ParticipantMetadata::from_bytes(&bytes).is_err(),
"framework {framework} must not parse"
);
}
}
#[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_its_framework_version_is_rejected() {
let bytes = br#"{"schema":"phoxal/participant-metadata/v0","id":"drive","kind":"service","requirement":null,"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
);
}
}