use crate::validation::{ValidationError, ValidationResult};
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct TypeId(String);
impl TypeId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for TypeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for TypeId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
pub type JsonValidatorFn = fn(&Value) -> ValidationResult<()>;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldFormat {
Text,
Number { unit: Option<&'static str> },
Percentage,
Boolean,
Timestamp,
Position,
Enum { variants: &'static [&'static str] },
Nested { nested_type_id: TypeId },
List { item_format: Box<FieldFormat> },
JsonString,
BlobRef,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FieldDescriptor {
pub name: &'static str,
pub label: &'static str,
pub format: FieldFormat,
}
impl FieldDescriptor {
pub fn new(name: &'static str, label: &'static str, format: FieldFormat) -> Self {
Self {
name,
label,
format,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct TypeDescriptor {
pub id: TypeId,
pub name: String,
pub version: String,
pub canonical_collection: Option<String>,
pub validate_json: JsonValidatorFn,
pub fields: Vec<FieldDescriptor>,
}
impl TypeDescriptor {
pub fn new(
id: TypeId,
name: impl Into<String>,
version: impl Into<String>,
validate_json: JsonValidatorFn,
) -> Self {
Self {
id,
name: name.into(),
version: version.into(),
canonical_collection: None,
validate_json,
fields: Vec::new(),
}
}
}
pub trait TypeRegistry: Send + Sync {
fn get(&self, id: &TypeId) -> Option<&TypeDescriptor>;
fn for_collection(&self, collection: &str) -> Option<&TypeDescriptor>;
fn iter(&self) -> Box<dyn Iterator<Item = &TypeDescriptor> + '_>;
}
#[derive(Debug, Default, Clone)]
pub struct BuiltinRegistry {
by_id: HashMap<TypeId, Arc<TypeDescriptor>>,
by_collection: HashMap<String, Arc<TypeDescriptor>>,
}
impl BuiltinRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, desc: TypeDescriptor) -> &mut Self {
let arc = Arc::new(desc);
if let Some(coll) = arc.canonical_collection.as_ref() {
self.by_collection.insert(coll.clone(), Arc::clone(&arc));
}
self.by_id.insert(arc.id.clone(), arc);
self
}
pub fn with_peat_schema_types() -> Self {
let mut r = Self::new();
r.register(descriptors::capability());
r.register(descriptors::node_config());
r.register(descriptors::node_state());
r.register(descriptors::cell_config());
r.register(descriptors::cell_state());
r
}
}
impl TypeRegistry for BuiltinRegistry {
fn get(&self, id: &TypeId) -> Option<&TypeDescriptor> {
self.by_id.get(id).map(|a| a.as_ref())
}
fn for_collection(&self, collection: &str) -> Option<&TypeDescriptor> {
self.by_collection.get(collection).map(|a| a.as_ref())
}
fn iter(&self) -> Box<dyn Iterator<Item = &TypeDescriptor> + '_> {
Box::new(self.by_id.values().map(|a| a.as_ref()))
}
}
mod descriptors {
use super::*;
pub fn capability() -> TypeDescriptor {
fn validate(value: &Value) -> ValidationResult<()> {
let msg = crate::capability::v1::Capability::deserialize(value).map_err(|e| {
ValidationError::InvalidValue(format!("could not deserialise as Capability: {e}"))
})?;
crate::validation::validate_capability(&msg)
}
const CAPABILITY_TYPE_VARIANTS: &[&str] = &[
"Unspecified",
"Sensor",
"Compute",
"Communication",
"Mobility",
"Payload",
"Emergent",
];
TypeDescriptor {
id: TypeId::new("peat.capability.v1.Capability"),
name: "Capability".to_string(),
version: "v1".to_string(),
canonical_collection: Some("capabilities".to_string()),
validate_json: validate,
fields: vec![
FieldDescriptor {
name: "id",
label: "ID",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "name",
label: "Name",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "capability_type",
label: "Type",
format: FieldFormat::Enum {
variants: CAPABILITY_TYPE_VARIANTS,
},
},
FieldDescriptor {
name: "confidence",
label: "Confidence",
format: FieldFormat::Percentage,
},
FieldDescriptor {
name: "metadata_json",
label: "Metadata",
format: FieldFormat::JsonString,
},
FieldDescriptor {
name: "registered_at",
label: "Registered",
format: FieldFormat::Timestamp,
},
],
}
}
pub fn node_config() -> TypeDescriptor {
fn validate(value: &Value) -> ValidationResult<()> {
let msg = crate::node::v1::NodeConfig::deserialize(value).map_err(|e| {
ValidationError::InvalidValue(format!("could not deserialise as NodeConfig: {e}"))
})?;
crate::validation::validate_node_config(&msg)
}
TypeDescriptor {
id: TypeId::new("peat.node.v1.NodeConfig"),
name: "NodeConfig".to_string(),
version: "v1".to_string(),
canonical_collection: Some("node-configs".to_string()),
validate_json: validate,
fields: vec![
FieldDescriptor {
name: "id",
label: "ID",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "platform_type",
label: "Platform",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "capabilities",
label: "Capabilities",
format: FieldFormat::List {
item_format: Box::new(FieldFormat::Nested {
nested_type_id: TypeId::new("peat.capability.v1.Capability"),
}),
},
},
FieldDescriptor {
name: "comm_range_m",
label: "Comm Range",
format: FieldFormat::Number { unit: Some("m") },
},
FieldDescriptor {
name: "max_speed_mps",
label: "Max Speed",
format: FieldFormat::Number { unit: Some("m/s") },
},
FieldDescriptor {
name: "operator_binding",
label: "Operator",
format: FieldFormat::JsonString,
},
FieldDescriptor {
name: "created_at",
label: "Created",
format: FieldFormat::Timestamp,
},
],
}
}
pub fn node_state() -> TypeDescriptor {
fn validate(value: &Value) -> ValidationResult<()> {
let msg = crate::node::v1::NodeState::deserialize(value).map_err(|e| {
ValidationError::InvalidValue(format!("could not deserialise as NodeState: {e}"))
})?;
crate::validation::validate_node_state(&msg)
}
const HEALTH_STATUS_VARIANTS: &[&str] =
&["Unspecified", "Nominal", "Degraded", "Critical", "Failed"];
const PHASE_VARIANTS: &[&str] = &["Unspecified", "Discovery", "Cell", "Hierarchy"];
TypeDescriptor {
id: TypeId::new("peat.node.v1.NodeState"),
name: "NodeState".to_string(),
version: "v1".to_string(),
canonical_collection: Some("node-states".to_string()),
validate_json: validate,
fields: vec![
FieldDescriptor {
name: "position",
label: "Position",
format: FieldFormat::Position,
},
FieldDescriptor {
name: "fuel_minutes",
label: "Fuel",
format: FieldFormat::Number { unit: Some("min") },
},
FieldDescriptor {
name: "health",
label: "Health",
format: FieldFormat::Enum {
variants: HEALTH_STATUS_VARIANTS,
},
},
FieldDescriptor {
name: "phase",
label: "Phase",
format: FieldFormat::Enum {
variants: PHASE_VARIANTS,
},
},
FieldDescriptor {
name: "cell_id",
label: "Cell",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "zone_id",
label: "Zone",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "timestamp",
label: "Updated",
format: FieldFormat::Timestamp,
},
],
}
}
pub fn cell_config() -> TypeDescriptor {
fn validate(value: &Value) -> ValidationResult<()> {
let msg = crate::cell::v1::CellConfig::deserialize(value).map_err(|e| {
ValidationError::InvalidValue(format!("could not deserialise as CellConfig: {e}"))
})?;
crate::validation::validate_cell_config(&msg)
}
TypeDescriptor {
id: TypeId::new("peat.cell.v1.CellConfig"),
name: "CellConfig".to_string(),
version: "v1".to_string(),
canonical_collection: Some("cell-configs".to_string()),
validate_json: validate,
fields: vec![
FieldDescriptor {
name: "id",
label: "ID",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "max_size",
label: "Max Size",
format: FieldFormat::Number { unit: None },
},
FieldDescriptor {
name: "min_size",
label: "Min Size",
format: FieldFormat::Number { unit: None },
},
FieldDescriptor {
name: "created_at",
label: "Created",
format: FieldFormat::Timestamp,
},
],
}
}
pub fn cell_state() -> TypeDescriptor {
fn validate(value: &Value) -> ValidationResult<()> {
let msg = crate::cell::v1::CellState::deserialize(value).map_err(|e| {
ValidationError::InvalidValue(format!("could not deserialise as CellState: {e}"))
})?;
crate::validation::validate_cell_state(&msg)
}
TypeDescriptor {
id: TypeId::new("peat.cell.v1.CellState"),
name: "CellState".to_string(),
version: "v1".to_string(),
canonical_collection: Some("cell-states".to_string()),
validate_json: validate,
fields: vec![
FieldDescriptor {
name: "config",
label: "Config",
format: FieldFormat::Nested {
nested_type_id: TypeId::new("peat.cell.v1.CellConfig"),
},
},
FieldDescriptor {
name: "leader_id",
label: "Leader",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "members",
label: "Members",
format: FieldFormat::List {
item_format: Box::new(FieldFormat::Text),
},
},
FieldDescriptor {
name: "capabilities",
label: "Capabilities",
format: FieldFormat::List {
item_format: Box::new(FieldFormat::Nested {
nested_type_id: TypeId::new("peat.capability.v1.Capability"),
}),
},
},
FieldDescriptor {
name: "platoon_id",
label: "Platoon",
format: FieldFormat::Text,
},
FieldDescriptor {
name: "timestamp",
label: "Updated",
format: FieldFormat::Timestamp,
},
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn typeid_display_and_str() {
let id = TypeId::new("peat.capability.v1.Capability");
assert_eq!(id.as_str(), "peat.capability.v1.Capability");
assert_eq!(id.to_string(), "peat.capability.v1.Capability");
}
#[test]
fn builtin_registry_resolves_core_types_by_id() {
let r = BuiltinRegistry::with_peat_schema_types();
for id in [
"peat.capability.v1.Capability",
"peat.node.v1.NodeConfig",
"peat.node.v1.NodeState",
"peat.cell.v1.CellConfig",
"peat.cell.v1.CellState",
] {
let desc = r.get(&TypeId::new(id));
assert!(desc.is_some(), "missing core type: {id}");
assert_eq!(desc.unwrap().id.as_str(), id);
}
}
#[test]
fn builtin_registry_resolves_by_canonical_collection() {
let r = BuiltinRegistry::with_peat_schema_types();
assert_eq!(
r.for_collection("capabilities").map(|d| d.id.as_str()),
Some("peat.capability.v1.Capability")
);
assert_eq!(
r.for_collection("node-configs").map(|d| d.id.as_str()),
Some("peat.node.v1.NodeConfig")
);
assert!(r.for_collection("unknown-collection").is_none());
}
#[test]
fn iter_lists_all_registered_types() {
let r = BuiltinRegistry::with_peat_schema_types();
let ids: Vec<&str> = r.iter().map(|d| d.id.as_str()).collect();
assert!(ids.contains(&"peat.capability.v1.Capability"));
assert!(ids.contains(&"peat.node.v1.NodeConfig"));
assert!(ids.contains(&"peat.cell.v1.CellState"));
assert_eq!(ids.len(), 5);
}
fn capability_json(confidence: f32) -> serde_json::Value {
json!({
"id": "cap-1",
"name": "thermal-sensor",
"capability_type": 0,
"confidence": confidence,
"metadata_json": "{}",
"registered_at": null,
})
}
#[test]
fn capability_json_validator_accepts_well_formed_input() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("capabilities").unwrap();
let result = (desc.validate_json)(&capability_json(0.95));
assert!(result.is_ok(), "expected ok, got {result:?}");
}
#[test]
fn capability_json_validator_rejects_invalid_confidence() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("capabilities").unwrap();
let err =
(desc.validate_json)(&capability_json(1.5)).expect_err("expected validation error");
assert!(matches!(err, ValidationError::InvalidConfidence(_)));
}
#[test]
fn capability_json_validator_rejects_malformed_json() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("capabilities").unwrap();
let value = json!({"not_a_capability": true});
let err = (desc.validate_json)(&value).expect_err("expected deserialise error");
match err {
ValidationError::InvalidValue(msg) => assert!(
msg.contains("Capability"),
"expected message naming Capability; got {msg}"
),
other => panic!("expected InvalidValue, got {other:?}"),
}
}
#[test]
fn consumer_can_extend_with_own_types() {
fn always_ok(_v: &Value) -> ValidationResult<()> {
Ok(())
}
let mut custom = TypeDescriptor::new(
TypeId::new("example.app.v1.Widget"),
"Widget",
"v1",
always_ok,
);
custom.canonical_collection = Some("widgets".to_string());
custom.fields = vec![FieldDescriptor::new("label", "Label", FieldFormat::Text)];
let mut r = BuiltinRegistry::new();
r.register(custom);
assert!(r.get(&TypeId::new("example.app.v1.Widget")).is_some());
assert_eq!(
r.for_collection("widgets").map(|d| d.id.as_str()),
Some("example.app.v1.Widget")
);
}
#[test]
fn core_types_carry_field_metadata() {
let r = BuiltinRegistry::with_peat_schema_types();
let cap = r.for_collection("capabilities").unwrap();
let field_names: Vec<&str> = cap.fields.iter().map(|f| f.name).collect();
assert_eq!(
field_names,
vec![
"id",
"name",
"capability_type",
"confidence",
"metadata_json",
"registered_at"
]
);
let confidence = cap.fields.iter().find(|f| f.name == "confidence").unwrap();
assert_eq!(confidence.format, FieldFormat::Percentage);
let cap_type = cap
.fields
.iter()
.find(|f| f.name == "capability_type")
.unwrap();
match &cap_type.format {
FieldFormat::Enum { variants } => {
assert_eq!(variants[0], "Unspecified");
assert_eq!(variants[1], "Sensor");
assert_eq!(variants[5], "Payload");
}
other => panic!("expected Enum format, got {other:?}"),
}
}
#[test]
fn node_state_position_and_units_are_typed() {
let r = BuiltinRegistry::with_peat_schema_types();
let ns = r.for_collection("node-states").unwrap();
let position = ns.fields.iter().find(|f| f.name == "position").unwrap();
assert_eq!(position.format, FieldFormat::Position);
let fuel = ns.fields.iter().find(|f| f.name == "fuel_minutes").unwrap();
assert_eq!(fuel.format, FieldFormat::Number { unit: Some("min") });
}
#[test]
fn nested_descriptors_link_via_type_id() {
let r = BuiltinRegistry::with_peat_schema_types();
let nc = r.for_collection("node-configs").unwrap();
let caps = nc.fields.iter().find(|f| f.name == "capabilities").unwrap();
match &caps.format {
FieldFormat::List { item_format } => match item_format.as_ref() {
FieldFormat::Nested { nested_type_id } => {
assert_eq!(nested_type_id.as_str(), "peat.capability.v1.Capability");
assert!(r.get(nested_type_id).is_some());
}
other => panic!("expected Nested item format, got {other:?}"),
},
other => panic!("expected List format, got {other:?}"),
}
}
mod fixtures {
use serde_json::{json, Value};
pub fn node_config() -> Value {
json!({
"id": "node-1",
"platform_type": "UAV",
"capabilities": [],
"comm_range_m": 1000.0,
"max_speed_mps": 10.0,
"operator_binding": null,
"created_at": null,
})
}
pub fn node_state() -> Value {
json!({
"position": {
"latitude": 38.0,
"longitude": -122.0,
"altitude": 0.0,
},
"fuel_minutes": 60,
"health": 1, "phase": 1, "cell_id": null,
"zone_id": null,
"timestamp": null,
})
}
pub fn cell_config() -> Value {
json!({
"id": "cell-1",
"max_size": 8,
"min_size": 2,
"created_at": null,
})
}
pub fn cell_state() -> Value {
json!({
"config": {
"id": "cell-1",
"max_size": 8,
"min_size": 2,
"created_at": null,
},
"leader_id": null,
"members": [],
"capabilities": [],
"platoon_id": null,
"timestamp": null,
})
}
}
#[test]
fn node_config_validator_accepts_well_formed_input() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("node-configs").unwrap();
let result = (desc.validate_json)(&fixtures::node_config());
assert!(result.is_ok(), "expected ok, got {result:?}");
}
#[test]
fn node_state_validator_accepts_well_formed_input() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("node-states").unwrap();
let result = (desc.validate_json)(&fixtures::node_state());
assert!(result.is_ok(), "expected ok, got {result:?}");
}
#[test]
fn cell_config_validator_accepts_well_formed_input() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("cell-configs").unwrap();
let result = (desc.validate_json)(&fixtures::cell_config());
assert!(result.is_ok(), "expected ok, got {result:?}");
}
#[test]
fn cell_state_validator_accepts_well_formed_input() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("cell-states").unwrap();
let result = (desc.validate_json)(&fixtures::cell_state());
assert!(result.is_ok(), "expected ok, got {result:?}");
}
#[test]
fn node_state_validator_rejects_out_of_range_latitude() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("node-states").unwrap();
let mut bad = fixtures::node_state();
bad["position"]["latitude"] = serde_json::json!(95.0);
let err = (desc.validate_json)(&bad).expect_err("expected validation error");
match err {
ValidationError::InvalidValue(msg) => {
assert!(msg.contains("latitude"), "got {msg}");
}
other => panic!("expected InvalidValue (range), got {other:?}"),
}
}
#[test]
fn cell_config_validator_rejects_undersize_min() {
let r = BuiltinRegistry::with_peat_schema_types();
let desc = r.for_collection("cell-configs").unwrap();
let mut bad = fixtures::cell_config();
bad["min_size"] = serde_json::json!(1);
let err = (desc.validate_json)(&bad).expect_err("expected validation error");
assert!(
matches!(err, ValidationError::ConstraintViolation(_)),
"{err:?}"
);
}
#[test]
fn every_nested_reference_in_builtin_registry_resolves() {
let r = BuiltinRegistry::with_peat_schema_types();
fn collect_nested_refs(fmt: &FieldFormat, out: &mut Vec<TypeId>) {
match fmt {
FieldFormat::Nested { nested_type_id } => out.push(nested_type_id.clone()),
FieldFormat::List { item_format } => {
collect_nested_refs(item_format, out);
}
_ => {}
}
}
let mut unresolved = Vec::new();
for desc in r.iter() {
for field in &desc.fields {
let mut refs = Vec::new();
collect_nested_refs(&field.format, &mut refs);
for nested in refs {
if r.get(&nested).is_none() {
unresolved.push(format!(
"{}::{} → {} (unregistered)",
desc.id, field.name, nested
));
}
}
}
}
assert!(
unresolved.is_empty(),
"Builtin registry has unresolved Nested references:\n {}",
unresolved.join("\n ")
);
}
#[test]
fn registry_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BuiltinRegistry>();
fn _accepts(_: &dyn TypeRegistry) {}
}
}