use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, ItemEnum, ItemStruct, Token};
use crate::util::{body_derives, phoxal};
mod kw {
syn::custom_keyword!(version);
syn::custom_keyword!(extends);
syn::custom_keyword!(topic);
syn::custom_keyword!(pubsub);
syn::custom_keyword!(query);
}
pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
let tree: ApiTree = syn::parse2(input)?;
tree.expand()
}
struct ApiTree {
versions: Vec<Version>,
}
struct Version {
name: Ident,
extends: Option<Ident>,
nodes: Vec<Node>,
}
#[derive(Clone)]
struct Node {
name: Ident,
var: Option<Ident>,
types: Vec<TypeDef>,
topics: Vec<TopicDef>,
children: Vec<Node>,
}
#[derive(Clone)]
enum TypeDef {
Struct(ItemStruct),
Enum(ItemEnum),
}
impl TypeDef {
fn ident(&self) -> &Ident {
match self {
TypeDef::Struct(item) => &item.ident,
TypeDef::Enum(item) => &item.ident,
}
}
}
#[derive(Clone)]
struct TopicDef {
leaf: Ident,
kind: TopicKind,
}
#[derive(Clone)]
enum TopicKind {
PubSub(Ident),
Query { request: Ident, response: Ident },
}
impl Parse for ApiTree {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut versions = Vec::new();
while !input.is_empty() {
versions.push(input.parse()?);
}
if versions.is_empty() {
return Err(input.error("phoxal_api_tree! requires at least one `version` block"));
}
Ok(ApiTree { versions })
}
}
impl Parse for Version {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<kw::version>()?;
let name: Ident = input.parse()?;
let extends = if input.peek(kw::extends) {
input.parse::<kw::extends>()?;
Some(input.parse::<Ident>()?)
} else {
None
};
let body;
syn::braced!(body in input);
let mut nodes = Vec::new();
while !body.is_empty() {
nodes.push(body.parse()?);
}
Ok(Version {
name,
extends,
nodes,
})
}
}
impl Parse for Node {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
let var = if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let var: Ident = content.parse()?;
if !content.is_empty() {
return Err(content.error(
"a dynamic node binds exactly one variable, e.g. `motor(capability) { … }`",
));
}
Some(var)
} else {
None
};
let body;
syn::braced!(body in input);
let mut types = Vec::new();
let mut topics = Vec::new();
let mut children = Vec::new();
while !body.is_empty() {
let attrs = body.call(syn::Attribute::parse_outer)?;
if body.peek(kw::topic) {
if let Some(attr) = attrs.first() {
return Err(syn::Error::new_spanned(
attr,
"attributes are not allowed on a `topic` declaration",
));
}
topics.push(body.parse()?);
} else if body.peek(Token![struct]) {
let mut item: ItemStruct = body.parse()?;
item.attrs = attrs;
item.vis = syn::Visibility::Public(syn::token::Pub::default());
types.push(TypeDef::Struct(item));
} else if body.peek(Token![enum]) {
let mut item: ItemEnum = body.parse()?;
item.attrs = attrs;
item.vis = syn::Visibility::Public(syn::token::Pub::default());
types.push(TypeDef::Enum(item));
} else if body.peek(Ident)
&& (body.peek2(syn::token::Paren) || body.peek2(syn::token::Brace))
{
if let Some(attr) = attrs.first() {
return Err(syn::Error::new_spanned(
attr,
"attributes are not allowed on a child node declaration",
));
}
children.push(body.parse()?);
} else {
return Err(body.error(
"expected `struct`, `enum`, `topic …;`, or a child node `name { … }` / \
`name(var) { … }` inside an API node block",
));
}
}
Ok(Node {
name,
var,
types,
topics,
children,
})
}
}
impl Parse for TopicDef {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<kw::topic>()?;
let leaf: Ident = input.parse()?;
input.parse::<Token![:]>()?;
let kind = if input.peek(kw::pubsub) {
input.parse::<kw::pubsub>()?;
let body: Ident = input.parse()?;
TopicKind::PubSub(body)
} else if input.peek(kw::query) {
input.parse::<kw::query>()?;
let request: Ident = input.parse()?;
input.parse::<Token![=>]>()?;
let response: Ident = input.parse()?;
TopicKind::Query { request, response }
} else {
return Err(input.error("expected `pubsub <Type>` or `query <Req> => <Resp>`"));
};
input.parse::<Token![;]>()?;
Ok(TopicDef { leaf, kind })
}
}
impl ApiTree {
fn expand(&self) -> syn::Result<TokenStream> {
let mut out = TokenStream::new();
let mut resolved: std::collections::HashMap<String, Vec<Node>> =
std::collections::HashMap::new();
for version in &self.versions {
let effective = match &version.extends {
None => version.nodes.clone(),
Some(parent) => {
let base = resolved.get(&parent.to_string()).ok_or_else(|| {
syn::Error::new_spanned(
parent,
format!(
"`extends {parent}`: unknown API version (it must be declared \
earlier in the same phoxal_api_tree! invocation)"
),
)
})?;
overlay_nodes(base, &version.nodes)?
}
};
resolved.insert(version.name.to_string(), effective.clone());
out.extend(expand_version(&version.name, &effective)?);
}
Ok(out)
}
}
fn overlay_nodes(base: &[Node], child: &[Node]) -> syn::Result<Vec<Node>> {
let mut result: Vec<Node> = base.to_vec();
for cn in child {
if let Some(bn) = result.iter_mut().find(|n| n.name == cn.name) {
if bn.var.as_ref().map(Ident::to_string) != cn.var.as_ref().map(Ident::to_string) {
return Err(syn::Error::new_spanned(
&cn.name,
format!(
"`extends`: node `{}` redeclares its dynamic variable differently from \
the inherited version; a same-named node must keep the same `(var)` (or \
its absence) so inherited topic keys stay stable",
cn.name
),
));
}
for ct in &cn.types {
if let Some(slot) = bn.types.iter_mut().find(|t| t.ident() == ct.ident()) {
*slot = ct.clone();
} else {
bn.types.push(ct.clone());
}
}
for ctp in &cn.topics {
if let Some(slot) = bn.topics.iter_mut().find(|t| t.leaf == ctp.leaf) {
*slot = ctp.clone();
} else {
bn.topics.push(ctp.clone());
}
}
bn.children = overlay_nodes(&bn.children, &cn.children)?;
} else {
result.push(cn.clone());
}
}
Ok(result)
}
fn expand_version(name: &Ident, nodes: &[Node]) -> syn::Result<TokenStream> {
let phoxal = phoxal();
let mod_name = name;
let id = name.to_string();
let mut node_mods = TokenStream::new();
for node in nodes {
node_mods.extend(expand_node_module(node, 1, "", "")?);
}
let topic_mod = expand_topic_module(nodes)?;
Ok(quote! {
pub mod #mod_name {
#[derive(Clone, Copy, Debug)]
pub enum Api {}
impl #phoxal::api::ApiVersion for Api {
const ID: &'static str = #id;
}
#node_mods
#topic_mod
}
})
}
fn expand_node_module(
node: &Node,
depth: usize,
family_prefix: &str,
key_prefix: &str,
) -> syn::Result<TokenStream> {
let phoxal = phoxal();
let name = &node.name;
let name_str = name.to_string();
let derives = body_derives();
let family_path = join_seg(family_prefix, "::", &name_str);
let key_seg = match &node.var {
Some(var) => format!("{}/{{{}}}", name_str, var),
None => name_str.clone(),
};
let node_key_prefix = join_seg(key_prefix, "/", &key_seg);
let api_supers = supers(depth);
let mut types = TokenStream::new();
for ty in &node.types {
match ty {
TypeDef::Struct(item) => {
let item = with_pub_fields_struct(item.clone());
types.extend(quote! { #derives #item });
}
TypeDef::Enum(item) => {
types.extend(quote! { #derives #item });
}
}
}
let mut impls = TokenStream::new();
for topic in &node.topics {
let key = format!("{}/{}", node_key_prefix, topic.leaf);
match &topic.kind {
TopicKind::PubSub(body) => {
let family_const = format!("{}::{}", family_path, body);
impls.extend(quote! {
impl #phoxal::api::ContractBody for #body {
type Api = #api_supers Api;
const FAMILY: &'static str = #family_const;
const TOPIC: &'static str = #key;
}
});
}
TopicKind::Query { request, response } => {
let req_family = format!("{}::{}", family_path, request);
let resp_family = format!("{}::{}", family_path, response);
impls.extend(quote! {
impl #phoxal::api::ContractBody for #request {
type Api = #api_supers Api;
const FAMILY: &'static str = #req_family;
const TOPIC: &'static str = #key;
}
impl #phoxal::api::ContractBody for #response {
type Api = #api_supers Api;
const FAMILY: &'static str = #resp_family;
const TOPIC: &'static str = #key;
}
});
}
}
}
let mut child_mods = TokenStream::new();
for child in &node.children {
child_mods.extend(expand_node_module(
child,
depth + 1,
&family_path,
&node_key_prefix,
)?);
}
Ok(quote! {
pub mod #name {
#types
#impls
#child_mods
}
})
}
fn supers(n: usize) -> TokenStream {
let mut ts = TokenStream::new();
for _ in 0..n {
ts.extend(quote! { super:: });
}
ts
}
fn seg_field(i: usize) -> Ident {
quote::format_ident!("__seg{}", i)
}
fn join_seg(prefix: &str, sep: &str, seg: &str) -> String {
if prefix.is_empty() {
seg.to_string()
} else {
format!("{prefix}{sep}{seg}")
}
}
fn expand_topic_module(nodes: &[Node]) -> syn::Result<TokenStream> {
let mut root_methods = TokenStream::new();
let mut builder_mods = TokenStream::new();
for node in nodes {
root_methods.extend(node_entry_method(node));
builder_mods.extend(expand_builder_module(node, &[])?);
}
Ok(quote! {
pub mod topic {
pub fn new() -> Root {
Root
}
pub struct Root;
impl Root {
#root_methods
}
#builder_mods
}
})
}
#[derive(Clone)]
struct NodeSeg {
name: Ident,
var: Option<Ident>,
}
fn node_entry_method(node: &Node) -> TokenStream {
let name = &node.name;
let name_str = name.to_string();
let target = quote!(#name::Builder);
match &node.var {
Some(var) => quote! {
#[doc = #name_str]
pub fn #name(self, #var: impl ::core::fmt::Display) -> #target {
#name::Builder::__from(self, #var.to_string())
}
},
None => quote! {
#[doc = #name_str]
pub fn #name(self) -> #target {
#name::Builder::__from(self)
}
},
}
}
fn expand_builder_module(node: &Node, ancestors: &[NodeSeg]) -> syn::Result<TokenStream> {
let phoxal = phoxal();
let name = &node.name;
let name_str = name.to_string();
let depth = ancestors.len() + 1;
let mut path: Vec<NodeSeg> = ancestors.to_vec();
path.push(NodeSeg {
name: name.clone(),
var: node.var.clone(),
});
let vars: Vec<&Ident> = path.iter().filter_map(|s| s.var.as_ref()).collect();
let ancestor_vars: Vec<&Ident> = ancestors.iter().filter_map(|s| s.var.as_ref()).collect();
let field_idents: Vec<Ident> = (0..vars.len()).map(seg_field).collect();
let ancestor_field_idents: Vec<Ident> = (0..ancestor_vars.len()).map(seg_field).collect();
let field_decls: Vec<TokenStream> = field_idents
.iter()
.map(|f| quote! { pub(super) #f: String })
.collect();
let parent_builder_ty = if depth == 1 {
quote! { super::Root }
} else {
quote! { super::Builder }
};
let parent_fields: Vec<TokenStream> = ancestor_field_idents
.iter()
.map(|f| quote! { #f: __parent.#f })
.collect();
let parent_pat = if ancestor_vars.is_empty() {
quote! { _parent }
} else {
quote! { __parent }
};
let ctor = match &node.var {
Some(var) => {
let new_field = seg_field(ancestor_vars.len());
quote! {
pub(super) fn __from(#parent_pat: #parent_builder_ty, #var: String) -> Self {
Self { #(#parent_fields,)* #new_field: #var }
}
}
}
None => quote! {
pub(super) fn __from(#parent_pat: #parent_builder_ty) -> Self {
Self { #(#parent_fields,)* }
}
},
};
let mut leaf_methods = TokenStream::new();
for topic in &node.topics {
let leaf = &topic.leaf;
let kind_ty = builder_leaf_kind(topic, &path, depth);
let (fmt_str, doc_key) = builder_leaf_key_parts(&path, &topic.leaf);
let constructor = if field_idents.is_empty() {
quote! { #phoxal::bus::Topic::new_static(#fmt_str) }
} else {
quote! {
#phoxal::bus::Topic::new_owned(::std::format!(#fmt_str, #(self.#field_idents),*))
}
};
leaf_methods.extend(quote! {
#[doc = #doc_key]
pub fn #leaf(self) -> #phoxal::bus::Topic<#kind_ty> {
#constructor
}
});
}
let mut child_methods = TokenStream::new();
let mut child_mods = TokenStream::new();
for child in &node.children {
child_methods.extend(node_entry_method(child));
child_mods.extend(expand_builder_module(child, &path)?);
}
let builder_doc = format!("Topic builder for the `{name_str}` node.");
Ok(quote! {
pub mod #name {
#[doc = #builder_doc]
pub struct Builder {
#(#field_decls,)*
}
impl Builder {
#ctor
#leaf_methods
#child_methods
}
#child_mods
}
})
}
fn builder_leaf_kind(topic: &TopicDef, path: &[NodeSeg], depth: usize) -> TokenStream {
let phoxal = phoxal();
let up = supers(depth + 1);
let node_path: Vec<&Ident> = path.iter().map(|s| &s.name).collect();
let body_path = |body: &Ident| quote! { #up #(#node_path::)* #body };
match &topic.kind {
TopicKind::PubSub(body) => {
let b = body_path(body);
quote! { #phoxal::bus::PubSub<#b> }
}
TopicKind::Query { request, response } => {
let req = body_path(request);
let resp = body_path(response);
quote! { #phoxal::bus::Query<#req, #resp> }
}
}
}
fn builder_leaf_key_parts(path: &[NodeSeg], leaf: &Ident) -> (String, String) {
let mut fmt_segs = Vec::new();
let mut doc_segs = Vec::new();
for seg in path {
let name = seg.name.to_string();
match &seg.var {
Some(var) => {
fmt_segs.push(format!("{name}/{{}}"));
doc_segs.push(format!("{name}/{{{var}}}"));
}
None => {
fmt_segs.push(name.clone());
doc_segs.push(name);
}
}
}
let leaf = leaf.to_string();
(
format!("{}/{}", fmt_segs.join("/"), leaf),
format!("{}/{}", doc_segs.join("/"), leaf),
)
}
fn with_pub_fields_struct(mut item: ItemStruct) -> ItemStruct {
if let syn::Fields::Named(named) = &mut item.fields {
for field in &mut named.named {
field.vis = syn::Visibility::Public(syn::token::Pub::default());
}
}
item
}
#[cfg(test)]
mod tests {
use super::expand;
use quote::quote;
#[test]
fn extends_rejects_var_ness_flip() {
let input = quote! {
version a { comp(instance) { struct S { x: u8 } topic s: pubsub S; } }
version b extends a { comp { struct S { x: u8, y: u8 } topic s: pubsub S; } }
};
let err = expand(input).expect_err("a var-ness flip across `extends` must be rejected");
assert!(
err.to_string().contains("redeclares its dynamic variable"),
"unexpected error: {err}"
);
}
#[test]
fn extends_accepts_matching_var_ness() {
let input = quote! {
version a { comp(instance) { motor(cap) { enum C { Stop } topic command: pubsub C; } } }
version b extends a {
comp(instance) { motor(cap) { enum C { Stop, Go } topic command: pubsub C; } }
}
};
assert!(expand(input).is_ok());
}
}