use serde_json::Value;
pub struct NodeDescriptor {
pub node_type: &'static str,
pub crate_name: &'static str,
pub description: &'static str,
pub default_run_target: &'static str,
pub catalog_tags: &'static [&'static str],
pub schema: fn() -> Value,
}
#[doc(hidden)]
pub struct NodeDescriptorThunk(pub fn() -> Option<NodeDescriptor>);
inventory::collect!(NodeDescriptorThunk);
pub fn registered_nodes() -> impl Iterator<Item = NodeDescriptor> {
inventory::iter::<NodeDescriptorThunk>
.into_iter()
.filter_map(|thunk| (thunk.0)())
}
pub fn json_schema_for<T: schemars::JsonSchema>() -> Value {
let root_schema = schemars::SchemaGenerator::default().into_root_schema_for::<T>();
serde_json::to_value(root_schema).expect("schemars Schema always serializes to JSON")
}
#[macro_export]
macro_rules! register_node {
($node_type:ty) => {
$crate::inventory::submit! {
$crate::node_registry::NodeDescriptorThunk(|| {
let crate_name = <$node_type as $crate::node::NodeImpl>::CRATE_NAME?;
let node_type = <$node_type as $crate::node::NodeImpl>::NODE_TYPE?;
Some($crate::node_registry::NodeDescriptor {
node_type: node_type.as_str(),
crate_name,
description: <$node_type as $crate::node::NodeImpl>::DESCRIPTION,
default_run_target: <$node_type as $crate::node::NodeImpl>::DEFAULT_RUN_TARGET,
catalog_tags: <$node_type as $crate::node::NodeImpl>::CATALOG_TAGS,
schema: $crate::node_registry::json_schema_for::<
<$node_type as $crate::node::NodeImpl>::Config,
>,
})
})
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::Context;
use crate::error::Result;
use crate::node::NodeImpl;
use async_trait::async_trait;
#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
struct UnregisteredConfig;
#[derive(Debug)]
struct UnregisteredNode;
#[async_trait]
impl NodeImpl for UnregisteredNode {
type Config = UnregisteredConfig;
async fn init(_config: Self::Config) -> Result<Self> {
Ok(Self)
}
async fn run(&mut self, _ctx: &Context) -> Result<()> {
Ok(())
}
}
crate::register_node!(UnregisteredNode);
#[derive(Debug, Default, serde::Deserialize, schemars::JsonSchema)]
struct RegisteredConfig {
#[allow(dead_code)]
rate_hz: f32,
}
#[derive(Debug)]
struct RegisteredNode;
#[async_trait]
impl NodeImpl for RegisteredNode {
type Config = RegisteredConfig;
const NODE_TYPE: Option<crate::node::NodeType> = Some(crate::node::NodeType::Camera);
const CRATE_NAME: Option<&'static str> = Some("mecha10-nodes-fake-for-test");
const DESCRIPTION: &'static str = "A fake node used only to exercise registration.";
const CATALOG_TAGS: &'static [&'static str] = &["fake", "test"];
async fn init(_config: Self::Config) -> Result<Self> {
Ok(Self)
}
async fn run(&mut self, _ctx: &Context) -> Result<()> {
Ok(())
}
}
crate::register_node!(RegisteredNode);
#[test]
fn registers_node_with_crate_name_and_node_type_set() {
let found = registered_nodes().find(|d| d.crate_name == "mecha10-nodes-fake-for-test");
let descriptor = found.expect("RegisteredNode should have registered an entry");
assert_eq!(descriptor.node_type, "camera");
assert_eq!(
descriptor.description,
"A fake node used only to exercise registration."
);
assert_eq!(descriptor.default_run_target, "robot");
assert_eq!(descriptor.catalog_tags, &["fake", "test"]);
let schema = (descriptor.schema)();
assert!(schema.is_object(), "schema should serialize to a JSON object");
}
#[test]
fn registered_node_catalog_tags_round_trip_through_register_node_macro() {
let found = registered_nodes().find(|d| d.crate_name == "mecha10-nodes-fake-for-test");
let descriptor = found.expect("RegisteredNode should have registered an entry");
assert_eq!(
descriptor.catalog_tags,
RegisteredNode::CATALOG_TAGS,
"registered descriptor's catalog_tags should match the NodeImpl's CATALOG_TAGS const"
);
}
#[test]
fn registers_exactly_once_even_though_an_unregistered_node_also_called_the_macro() {
let matches = registered_nodes()
.filter(|d| d.crate_name == "mecha10-nodes-fake-for-test")
.count();
assert_eq!(matches, 1);
}
}