mod emit;
pub use emit::{ParticipantContractRecord, ParticipantMetadataRecord, connection_json};
use serde::{Deserialize, Serialize};
use crate::__compat::wire::{
DescribeWire, EnumRepresentation, FieldPresence, VariantBody, WireField, WireSchema,
WireVariant,
};
use crate::identity::ParticipantArtifactId;
use crate::model::connection::ConnectionKind;
use crate::version::FrameworkVersion;
pub const PARTICIPANT_METADATA_SCHEMA_TAG: &str = "phoxal/participant-metadata/v0";
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ParticipantContract {
pub framework: FrameworkVersion,
pub id: ParticipantArtifactId,
pub kind: ParticipantKind,
pub connection: Option<ConnectionKind>,
pub config_schema: serde_json::Value,
}
impl DescribeWire for ParticipantContract {
fn wire_schema() -> WireSchema {
WireSchema::structure([
WireField::required("framework", FrameworkVersion::wire_schema()),
WireField::required("id", ParticipantArtifactId::wire_schema()),
WireField::required("kind", ParticipantKind::wire_schema()),
WireField::new(
"connection",
Option::<ConnectionKind>::wire_schema(),
FieldPresence::Defaulted,
),
WireField::required("config_schema", serde_json::Value::wire_schema()),
])
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ParticipantKind {
Service,
Driver,
Brain,
}
impl ParticipantKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
ParticipantKind::Service => "service",
ParticipantKind::Driver => "driver",
ParticipantKind::Brain => "brain",
}
}
const ALL: [Self; 3] = [Self::Service, Self::Driver, Self::Brain];
}
impl DescribeWire for ParticipantKind {
fn wire_schema() -> WireSchema {
WireSchema::enumeration(
EnumRepresentation::ExternallyTagged,
ParticipantKind::ALL.map(|kind| WireVariant::new(kind.as_str(), VariantBody::Unit)),
)
}
}
#[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 DescribeWire for ParticipantMetadata {
fn wire_schema() -> WireSchema {
WireSchema::enumeration(
EnumRepresentation::InternallyTagged {
tag: String::from("schema"),
},
[WireVariant::new(
PARTICIPANT_METADATA_SCHEMA_TAG,
VariantBody::newtype(ParticipantContract::wire_schema()),
)],
)
}
}
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);
#[doc(hidden)]
pub mod __compat {
use super::{PARTICIPANT_METADATA_SCHEMA_TAG, ParticipantMetadata};
use crate::__compat::surface::{ContractRecord, ContractSurface};
use crate::__compat::wire::DescribeWire;
#[must_use]
pub fn contract_surface() -> String {
let mut records = Vec::new();
contract_records(&mut records);
ContractSurface::new(records).canonical_json()
}
pub(crate) fn contract_records(out: &mut Vec<ContractRecord>) {
out.push(ContractRecord::document(
"ParticipantMetadata",
PARTICIPANT_METADATA_SCHEMA_TAG,
ParticipantMetadata::wire_schema(),
));
}
#[cfg(test)]
mod tests {
use super::contract_surface;
#[test]
fn the_surface_is_deterministic_json_naming_the_metadata_document() {
let rendered = contract_surface();
serde_json::from_str::<serde_json::Value>(&rendered).expect("the surface is JSON");
assert_eq!(contract_surface(), rendered);
assert!(
rendered.contains(super::PARTICIPANT_METADATA_SCHEMA_TAG),
"{rendered}"
);
assert!(rendered.contains(r#""record":"document""#), "{rendered}");
assert!(rendered.contains("config_schema"), "{rendered}");
assert!(rendered.contains("connection"), "{rendered}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn record(fields: &str) -> Vec<u8> {
format!(r#"{{"schema":"phoxal/participant-metadata/v0","framework":"0.57.2",{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","connection":null,"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.connection, 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","connection":null,"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 a_declared_connection_parses_as_the_typed_kind() {
let ParticipantMetadata::V0 { contract } = ParticipantMetadata::from_bytes(&record(
r#""id":"ddsm115","kind":"driver","connection":"serial","config_schema":{"type":"null"}"#,
))
.expect("the exact document a declared driver embeds must parse");
assert_eq!(contract.kind, ParticipantKind::Driver);
assert_eq!(contract.connection, Some(ConnectionKind::Serial));
}
#[test]
fn the_kind_wire_token_is_the_serde_rename() {
for kind in ParticipantKind::ALL {
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","connection":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","connection":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","connection":null,"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","connection":null,"config_schema":null}"#;
assert!(ParticipantMetadata::from_bytes(bytes).is_err());
}
#[test]
fn the_declared_document_shape_is_the_shape_the_writer_emits() {
for connection in [None, Some(ConnectionKind::Serial)] {
let emitted = crate::participant::metadata::ParticipantMetadataRecord::V0 {
contract: crate::participant::metadata::ParticipantContractRecord {
framework: FrameworkVersion::CURRENT,
id: "drive",
kind: ParticipantKind::Service,
connection,
config_schema: serde_json::json!({"type": "null"}),
},
};
let json = serde_json::to_value(&emitted).expect("the writer's record serializes");
assert_eq!(ParticipantMetadata::wire_schema().conforms(&json), Ok(()));
}
assert_eq!(
ParticipantMetadata::wire_schema(),
crate::participant::metadata::ParticipantMetadataRecord::wire_schema()
);
}
}