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>,
families: Vec<Family>,
}
#[derive(Clone)]
struct Family {
name: Ident,
types: Vec<TypeDef>,
topics: Vec<TopicDef>,
}
#[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,
params: Vec<Ident>,
key_template: Option<syn::LitStr>,
}
impl TopicDef {
fn topic_key(&self, family: &str) -> String {
match &self.key_template {
Some(t) => t.value(),
None => format!("{}/{}", family, self.leaf),
}
}
}
#[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 families = Vec::new();
while !body.is_empty() {
families.push(body.parse()?);
}
Ok(Version {
name,
extends,
families,
})
}
}
impl Parse for Family {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
let body;
syn::braced!(body in input);
let mut types = Vec::new();
let mut topics = 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 {
return Err(body
.error("expected `struct`, `enum`, or `topic …;` inside an API family block"));
}
}
Ok(Family {
name,
types,
topics,
})
}
}
impl Parse for TopicDef {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<kw::topic>()?;
let leaf: Ident = input.parse()?;
let mut params = Vec::new();
if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let parsed =
syn::punctuated::Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
params = parsed.into_iter().collect();
if params.is_empty() {
return Err(syn::Error::new_spanned(
&leaf,
"a dynamic topic must declare at least one param, e.g. `topic foo(id): …`",
));
}
}
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>`"));
};
let key_template = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
Some(input.parse::<syn::LitStr>()?)
} else {
None
};
if params.is_empty() && key_template.is_some() {
return Err(syn::Error::new_spanned(
&leaf,
"a static topic must not declare a key template; add params to make it dynamic",
));
}
if !params.is_empty() && key_template.is_none() {
return Err(syn::Error::new_spanned(
&leaf,
"a dynamic topic requires a key template, e.g. `= \"component/{id}/command\"`",
));
}
input.parse::<Token![;]>()?;
Ok(TopicDef {
leaf,
kind,
params,
key_template,
})
}
}
impl ApiTree {
fn expand(&self) -> syn::Result<TokenStream> {
let mut out = TokenStream::new();
let mut resolved: std::collections::HashMap<String, Vec<Family>> =
std::collections::HashMap::new();
for version in &self.versions {
let effective = match &version.extends {
None => version.families.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_families(base, &version.families)
}
};
resolved.insert(version.name.to_string(), effective.clone());
out.extend(expand_version(&version.name, &effective)?);
}
Ok(out)
}
}
fn overlay_families(base: &[Family], child: &[Family]) -> Vec<Family> {
let mut result: Vec<Family> = base.to_vec();
for cf in child {
if let Some(bf) = result.iter_mut().find(|f| f.name == cf.name) {
for ct in &cf.types {
if let Some(slot) = bf.types.iter_mut().find(|t| t.ident() == ct.ident()) {
*slot = ct.clone();
} else {
bf.types.push(ct.clone());
}
}
for ctp in &cf.topics {
if let Some(slot) = bf.topics.iter_mut().find(|t| t.leaf == ctp.leaf) {
*slot = ctp.clone();
} else {
bf.topics.push(ctp.clone());
}
}
} else {
result.push(cf.clone());
}
}
result
}
fn expand_version(name: &Ident, families: &[Family]) -> syn::Result<TokenStream> {
let phoxal = phoxal();
let mod_name = name;
let id = name.to_string();
let mut family_mods = TokenStream::new();
for family in families {
family_mods.extend(family.expand_module()?);
}
let topic_mod = expand_topic_module(families)?;
Ok(quote! {
pub mod #mod_name {
#[derive(Clone, Copy, Debug)]
pub enum Api {}
impl #phoxal::api::ApiVersion for Api {
const ID: &'static str = #id;
}
#family_mods
#topic_mod
}
})
}
fn expand_topic_module(families: &[Family]) -> syn::Result<TokenStream> {
let phoxal = phoxal();
let mut root_methods = TokenStream::new();
let mut builder_mods = TokenStream::new();
for family in families {
let fam = &family.name;
let fam_str = fam.to_string();
root_methods.extend(quote! {
pub fn #fam(self) -> #fam::Builder { #fam::Builder }
});
let mut leaf_methods = TokenStream::new();
for topic in &family.topics {
let leaf = &topic.leaf;
let key = topic.topic_key(&fam_str);
let kind_ty = match &topic.kind {
TopicKind::PubSub(body) => quote! {
#phoxal::bus::PubSub<super::super::#fam::#body>
},
TopicKind::Query { request, response } => quote! {
#phoxal::bus::Query<
super::super::#fam::#request,
super::super::#fam::#response,
>
},
};
if topic.params.is_empty() {
leaf_methods.extend(quote! {
#[doc = #key]
pub fn #leaf(self) -> #phoxal::bus::Topic<#kind_ty> {
#phoxal::bus::Topic::new_static(#key)
}
});
} else {
let params = &topic.params;
let template = topic
.key_template
.as_ref()
.expect("dynamic topic has a template");
leaf_methods.extend(quote! {
#[doc = #key]
pub fn #leaf(
self,
#(#params: impl ::core::fmt::Display),*
) -> #phoxal::bus::Topic<#kind_ty> {
#phoxal::bus::Topic::new_owned(::std::format!(#template, #(#params = #params),*))
}
});
}
}
builder_mods.extend(quote! {
pub mod #fam {
pub struct Builder;
impl Builder {
#leaf_methods
}
}
});
}
Ok(quote! {
pub mod topic {
pub fn new() -> Root {
Root
}
pub struct Root;
impl Root {
#root_methods
}
#builder_mods
}
})
}
impl Family {
fn expand_module(&self) -> syn::Result<TokenStream> {
let phoxal = phoxal();
let fam = &self.name;
let fam_str = fam.to_string();
let derives = body_derives();
let mut types = TokenStream::new();
for ty in &self.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 &self.topics {
let key = topic.topic_key(&fam_str);
match &topic.kind {
TopicKind::PubSub(body) => {
let family_const = format!("{}::{}", fam_str, body);
impls.extend(quote! {
impl #phoxal::api::ContractBody for #body {
type Api = super::Api;
const FAMILY: &'static str = #family_const;
const TOPIC: &'static str = #key;
}
});
}
TopicKind::Query { request, response } => {
let req_family = format!("{}::{}", fam_str, request);
let resp_family = format!("{}::{}", fam_str, response);
impls.extend(quote! {
impl #phoxal::api::ContractBody for #request {
type Api = super::Api;
const FAMILY: &'static str = #req_family;
const TOPIC: &'static str = #key;
}
impl #phoxal::api::ContractBody for #response {
type Api = super::Api;
const FAMILY: &'static str = #resp_family;
const TOPIC: &'static str = #key;
}
});
}
}
}
Ok(quote! {
pub mod #fam {
#![allow(unused_imports)]
use super::Api;
#types
#impls
}
})
}
}
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
}