A channel's message slots are named from the client, on every backend
The IR called them react-message and django-message, so a FastAPI channel had to declare a DjangoMessage. They are client-message and server-message now, and the direction words hold wherever a channel is declared: Params / ClientMessage / ServerMessage, with mizan-core deriving <Pascal>Params and friends so no backend names a type itself. Django's ReactChannel and FastAPI's ReactChannel are both Channel. mizan-fastapi never registered a channels extension, so build_ir() emitted no channel at all and every payload type was invisible to codegen. It registers one now. RegistryExtension is an ABC requiring all(), which is what the IR reads — an extension that cannot enumerate its registrations no longer exists. The gate that should have caught the rename could not: tests/afi registered no channel because mizan-rust had no channel registry to register one in, so a five-package rename of the wire contract passed byte-parity without a channel byte crossing it. mizan-rust grows ChannelSlotKind, a CHANNELS slice, a #[mizan::channel] macro, and KDL emission whose wire_to_pascal matches Python's split; the AFI fixture now carries a channel with every slot and one with a single slot, so all three backends prove the contract byte for byte. MizanChannel held three Option<String> beside three has_*() predicates and unwrapped them with defaults; it holds an ordered slot vector, so an absent slot is absent rather than defaulted. The channels target emitted a React hooks file that a stage1-only consumer could not compile — react emits that now. The codegen's parity tests byte-compared emitted source against baselines without ever compiling it: they compile the generated crate and run its tests, import the generated Python package and call every method, and typecheck each TypeScript target against a consumer. Also fixed at source: app_visitor printed its import diagnostic to stdout, the stream export_mizan_ir writes KDL to, so a failed import silently corrupted the IR; the apps root was hardcoded to "apps"; _default_literal crashed build_ir on any non-JSON-serializable field default; Django and mizan-core derived Pascal names two different ways, disagreeing on every dotted channel name. ir.py builds a document and renders templates/ir/document.kdl.j2 rather than appending KDL strings with hand-tracked indentation, and named types resolve to a fixed point — a model reachable only through a union branch was referenced by a ref that no type block ever defined. The rest is the write-gate's own classifiers run over the standing tree: relative imports, silent swallows, Protocol contracts that should be ABCs, emitters hand-rendering target source, catch-all arms over closed enums, and comments narrating the project rather than the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
111
cores/mizan-rust-macros/src/channel.rs
Normal file
111
cores/mizan-rust-macros/src/channel.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! `#[mizan::channel("<wire-name>", params = T, client_message = T,
|
||||
//! server_message = T)]` — emit the linkme `ChannelEntry` registration for a
|
||||
//! unit struct. Every slot is optional; only the declared ones register, and
|
||||
//! each slot type must implement `MizanType` (via `#[derive(Mizan)]`).
|
||||
|
||||
use heck::ToShoutySnakeCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
ItemStruct, LitStr, Path, Token,
|
||||
};
|
||||
|
||||
mod kw {
|
||||
syn::custom_keyword!(params);
|
||||
syn::custom_keyword!(client_message);
|
||||
syn::custom_keyword!(server_message);
|
||||
}
|
||||
|
||||
/// Attribute args: the wire name, then the slot types the channel declares.
|
||||
pub struct ChannelArgs {
|
||||
pub wire_name: String,
|
||||
pub params: Option<Path>,
|
||||
pub client_message: Option<Path>,
|
||||
pub server_message: Option<Path>,
|
||||
}
|
||||
|
||||
impl Parse for ChannelArgs {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let name: LitStr = input.parse()?;
|
||||
let mut out = Self {
|
||||
wire_name: name.value(),
|
||||
params: None,
|
||||
client_message: None,
|
||||
server_message: None,
|
||||
};
|
||||
while input.peek(Token![,]) {
|
||||
input.parse::<Token![,]>()?;
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
if input.peek(kw::params) {
|
||||
input.parse::<kw::params>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
out.params = Some(input.parse()?);
|
||||
} else if input.peek(kw::client_message) {
|
||||
input.parse::<kw::client_message>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
out.client_message = Some(input.parse()?);
|
||||
} else if input.peek(kw::server_message) {
|
||||
input.parse::<kw::server_message>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
out.server_message = Some(input.parse()?);
|
||||
} else {
|
||||
return Err(input.error(
|
||||
"expected a channel slot: params, client_message, or server_message",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(args: ChannelArgs, item: ItemStruct) -> TokenStream {
|
||||
if !item.fields.is_empty() {
|
||||
return syn::Error::new_spanned(
|
||||
&item.fields,
|
||||
"#[mizan::channel] requires a unit struct — the payload types are declared in the attribute.",
|
||||
)
|
||||
.to_compile_error();
|
||||
}
|
||||
|
||||
let ident = item.ident.clone();
|
||||
let wire_name = args.wire_name;
|
||||
|
||||
// Slots register in the order the IR emits them: params, client-message,
|
||||
// server-message.
|
||||
let mut slot_exprs: Vec<TokenStream> = Vec::new();
|
||||
for (kind, declared) in [
|
||||
(format_ident!("Params"), args.params),
|
||||
(format_ident!("ClientMessage"), args.client_message),
|
||||
(format_ident!("ServerMessage"), args.server_message),
|
||||
] {
|
||||
if let Some(ty) = declared {
|
||||
slot_exprs.push(quote! {
|
||||
::mizan_core::ChannelSlot {
|
||||
kind: ::mizan_core::ChannelSlotKind::#kind,
|
||||
shape_fn: <#ty as ::mizan_core::MizanType>::shape,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let register_static = format_ident!(
|
||||
"__MIZAN_CHANNEL_REGISTER_{}",
|
||||
ident.to_string().to_shouty_snake_case()
|
||||
);
|
||||
|
||||
quote! {
|
||||
#item
|
||||
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::CHANNELS)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #register_static: ::mizan_core::ChannelEntry = ::mizan_core::ChannelEntry {
|
||||
name: #wire_name,
|
||||
slots: &[
|
||||
#(#slot_exprs),*
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,33 @@ use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{parse::Parser, punctuated::Punctuated, ItemStruct, Lit, LitStr, Meta, Token};
|
||||
|
||||
/// Attribute args: either nothing, or one string literal that overrides the
|
||||
/// derived snake_case context name.
|
||||
pub struct ContextArgs {
|
||||
pub explicit_name: Option<String>,
|
||||
/// Where the context's wire name comes from: the attribute, or the struct's
|
||||
/// own identifier when the attribute names none.
|
||||
pub enum ContextName {
|
||||
Explicit(String),
|
||||
FromIdent,
|
||||
}
|
||||
|
||||
impl ContextArgs {
|
||||
impl ContextName {
|
||||
/// Both `#[mizan::context("user")]` (bare string literal) and
|
||||
/// `#[mizan::context(name = "user")]` name the context explicitly.
|
||||
pub fn parse(attr_tokens: TokenStream) -> syn::Result<Self> {
|
||||
if attr_tokens.is_empty() {
|
||||
return Ok(Self { explicit_name: None });
|
||||
return Ok(ContextName::FromIdent);
|
||||
}
|
||||
// Support both `#[mizan::context("user")]` (string literal) and
|
||||
// `#[mizan::context(name = "user")]` (key=value).
|
||||
if let Ok(lit) = syn::parse2::<LitStr>(attr_tokens.clone()) {
|
||||
return Ok(Self {
|
||||
explicit_name: Some(lit.value()),
|
||||
});
|
||||
return Ok(ContextName::Explicit(lit.value()));
|
||||
}
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = parser.parse2(attr_tokens)?;
|
||||
for meta in metas {
|
||||
if let Meta::NameValue(nv) = meta {
|
||||
if nv.path.is_ident("name") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
|
||||
return Ok(Self {
|
||||
explicit_name: Some(s.value()),
|
||||
});
|
||||
if let syn::Expr::Lit(syn::ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
}) = nv.value
|
||||
{
|
||||
return Ok(ContextName::Explicit(s.value()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,16 @@ impl ContextArgs {
|
||||
"expected `#[mizan::context]` or `#[mizan::context(\"<name>\")]` or `#[mizan::context(name = \"<name>\")]`",
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve(self, ident: &syn::Ident) -> String {
|
||||
match self {
|
||||
ContextName::Explicit(name) => name,
|
||||
ContextName::FromIdent => ident.to_string().to_snake_case(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(args: ContextArgs, item: ItemStruct) -> TokenStream {
|
||||
pub fn expand(name: ContextName, item: ItemStruct) -> TokenStream {
|
||||
if !item.fields.is_empty() {
|
||||
return syn::Error::new_spanned(
|
||||
&item.fields,
|
||||
@@ -54,9 +61,7 @@ pub fn expand(args: ContextArgs, item: ItemStruct) -> TokenStream {
|
||||
}
|
||||
|
||||
let ident = item.ident.clone();
|
||||
let name = args
|
||||
.explicit_name
|
||||
.unwrap_or_else(|| ident.to_string().to_snake_case());
|
||||
let name = name.resolve(&ident);
|
||||
|
||||
let register_static =
|
||||
format_ident!("__MIZAN_CTX_REGISTER_{}", ident.to_string().to_uppercase());
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
//! `#[derive(Mizan)]` — emit `MizanType` impl + linkme registration.
|
||||
|
||||
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use proc_macro2::{TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
parse::Parser, punctuated::Punctuated, Data, DataEnum, DataStruct, DeriveInput, Fields, Lit,
|
||||
Meta, Token,
|
||||
parse::{Parse, ParseStream},
|
||||
Data, DeriveInput, Field, Fields, FieldsNamed, Ident, Lit, Meta, Type,
|
||||
};
|
||||
|
||||
use crate::shape::type_shape_expr;
|
||||
use crate::shape::{is_optional, type_shape_expr};
|
||||
|
||||
/// Apply a `#[serde(rename_all = "...")]` casing transform to a Rust
|
||||
/// variant identifier so the IR's enum variant matches what serde emits
|
||||
/// on the wire. Supported casings mirror serde's set.
|
||||
/// variant identifier so the IR's enum variant matches what serde emits on
|
||||
/// the wire. Supported casings mirror serde's set; any other rule — including
|
||||
/// the empty rule an undecorated enum carries — leaves the identifier as
|
||||
/// written.
|
||||
fn apply_rename_all(rule: &str, ident: &str) -> String {
|
||||
match rule {
|
||||
"lowercase" => ident.to_lowercase(),
|
||||
@@ -26,87 +28,210 @@ fn apply_rename_all(rule: &str, ident: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk the enum's outer attributes for `#[serde(rename_all = "...")]`.
|
||||
fn serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
/// The string a `#[serde(<key> = "...")]` entry in `attrs` carries, or
|
||||
/// `fallback` when no entry names `key`. serde owns that attribute's grammar
|
||||
/// and its own derive reports a malformed body, so a body without the
|
||||
/// `<key> = <string>` triple reads here as "no override".
|
||||
fn serde_string(attrs: &[syn::Attribute], key: &str, fallback: String) -> String {
|
||||
for attr in attrs {
|
||||
if !attr.path().is_ident("serde") {
|
||||
continue;
|
||||
}
|
||||
let list = match &attr.meta {
|
||||
Meta::List(l) => l,
|
||||
_ => continue,
|
||||
};
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = match parser.parse2(list.tokens.clone()) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for meta in metas {
|
||||
if let Meta::NameValue(nv) = meta {
|
||||
if nv.path.is_ident("rename_all") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
|
||||
return Some(s.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Walk a variant's attributes for an explicit `#[serde(rename = "...")]`
|
||||
/// override. Variant-level rename overrides the enum-level rename_all.
|
||||
fn serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
|
||||
for attr in attrs {
|
||||
if !attr.path().is_ident("serde") {
|
||||
let Meta::List(list) = &attr.meta else {
|
||||
continue;
|
||||
}
|
||||
let list = match &attr.meta {
|
||||
Meta::List(l) => l,
|
||||
_ => continue,
|
||||
};
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = match parser.parse2(list.tokens.clone()) {
|
||||
Ok(m) => m,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for meta in metas {
|
||||
if let Meta::NameValue(nv) = meta {
|
||||
if nv.path.is_ident("rename") {
|
||||
if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
|
||||
return Some(s.value());
|
||||
let mut on_key = false;
|
||||
let mut on_value = false;
|
||||
for tree in list.tokens.clone() {
|
||||
match tree {
|
||||
TokenTree::Ident(ident) => {
|
||||
on_key = ident == key;
|
||||
on_value = false;
|
||||
}
|
||||
TokenTree::Punct(punct) => {
|
||||
on_value = on_key && punct.as_char() == '=';
|
||||
}
|
||||
TokenTree::Literal(literal) => {
|
||||
if on_value {
|
||||
if let Lit::Str(s) = Lit::new(literal) {
|
||||
return s.value();
|
||||
}
|
||||
}
|
||||
on_key = false;
|
||||
on_value = false;
|
||||
}
|
||||
TokenTree::Group(_) => {
|
||||
on_key = false;
|
||||
on_value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
fallback
|
||||
}
|
||||
|
||||
/// Expand `#[derive(Mizan)]`. Emits the `MizanType` impl AND a linkme
|
||||
/// TypeEntry registration. Every Mizan-shaped type lands in the IR;
|
||||
/// the emitter's inline-substitution pass collapses primitive-aliases
|
||||
/// and enums at use sites so the IR stays tight.
|
||||
pub fn expand(input: DeriveInput) -> TokenStream {
|
||||
let ident = input.ident.clone();
|
||||
/// A braced struct field paired with the identifier it carries. `all` is the
|
||||
/// only constructor and it reads a `FieldsNamed` group, so `ident` is a total
|
||||
/// accessor rather than an Option the caller has to open.
|
||||
struct NamedField<'a> {
|
||||
ident: &'a Ident,
|
||||
field: &'a Field,
|
||||
}
|
||||
|
||||
impl<'a> NamedField<'a> {
|
||||
fn all(braced: &'a FieldsNamed) -> impl Iterator<Item = Self> {
|
||||
braced
|
||||
.named
|
||||
.iter()
|
||||
.flat_map(|field| field.ident.as_ref().map(|ident| Self { ident, field }))
|
||||
}
|
||||
|
||||
/// The wire name serde emits: a `#[serde(rename)]` override, else the
|
||||
/// identifier with serde's `r#` raw-prefix stripping applied.
|
||||
fn wire_name(&self) -> String {
|
||||
let raw_ident = self.ident.to_string();
|
||||
let default = raw_ident.trim_start_matches("r#").to_string();
|
||||
serde_string(&self.field.attrs, "rename", default)
|
||||
}
|
||||
}
|
||||
|
||||
/// One struct field reduced to what the IR carries: the name serde puts on the
|
||||
/// wire and the declared Rust type.
|
||||
struct FieldShape {
|
||||
wire_name: String,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
/// The two type forms the IR can express.
|
||||
enum DerivedShape {
|
||||
Struct(Vec<FieldShape>),
|
||||
Enum(Vec<String>),
|
||||
}
|
||||
|
||||
/// A derive input already reduced to the IR form its body takes. The token
|
||||
/// stream is parsed straight into this shape, so `expand` reads a settled
|
||||
/// name and body and has nothing left to reject.
|
||||
pub struct MizanDerive {
|
||||
ident: Ident,
|
||||
shape: DerivedShape,
|
||||
}
|
||||
|
||||
impl Parse for MizanDerive {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let input: DeriveInput = input.parse()?;
|
||||
let shape = match &input.data {
|
||||
Data::Struct(s) => {
|
||||
let braced = match &s.fields {
|
||||
Fields::Named(named) => named,
|
||||
Fields::Unnamed(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&s.fields,
|
||||
"#[derive(Mizan)] requires named fields. Tuple structs aren't part of the IR shape.",
|
||||
));
|
||||
}
|
||||
Fields::Unit => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&s.fields,
|
||||
"#[derive(Mizan)] requires named fields. Unit structs aren't part of the IR shape.",
|
||||
));
|
||||
}
|
||||
};
|
||||
let mut fields = Vec::new();
|
||||
for named in NamedField::all(braced) {
|
||||
fields.push(FieldShape {
|
||||
wire_name: named.wire_name(),
|
||||
ty: named.field.ty.clone(),
|
||||
});
|
||||
}
|
||||
DerivedShape::Struct(fields)
|
||||
}
|
||||
Data::Enum(e) => {
|
||||
let rename_all = serde_string(&input.attrs, "rename_all", String::new());
|
||||
let mut variants = Vec::new();
|
||||
for variant in &e.variants {
|
||||
match &variant.fields {
|
||||
Fields::Unit => {}
|
||||
Fields::Named(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&variant.fields,
|
||||
"#[derive(Mizan)] only supports unit-variant enums (string-literal enums in the IR). Struct variants aren't expressible in the current IR.",
|
||||
));
|
||||
}
|
||||
Fields::Unnamed(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&variant.fields,
|
||||
"#[derive(Mizan)] only supports unit-variant enums (string-literal enums in the IR). Tuple variants aren't expressible in the current IR.",
|
||||
));
|
||||
}
|
||||
}
|
||||
// Variant-level `rename` wins over the enum-level
|
||||
// `rename_all` rule.
|
||||
let default = apply_rename_all(&rename_all, &variant.ident.to_string());
|
||||
variants.push(serde_string(&variant.attrs, "rename", default));
|
||||
}
|
||||
DerivedShape::Enum(variants)
|
||||
}
|
||||
Data::Union(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&input,
|
||||
"#[derive(Mizan)] does not support `union` types — use a struct or enum.",
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
ident: input.ident,
|
||||
shape,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `NamedType` expression the generated `shape()` returns.
|
||||
fn named_type_expr(shape: &DerivedShape) -> TokenStream {
|
||||
match shape {
|
||||
DerivedShape::Struct(fields) => {
|
||||
let field_exprs: Vec<TokenStream> = fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let name = &field.wire_name;
|
||||
// A Rust struct-field declaration carries no default
|
||||
// expression, so `default` is always None and `required`
|
||||
// follows the Option wrapper.
|
||||
let required = !is_optional(&field.ty);
|
||||
let shape = type_shape_expr(&field.ty);
|
||||
quote! {
|
||||
::mizan_core::StructField {
|
||||
name: #name,
|
||||
required: #required,
|
||||
default: ::std::option::Option::None,
|
||||
shape: #shape,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
quote! {
|
||||
::mizan_core::NamedType::Struct(::std::vec![
|
||||
#(#field_exprs),*
|
||||
])
|
||||
}
|
||||
}
|
||||
DerivedShape::Enum(variants) => quote! {
|
||||
::mizan_core::NamedType::Enum(::std::vec![
|
||||
#(#variants),*
|
||||
])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand `#[derive(Mizan)]` — the `MizanType` impl plus the linkme
|
||||
/// `TypeEntry` registration for the derived type.
|
||||
pub fn expand(derived: MizanDerive) -> TokenStream {
|
||||
let MizanDerive { ident, shape } = derived;
|
||||
let named_type_body = named_type_expr(&shape);
|
||||
let type_name = ident.to_string();
|
||||
|
||||
let rename_all = serde_rename_all(&input.attrs);
|
||||
|
||||
let named_type_body = match &input.data {
|
||||
Data::Struct(s) => emit_struct(s),
|
||||
Data::Enum(e) => emit_enum(e, rename_all.as_deref()),
|
||||
Data::Union(_) => {
|
||||
return syn::Error::new_spanned(
|
||||
&input,
|
||||
"#[derive(Mizan)] does not support `union` types — use a struct or enum.",
|
||||
)
|
||||
.to_compile_error();
|
||||
}
|
||||
};
|
||||
|
||||
let register_static =
|
||||
quote::format_ident!("__MIZAN_TYPE_REGISTER_{}", ident.to_string().to_shouty_snake_case());
|
||||
let register_static = format_ident!(
|
||||
"__MIZAN_TYPE_REGISTER_{}",
|
||||
type_name.to_shouty_snake_case()
|
||||
);
|
||||
|
||||
quote! {
|
||||
impl ::mizan_core::MizanType for #ident {
|
||||
@@ -123,84 +248,3 @@ pub fn expand(input: DeriveInput) -> TokenStream {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_struct(s: &DataStruct) -> TokenStream {
|
||||
let fields = match &s.fields {
|
||||
Fields::Named(named) => &named.named,
|
||||
Fields::Unnamed(_) | Fields::Unit => {
|
||||
return syn::Error::new_spanned(
|
||||
&s.fields,
|
||||
"#[derive(Mizan)] requires named fields. Tuple structs and unit structs aren't part of the IR shape.",
|
||||
)
|
||||
.to_compile_error();
|
||||
}
|
||||
};
|
||||
|
||||
let mut field_exprs: Vec<TokenStream> = Vec::new();
|
||||
for field in fields {
|
||||
let ident = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect("named field always has an ident");
|
||||
// Field-level `#[serde(rename = "...")]` wins; otherwise strip
|
||||
// the raw-identifier prefix that Rust uses to escape keywords
|
||||
// (`r#type` → `type`). Serde itself strips the prefix when
|
||||
// computing the default field name; the IR has to match the
|
||||
// wire form, not the Rust source form.
|
||||
let raw_ident = ident.to_string();
|
||||
let stripped = raw_ident.strip_prefix("r#").unwrap_or(&raw_ident);
|
||||
let name = serde_rename(&field.attrs).unwrap_or_else(|| stripped.to_string());
|
||||
let shape = type_shape_expr(&field.ty);
|
||||
|
||||
// A field is `required` iff its type is not `Option<...>`. Defaults
|
||||
// are not encodable from Rust syntax (no `= expr` on a struct field
|
||||
// declaration) — the macro emits `required: false, default: None`
|
||||
// for Option-wrapped fields, leaving defaults for a future
|
||||
// attribute-based extension.
|
||||
let is_optional = crate::shape::unwrap_option(&field.ty).is_some();
|
||||
let required = !is_optional;
|
||||
field_exprs.push(quote! {
|
||||
::mizan_core::StructField {
|
||||
name: #name,
|
||||
required: #required,
|
||||
default: ::std::option::Option::None,
|
||||
shape: #shape,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
quote! {
|
||||
::mizan_core::NamedType::Struct(::std::vec![
|
||||
#(#field_exprs),*
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_enum(e: &DataEnum, rename_all: Option<&str>) -> TokenStream {
|
||||
let mut variants: Vec<TokenStream> = Vec::new();
|
||||
for variant in &e.variants {
|
||||
if !matches!(variant.fields, Fields::Unit) {
|
||||
return syn::Error::new_spanned(
|
||||
&variant.fields,
|
||||
"#[derive(Mizan)] only supports unit-variant enums (string-literal enums in the IR). Variants with payload aren't expressible in the current IR.",
|
||||
)
|
||||
.to_compile_error();
|
||||
}
|
||||
let raw = variant.ident.to_string();
|
||||
// Variant-level `#[serde(rename = "...")]` wins; otherwise apply
|
||||
// the enum-level `#[serde(rename_all = "...")]` rule.
|
||||
let name = if let Some(explicit) = serde_rename(&variant.attrs) {
|
||||
explicit
|
||||
} else if let Some(rule) = rename_all {
|
||||
apply_rename_all(rule, &raw)
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
variants.push(quote! { #name });
|
||||
}
|
||||
quote! {
|
||||
::mizan_core::NamedType::Enum(::std::vec![
|
||||
#(#variants),*
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! * a synthetic Input struct (`<camelName>Input`) when the fn has params
|
||||
//! * `MizanType` impl on the Input struct
|
||||
//! * canonical type entries (`<camelName>Input` / `<camelName>Output`)
|
||||
//! * Vec-element sub-type entries (so `Vec<T>` outputs surface `T` too)
|
||||
//! * list-element sub-type entries (so `Vec<T>` outputs surface `T` too)
|
||||
//! * `FunctionSpec` impl on a ZST `__MizanFn_<name>`
|
||||
//! * `FUNCTIONS` linkme registration of `&__MIZAN_FN_<NAME>_INSTANCE`
|
||||
|
||||
@@ -10,13 +10,25 @@ use heck::{ToLowerCamelCase, ToShoutySnakeCase};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{
|
||||
parse::Parser,
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
Expr, ExprPath, ExprTuple, FnArg, ItemFn, Meta, Pat, Path, ReturnType, Token, Type,
|
||||
token::Paren,
|
||||
FnArg, Ident, ItemFn, Pat, Path, ReturnType, Token, Type,
|
||||
};
|
||||
|
||||
use crate::shape::{analyze_return, primitive_of, type_shape_expr, unwrap_option};
|
||||
use crate::shape::{
|
||||
analyze_return, classify, is_optional, path_head, ref_shape_expr, type_shape_expr, Head,
|
||||
ReturnForm, TypeForm,
|
||||
};
|
||||
|
||||
mod kw {
|
||||
syn::custom_keyword!(context);
|
||||
syn::custom_keyword!(affects);
|
||||
syn::custom_keyword!(merge);
|
||||
syn::custom_keyword!(websocket);
|
||||
syn::custom_keyword!(private);
|
||||
}
|
||||
|
||||
/// Parsed attribute args for `#[mizan(...)]`.
|
||||
#[derive(Default)]
|
||||
@@ -28,125 +40,149 @@ pub struct FunctionArgs {
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
impl FunctionArgs {
|
||||
pub fn parse(attr_tokens: TokenStream) -> syn::Result<Self> {
|
||||
if attr_tokens.is_empty() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = parser.parse2(attr_tokens)?;
|
||||
impl Parse for FunctionArgs {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut out = Self::default();
|
||||
for meta in metas {
|
||||
match meta {
|
||||
Meta::NameValue(nv) => {
|
||||
if nv.path.is_ident("context") {
|
||||
out.context = Some(expect_path(&nv.value)?);
|
||||
} else if nv.path.is_ident("affects") {
|
||||
out.affects = collect_paths(&nv.value)?;
|
||||
} else if nv.path.is_ident("merge") {
|
||||
out.merge = collect_paths(&nv.value)?;
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
nv.path,
|
||||
"unknown attribute key; expected one of: context, affects, merge",
|
||||
));
|
||||
}
|
||||
}
|
||||
Meta::Path(p) => {
|
||||
if p.is_ident("websocket") {
|
||||
out.websocket = true;
|
||||
} else if p.is_ident("private") {
|
||||
out.private = true;
|
||||
} else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
p,
|
||||
"unknown flag; expected `websocket` or `private`",
|
||||
));
|
||||
}
|
||||
}
|
||||
Meta::List(l) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
l,
|
||||
"list-shaped attribute args not supported here",
|
||||
));
|
||||
}
|
||||
while !input.is_empty() {
|
||||
if input.peek(kw::context) {
|
||||
input.parse::<kw::context>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
out.context = Some(input.parse()?);
|
||||
} else if input.peek(kw::affects) {
|
||||
input.parse::<kw::affects>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
out.affects = parse_path_group(input)?;
|
||||
} else if input.peek(kw::merge) {
|
||||
input.parse::<kw::merge>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
out.merge = parse_path_group(input)?;
|
||||
} else if input.peek(kw::websocket) {
|
||||
input.parse::<kw::websocket>()?;
|
||||
out.websocket = true;
|
||||
} else if input.peek(kw::private) {
|
||||
input.parse::<kw::private>()?;
|
||||
out.private = true;
|
||||
} else {
|
||||
return Err(input.error(
|
||||
"expected one of: `context = T`, `affects = T`, `merge = T`, `websocket`, `private`",
|
||||
));
|
||||
}
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
input.parse::<Token![,]>()?;
|
||||
}
|
||||
if out.context.is_some() && !out.affects.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
out.context.as_ref().unwrap(),
|
||||
"`context` and `affects` are mutually exclusive — a function is either a context reader or a mutation.",
|
||||
));
|
||||
}
|
||||
if out.context.is_some() && !out.merge.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
out.context.as_ref().unwrap(),
|
||||
"`context` and `merge` are mutually exclusive — a function is either a context reader or a mutation.",
|
||||
));
|
||||
if let Some(ctx) = &out.context {
|
||||
if !out.affects.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ctx,
|
||||
"`context` and `affects` are mutually exclusive — a function is either a context reader or a mutation.",
|
||||
));
|
||||
}
|
||||
if !out.merge.is_empty() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
ctx,
|
||||
"`context` and `merge` are mutually exclusive — a function is either a context reader or a mutation.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_path(expr: &Expr) -> syn::Result<Path> {
|
||||
if let Expr::Path(ExprPath { path, .. }) = expr {
|
||||
Ok(path.clone())
|
||||
/// One context type (`affects = Ctx`) or a parenthesized group of them
|
||||
/// (`affects = (CtxA, CtxB)`).
|
||||
fn parse_path_group(input: ParseStream) -> syn::Result<Vec<Path>> {
|
||||
if input.peek(Paren) {
|
||||
let group;
|
||||
parenthesized!(group in input);
|
||||
Ok(Punctuated::<Path, Token![,]>::parse_terminated(&group)?
|
||||
.into_iter()
|
||||
.collect())
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(
|
||||
expr,
|
||||
"expected a type path (e.g. `UserCtx`)",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_paths(expr: &Expr) -> syn::Result<Vec<Path>> {
|
||||
match expr {
|
||||
Expr::Path(_) => Ok(vec![expect_path(expr)?]),
|
||||
Expr::Tuple(ExprTuple { elems, .. }) => elems.iter().map(expect_path).collect(),
|
||||
_ => Err(syn::Error::new_spanned(
|
||||
expr,
|
||||
"expected a context type or a tuple of context types (e.g. `UserCtx` or `(UserCtx, OrderCtx)`)",
|
||||
)),
|
||||
Ok(vec![input.parse()?])
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about one input parameter, extracted from the fn signature.
|
||||
struct InputArg {
|
||||
ident: syn::Ident,
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
if item.sig.asyncness.is_none() {
|
||||
return syn::Error::new_spanned(
|
||||
&item.sig.fn_token,
|
||||
"#[mizan] requires an `async fn`. Wrap synchronous handlers if needed.",
|
||||
)
|
||||
.to_compile_error();
|
||||
/// The handler grammar `#[mizan::client]` accepts: an `async fn` taking a
|
||||
/// request handle followed by plain-identifier params, with an explicit return
|
||||
/// type. The token stream is parsed straight into this shape, so `expand`
|
||||
/// reads three settled fields and has nothing left to reject.
|
||||
///
|
||||
/// A missing `async` or a missing request handle needs no rejection here: the
|
||||
/// dispatch wrapper `expand` emits calls the handler with `&req` and awaits
|
||||
/// the call, so rustc rejects both at the generated call site.
|
||||
pub struct Handler {
|
||||
item: ItemFn,
|
||||
input_args: Vec<InputArg>,
|
||||
return_ty: Type,
|
||||
}
|
||||
|
||||
impl Parse for Handler {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let item: ItemFn = input.parse()?;
|
||||
|
||||
let ReturnType::Type(_, declared) = &item.sig.output else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&item.sig,
|
||||
"#[mizan] requires an explicit return type. Add `-> T` to the signature.",
|
||||
));
|
||||
};
|
||||
let return_ty = (**declared).clone();
|
||||
|
||||
let mut input_args = Vec::new();
|
||||
// The first arg is the request handle, which the dispatch wrapper
|
||||
// forwards as `req`; it never becomes an Input field.
|
||||
for arg in item.sig.inputs.iter().skip(1) {
|
||||
let typed = match arg {
|
||||
FnArg::Typed(typed) => typed,
|
||||
FnArg::Receiver(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
arg,
|
||||
"#[mizan] functions are free functions, not methods. `self` is not allowed.",
|
||||
));
|
||||
}
|
||||
};
|
||||
let Pat::Ident(bound) = &*typed.pat else {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&typed.pat,
|
||||
"#[mizan] function parameters must be plain identifiers (no destructuring).",
|
||||
));
|
||||
};
|
||||
input_args.push(InputArg {
|
||||
ident: bound.ident.clone(),
|
||||
ty: (*typed.ty).clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
item,
|
||||
input_args,
|
||||
return_ty,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(args: FunctionArgs, handler: Handler) -> TokenStream {
|
||||
let Handler {
|
||||
item,
|
||||
input_args,
|
||||
return_ty,
|
||||
} = handler;
|
||||
|
||||
let fn_name = item.sig.ident.to_string();
|
||||
let camel = fn_name.to_lower_camel_case();
|
||||
let input_type_name = format!("{camel}Input");
|
||||
let output_type_name = format!("{camel}Output");
|
||||
|
||||
let input_args = match collect_input_args(&item) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e.to_compile_error(),
|
||||
};
|
||||
let has_input = !input_args.is_empty();
|
||||
let input_type_ident = format_ident!("{}", input_type_name);
|
||||
|
||||
let return_ty = match &item.sig.output {
|
||||
ReturnType::Type(_, t) => (**t).clone(),
|
||||
ReturnType::Default => {
|
||||
return syn::Error::new_spanned(
|
||||
&item.sig,
|
||||
"#[mizan] requires an explicit return type. Add `-> T` to the signature.",
|
||||
)
|
||||
.to_compile_error();
|
||||
}
|
||||
};
|
||||
let analysis = analyze_return(&return_ty);
|
||||
|
||||
// ─── Synthetic Input struct ────────────────────────────────────────────
|
||||
@@ -156,12 +192,11 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
for arg in &input_args {
|
||||
let ident = &arg.ident;
|
||||
let ty = &arg.ty;
|
||||
// Strip a leading underscore from the wire-level field name —
|
||||
// Rust convention uses `_foo` to silence unused-arg warnings,
|
||||
// but the wire schema and the Python fixture name the param
|
||||
// `foo`. The struct field keeps its source ident (so the
|
||||
// dispatch wrapper's `validated.#ident` compiles), and a serde
|
||||
// `rename` bridges the wire-level JSON name.
|
||||
// Rust convention writes `_foo` to silence an unused-arg warning,
|
||||
// but the wire schema names the param `foo`. The struct field
|
||||
// keeps its source ident so the dispatch wrapper's
|
||||
// `validated.#ident` compiles, and a serde `rename` bridges the
|
||||
// JSON name.
|
||||
let name_str = ident.to_string();
|
||||
let wire_name = name_str.trim_start_matches('_').to_string();
|
||||
let serde_rename = if wire_name != name_str {
|
||||
@@ -170,8 +205,7 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
TokenStream::new()
|
||||
};
|
||||
field_defs.push(quote! { #serde_rename pub #ident: #ty, });
|
||||
let is_optional = unwrap_option(ty).is_some();
|
||||
let required = !is_optional;
|
||||
let required = !is_optional(ty);
|
||||
let shape = type_shape_expr(ty);
|
||||
field_shapes.push(quote! {
|
||||
::mizan_core::StructField {
|
||||
@@ -202,11 +236,6 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
};
|
||||
|
||||
// ─── Type entry registrations ──────────────────────────────────────────
|
||||
// - Input: TypeEntry pointing at the synthetic input struct's shape_fn.
|
||||
// - Output: TypeEntry whose shape is a copy of the user's Output shape
|
||||
// (for struct outputs) or an `Alias(List(Ref("T")))` (for Vec outputs).
|
||||
// - For Vec<T> outputs, ALSO register T's TypeEntry pointing at T's
|
||||
// MizanType impl (so the Ref resolves in the IR).
|
||||
let mut type_registrations = Vec::new();
|
||||
if has_input {
|
||||
let static_ident =
|
||||
@@ -222,66 +251,67 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
}
|
||||
|
||||
let output_static = format_ident!("__MIZAN_TYPE_{}", output_type_name.to_shouty_snake_case());
|
||||
if analysis.is_vec {
|
||||
let elem = analysis.vec_inner.as_ref().expect("vec_inner set");
|
||||
// userOrdersOutput → alias { list { ref "OrderOutput" } }
|
||||
// The Ref name is resolved via `<T as MizanType>::type_name()`.
|
||||
type_registrations.push(quote! {
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #output_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: #output_type_name,
|
||||
shape_fn: || ::mizan_core::NamedType::Alias(
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(
|
||||
::mizan_core::TypeShape::Ref(<#elem as ::mizan_core::MizanType>::TYPE_NAME)
|
||||
))
|
||||
),
|
||||
let output_shape_expr = match &analysis.form {
|
||||
ReturnForm::Sequence { element } => {
|
||||
let element_ref = ref_shape_expr(element);
|
||||
let alias = quote! {
|
||||
::mizan_core::NamedType::Alias(
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#element_ref))
|
||||
)
|
||||
};
|
||||
});
|
||||
// Also register the element type itself by its own name. `TYPE_NAME`
|
||||
// is an associated const, so this is usable in a static initializer.
|
||||
// The static ident scopes by the function name so two handlers
|
||||
// returning `Vec<Same>` don't collide; the IrSnapshot's BTreeMap
|
||||
// dedupes by the entry's `name` at emit time.
|
||||
let elem_static =
|
||||
element_type_static_ident_scoped(elem, &fn_name.to_shouty_snake_case());
|
||||
type_registrations.push(quote! {
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #elem_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: <#elem as ::mizan_core::MizanType>::TYPE_NAME,
|
||||
shape_fn: <#elem as ::mizan_core::MizanType>::shape,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Non-Vec output: copy the inner type's shape under the canonical name.
|
||||
let inner_ty = &analysis.inner;
|
||||
type_registrations.push(quote! {
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #output_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: #output_type_name,
|
||||
shape_fn: <#inner_ty as ::mizan_core::MizanType>::shape,
|
||||
};
|
||||
});
|
||||
}
|
||||
type_registrations.push(quote! {
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #output_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: #output_type_name,
|
||||
shape_fn: || #alias,
|
||||
};
|
||||
});
|
||||
// The element type also registers under its own name. The static
|
||||
// ident is scoped by the function name so two handlers returning
|
||||
// `Vec<Same>` don't collide; the emitter dedupes by entry name.
|
||||
let element_static =
|
||||
element_type_static_ident_scoped(element, &fn_name.to_shouty_snake_case());
|
||||
type_registrations.push(quote! {
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #element_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: <#element as ::mizan_core::MizanType>::TYPE_NAME,
|
||||
shape_fn: <#element as ::mizan_core::MizanType>::shape,
|
||||
};
|
||||
});
|
||||
alias
|
||||
}
|
||||
ReturnForm::Scalar { inner } => {
|
||||
type_registrations.push(quote! {
|
||||
#[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
|
||||
#[linkme(crate = ::mizan_core::__priv::linkme)]
|
||||
static #output_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
|
||||
name: #output_type_name,
|
||||
shape_fn: <#inner as ::mizan_core::MizanType>::shape,
|
||||
};
|
||||
});
|
||||
quote! { <#inner as ::mizan_core::MizanType>::shape() }
|
||||
}
|
||||
};
|
||||
|
||||
// ─── InputParam slice (for context-builder shared-param elevation) ────
|
||||
// A non-primitive param is an opaque payload in the context's `param`
|
||||
// block and carries the string primitive.
|
||||
let opaque_primitive = || quote! { ::mizan_core::Primitive::String };
|
||||
let mut input_params = Vec::new();
|
||||
for arg in &input_args {
|
||||
// Wire-level name strips the underscore prefix — see input_struct
|
||||
// above for the rationale.
|
||||
// above.
|
||||
let name_str = arg.ident.to_string();
|
||||
let name_str = name_str.trim_start_matches('_').to_string();
|
||||
let primitive = primitive_of(&arg.ty).unwrap_or_else(|| {
|
||||
// Non-primitive params don't surface in the context's `param`
|
||||
// block; they participate as opaque payloads. Using `String` as
|
||||
// the placeholder primitive matches Python's fallback in
|
||||
// `_annotation_to_primitive`.
|
||||
quote! { ::mizan_core::Primitive::String }
|
||||
});
|
||||
let is_optional = unwrap_option(&arg.ty).is_some();
|
||||
let required = !is_optional;
|
||||
let primitive = match classify(&arg.ty) {
|
||||
TypeForm::Primitive(p) => p,
|
||||
TypeForm::Optional(_) => opaque_primitive(),
|
||||
TypeForm::Sequence(_) => opaque_primitive(),
|
||||
TypeForm::Named(_) => opaque_primitive(),
|
||||
};
|
||||
let required = !is_optional(&arg.ty);
|
||||
input_params.push(quote! {
|
||||
::mizan_core::InputParam {
|
||||
name: #name_str,
|
||||
@@ -354,7 +384,7 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
let private = args.private;
|
||||
|
||||
let dispatch_body = build_dispatch(
|
||||
&item,
|
||||
&inner_fn_ident,
|
||||
&input_args,
|
||||
has_input,
|
||||
&input_type_ident,
|
||||
@@ -362,8 +392,6 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
);
|
||||
|
||||
quote! {
|
||||
// Keep the user's original fn intact — the macro never rewrites the
|
||||
// body, only wraps it for dispatch.
|
||||
#item
|
||||
|
||||
#input_struct
|
||||
@@ -383,6 +411,7 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
fn has_input(&self) -> bool { #has_input }
|
||||
fn input_type(&self) -> ::std::option::Option<&'static str> { #input_type_opt }
|
||||
fn output_type(&self) -> &'static str { #output_type_name }
|
||||
fn output_shape(&self) -> ::mizan_core::NamedType { #output_shape_expr }
|
||||
fn output_nullable(&self) -> bool { #output_nullable }
|
||||
fn context(&self) -> ::std::option::Option<&'static str> { #context_value }
|
||||
fn affects(&self) -> &'static [::mizan_core::AffectTarget] { #affects_static }
|
||||
@@ -416,57 +445,15 @@ pub fn expand(args: FunctionArgs, item: ItemFn) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_input_args(item: &ItemFn) -> syn::Result<Vec<InputArg>> {
|
||||
let mut out = Vec::new();
|
||||
let mut iter = item.sig.inputs.iter();
|
||||
// First arg is the request handle — skip without inspection. The function
|
||||
// body uses it directly; the dispatch wrapper forwards `req`.
|
||||
if iter.next().is_none() {
|
||||
return Err(syn::Error::new(
|
||||
item.sig.span(),
|
||||
"#[mizan] functions must accept at least a request handle as the first parameter (e.g. `&Request` or `RequestHandle`).",
|
||||
));
|
||||
}
|
||||
for arg in iter {
|
||||
match arg {
|
||||
FnArg::Typed(pat) => {
|
||||
let ident = match &*pat.pat {
|
||||
Pat::Ident(pi) => pi.ident.clone(),
|
||||
_ => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&pat.pat,
|
||||
"#[mizan] function parameters must be plain identifiers (no destructuring).",
|
||||
));
|
||||
}
|
||||
};
|
||||
out.push(InputArg {
|
||||
ident,
|
||||
ty: (*pat.ty).clone(),
|
||||
});
|
||||
}
|
||||
FnArg::Receiver(_) => {
|
||||
return Err(syn::Error::new_spanned(
|
||||
arg,
|
||||
"#[mizan] functions are free functions, not methods. `self` is not allowed.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn build_dispatch(
|
||||
item: &ItemFn,
|
||||
inner: &Ident,
|
||||
input_args: &[InputArg],
|
||||
has_input: bool,
|
||||
input_type_ident: &syn::Ident,
|
||||
input_type_ident: &Ident,
|
||||
returns_result: bool,
|
||||
) -> TokenStream {
|
||||
let inner = &item.sig.ident;
|
||||
// When the user returns `Result<T, MizanError>`, lift Err out into the
|
||||
// dispatch wrapper's outer Result so the HTTP/IPC adapter can surface
|
||||
// it as the standard error envelope. When the user returns `T`,
|
||||
// serialize directly — the substrate has no error path for them.
|
||||
// `?` lifts a user `Result<T, MizanError>`'s Err into the wrapper's outer
|
||||
// Result; a plain `T` serializes directly.
|
||||
let unwrap_user_result = if returns_result {
|
||||
quote! { ? }
|
||||
} else {
|
||||
@@ -501,16 +488,17 @@ fn build_dispatch(
|
||||
}
|
||||
}
|
||||
|
||||
fn element_type_static_ident_scoped(ty: &Type, fn_scope: &str) -> syn::Ident {
|
||||
// Derive a unique static-name for the type's registration entry,
|
||||
// scoped by the surrounding function so siblings returning the same
|
||||
// `Vec<T>` don't collide at the static-name layer. The IR-side
|
||||
// BTreeMap dedupes by TypeEntry.name at emission time.
|
||||
let last = match ty {
|
||||
Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()),
|
||||
_ => None,
|
||||
/// A static-name for the element type's registration entry, scoped by the
|
||||
/// surrounding function so siblings returning the same `Vec<T>` don't collide
|
||||
/// at the static-name layer.
|
||||
fn element_type_static_ident_scoped(ty: &Type, fn_scope: &str) -> Ident {
|
||||
let stem = match path_head(ty) {
|
||||
Head::Path { name, .. } => name,
|
||||
Head::Unnamed => "ANON".to_string(),
|
||||
};
|
||||
let suffix = last.unwrap_or_else(|| "ANON".to_string()).to_shouty_snake_case();
|
||||
format_ident!("__MIZAN_TYPE_ELEM_{}_FOR_{}", suffix, fn_scope)
|
||||
format_ident!(
|
||||
"__MIZAN_TYPE_ELEM_{}_FOR_{}",
|
||||
stem.to_shouty_snake_case(),
|
||||
fn_scope
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,34 @@
|
||||
//! Proc macros for `mizan-core`. See sibling modules for each macro's body.
|
||||
//! Proc macros for `mizan-core`. See sibling modules for each macro's body:
|
||||
//! `derive` for `#[derive(Mizan)]`, `context` / `function` / `channel` for the
|
||||
//! three attribute macros, `shape` for the shared `syn::Type` lowering.
|
||||
//!
|
||||
//! Consumer code reads:
|
||||
//! ```ignore
|
||||
//! use mizan_core::prelude::*;
|
||||
//! pub use mizan_core as mizan; // so `#[mizan::context]` / `#[mizan::client]` read naturally
|
||||
//!
|
||||
//! #[derive(Mizan, serde::Serialize, serde::Deserialize)]
|
||||
//! pub struct ProfileOutput { pub user_id: i64, pub name: String }
|
||||
//!
|
||||
//! #[mizan::context("user")]
|
||||
//! pub struct UserCtx;
|
||||
//!
|
||||
//! #[mizan::client(context = UserCtx)]
|
||||
//! pub async fn user_profile(req: &Request, user_id: i64) -> ProfileOutput { ... }
|
||||
//! ```
|
||||
//!
|
||||
//! The function macro is named `client` to mirror Python's `@client`
|
||||
//! decorator and to keep the namespace `mizan::` purely a module path —
|
||||
//! `#[mizan(...)]` would collide with `mizan::context` (a module path
|
||||
//! can't simultaneously be a callable macro in Rust).
|
||||
//! The function macro is named `client` so `mizan::` stays purely a module
|
||||
//! path — a module path can't simultaneously be a callable macro in Rust, so
|
||||
//! `#[mizan(...)]` would collide with `mizan::context`.
|
||||
|
||||
mod channel;
|
||||
mod context;
|
||||
mod derive;
|
||||
mod function;
|
||||
mod shape;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{parse_macro_input, DeriveInput, ItemFn, ItemStruct};
|
||||
use syn::{parse_macro_input, ItemStruct};
|
||||
|
||||
#[proc_macro_derive(Mizan)]
|
||||
pub fn derive_mizan(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
derive::expand(input).into()
|
||||
let derived = parse_macro_input!(input as derive::MizanDerive);
|
||||
derive::expand(derived).into()
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn context(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let args = match context::ContextArgs::parse(attr.into()) {
|
||||
Ok(a) => a,
|
||||
let name = match context::ContextName::parse(attr.into()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
let item = parse_macro_input!(item as ItemStruct);
|
||||
context::expand(args, item).into()
|
||||
context::expand(name, item).into()
|
||||
}
|
||||
|
||||
/// The function-registration attribute macro. Used as `#[mizan::client]`
|
||||
@@ -49,10 +36,17 @@ pub fn context(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
/// websocket, private)]`.
|
||||
#[proc_macro_attribute]
|
||||
pub fn client(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let args = match function::FunctionArgs::parse(attr.into()) {
|
||||
Ok(a) => a,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
let item = parse_macro_input!(item as ItemFn);
|
||||
function::expand(args, item).into()
|
||||
let args = parse_macro_input!(attr as function::FunctionArgs);
|
||||
let handler = parse_macro_input!(item as function::Handler);
|
||||
function::expand(args, handler).into()
|
||||
}
|
||||
|
||||
/// The channel-registration attribute macro. Used as
|
||||
/// `#[mizan::channel("<wire-name>", params = P, client_message = C,
|
||||
/// server_message = S)]` on a unit struct; every slot is optional.
|
||||
#[proc_macro_attribute]
|
||||
pub fn channel(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let args = parse_macro_input!(attr as channel::ChannelArgs);
|
||||
let item = parse_macro_input!(item as ItemStruct);
|
||||
channel::expand(args, item).into()
|
||||
}
|
||||
|
||||
@@ -6,203 +6,189 @@ use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{GenericArgument, PathArguments, Type, TypePath};
|
||||
|
||||
/// Result of inspecting a fn's return type.
|
||||
/// The IR-relevant form of a Rust type. Every `syn::Type` lands in exactly
|
||||
/// one arm, so classification never reports "unknown".
|
||||
pub enum TypeForm {
|
||||
/// `Option<T>` — the wire field is nullable.
|
||||
Optional(Type),
|
||||
/// `Vec<T>`, `[T; N]`, or a map whose values are `T` — a JSON array.
|
||||
Sequence(Type),
|
||||
/// A scalar, carrying the `::mizan_core::Primitive` variant expression.
|
||||
Primitive(TokenStream),
|
||||
/// Anything else: a type expected to implement `MizanType`.
|
||||
Named(Type),
|
||||
}
|
||||
|
||||
/// What a type's head is, as the lowering reads it. `Unnamed` covers the
|
||||
/// forms with no path to name — tuples, references, slices, bare fns — which
|
||||
/// carry no keyword the callers below test for.
|
||||
pub enum Head {
|
||||
Path { name: String, generics: Vec<Type> },
|
||||
Unnamed,
|
||||
}
|
||||
|
||||
/// Which of the two output shapes a handler's return type produces.
|
||||
pub enum ReturnForm {
|
||||
/// The handler yields a list; the caller registers an alias type over
|
||||
/// `element`'s Ref.
|
||||
Sequence { element: Type },
|
||||
/// The handler yields one value; the caller registers `inner`'s own shape
|
||||
/// under the canonical output name.
|
||||
Scalar { inner: Type },
|
||||
}
|
||||
|
||||
pub struct ReturnAnalysis {
|
||||
/// Inner type once `Option<...>` is unwrapped.
|
||||
pub inner: Type,
|
||||
/// True if the outermost wrapper is `Option<...>`.
|
||||
pub form: ReturnForm,
|
||||
/// True if the outermost wrapper (after `Result`) is `Option<...>`.
|
||||
pub nullable: bool,
|
||||
/// True if `inner` is `Vec<T>` — caller emits an alias type entry.
|
||||
pub is_vec: bool,
|
||||
/// When `is_vec`, this is the element type `T`.
|
||||
pub vec_inner: Option<Type>,
|
||||
/// True when the user's return type is `Result<T, MizanError>` — the
|
||||
/// dispatch wrapper emits `?` so user-side errors bubble out as
|
||||
/// `MizanError` instead of being serialized into the success payload.
|
||||
/// The IR sees only the `T` side; the error variant is the substrate's
|
||||
/// invariant, not part of the output shape.
|
||||
pub returns_result: bool,
|
||||
}
|
||||
|
||||
pub fn analyze_return(ty: &Type) -> ReturnAnalysis {
|
||||
let (effective, returns_result) = if let Some(ok) = unwrap_result_ok(ty) {
|
||||
(ok, true)
|
||||
} else {
|
||||
(ty.clone(), false)
|
||||
let (effective, returns_result) = strip_result(ty);
|
||||
let (unwrapped, nullable) = match classify(&effective) {
|
||||
TypeForm::Optional(inner) => (inner, true),
|
||||
TypeForm::Sequence(_) | TypeForm::Primitive(_) | TypeForm::Named(_) => (effective, false),
|
||||
};
|
||||
let (inner, nullable) = if let Some(t) = unwrap_option(&effective) {
|
||||
(t, true)
|
||||
} else {
|
||||
(effective, false)
|
||||
let form = match classify(&unwrapped) {
|
||||
TypeForm::Sequence(element) => ReturnForm::Sequence { element },
|
||||
TypeForm::Optional(_) | TypeForm::Primitive(_) | TypeForm::Named(_) => {
|
||||
ReturnForm::Scalar { inner: unwrapped }
|
||||
}
|
||||
};
|
||||
if let Some(elem) = unwrap_vec(&inner) {
|
||||
ReturnAnalysis {
|
||||
inner: inner.clone(),
|
||||
nullable,
|
||||
is_vec: true,
|
||||
vec_inner: Some(elem),
|
||||
returns_result,
|
||||
}
|
||||
} else {
|
||||
ReturnAnalysis {
|
||||
inner,
|
||||
nullable,
|
||||
is_vec: false,
|
||||
vec_inner: None,
|
||||
returns_result,
|
||||
}
|
||||
ReturnAnalysis {
|
||||
form,
|
||||
nullable,
|
||||
returns_result,
|
||||
}
|
||||
}
|
||||
|
||||
/// If `ty` is `Result<T, E>`, return `T`. Otherwise None. The substrate
|
||||
/// only honors `Result<T, MizanError>`; the macro doesn't try to verify
|
||||
/// `E` here — it lets rustc raise the type-mismatch at the `?` site if
|
||||
/// the consumer used a non-MizanError variant.
|
||||
pub fn unwrap_result_ok(ty: &Type) -> Option<Type> {
|
||||
let path = match ty {
|
||||
Type::Path(TypePath { qself: None, path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let last = path.segments.last()?;
|
||||
if last.ident != "Result" {
|
||||
return None;
|
||||
/// Peel `Result<T, E>` down to `T`. `E` is left to rustc: a non-`MizanError`
|
||||
/// error type fails at the `?` site the dispatch wrapper emits.
|
||||
pub fn strip_result(ty: &Type) -> (Type, bool) {
|
||||
if let Head::Path { name, generics } = path_head(ty) {
|
||||
if name == "Result" {
|
||||
if let [ok, ..] = generics.as_slice() {
|
||||
return (ok.clone(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
extract_single_generic(&last.arguments)
|
||||
(ty.clone(), false)
|
||||
}
|
||||
|
||||
/// Emit a `TypeShape` const-expression for `ty`. Used inside `#[derive(Mizan)]`
|
||||
/// when constructing the struct field shapes.
|
||||
pub fn classify(ty: &Type) -> TypeForm {
|
||||
if let Type::Array(array) = ty {
|
||||
return TypeForm::Sequence((*array.elem).clone());
|
||||
}
|
||||
let Head::Path { name, generics } = path_head(ty) else {
|
||||
return TypeForm::Named(ty.clone());
|
||||
};
|
||||
let args = generics.as_slice();
|
||||
if name == "Option" {
|
||||
if let [inner, ..] = args {
|
||||
return TypeForm::Optional(inner.clone());
|
||||
}
|
||||
}
|
||||
if name == "Vec" {
|
||||
if let [element, ..] = args {
|
||||
return TypeForm::Sequence(element.clone());
|
||||
}
|
||||
}
|
||||
if name == "BTreeMap" || name == "HashMap" {
|
||||
// A string-keyed map lands on the wire as a JSON object; the IR
|
||||
// carries only the value shape, as a list element.
|
||||
if let [_key, value, ..] = args {
|
||||
return TypeForm::Sequence(value.clone());
|
||||
}
|
||||
}
|
||||
classify_scalar(ty, &name)
|
||||
}
|
||||
|
||||
pub fn is_optional(ty: &Type) -> bool {
|
||||
matches!(classify(ty), TypeForm::Optional(_))
|
||||
}
|
||||
|
||||
/// Emit a `TypeShape` const-expression for `ty`. Used inside
|
||||
/// `#[derive(Mizan)]` when constructing the struct field shapes.
|
||||
pub fn type_shape_expr(ty: &Type) -> TokenStream {
|
||||
if let Some(inner) = unwrap_option(ty) {
|
||||
let inner_shape = type_shape_expr(&inner);
|
||||
return quote! {
|
||||
::mizan_core::TypeShape::Optional(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(elem) = unwrap_vec(ty) {
|
||||
let inner_shape = type_shape_expr(&elem);
|
||||
return quote! {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(elem) = unwrap_array(ty) {
|
||||
// `[T; N]` lowers to `list { T }` on the wire — JSON arrays don't
|
||||
// carry length, so the IR contract is the same as `Vec<T>`.
|
||||
let inner_shape = type_shape_expr(&elem);
|
||||
return quote! {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(elem) = unwrap_btreemap_value(ty) {
|
||||
// `BTreeMap<K, V>` on the wire is a JSON object keyed by `K`'s
|
||||
// string form. The Mizan IR doesn't model dynamic-keyed maps as a
|
||||
// distinct shape — closest equivalent is a list of value entries.
|
||||
let inner_shape = type_shape_expr(&elem);
|
||||
return quote! {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
};
|
||||
}
|
||||
if let Some(p) = primitive_of(ty) {
|
||||
return quote! { ::mizan_core::TypeShape::Primitive(#p) };
|
||||
}
|
||||
// Fallback: assume a user-defined struct/enum implementing MizanType.
|
||||
// The Ref name comes from `<T as MizanType>::TYPE_NAME` (associated const).
|
||||
quote! { ::mizan_core::TypeShape::Ref(<#ty as ::mizan_core::MizanType>::TYPE_NAME) }
|
||||
}
|
||||
|
||||
/// If `ty` is `[T; N]`, return `T`. Otherwise None.
|
||||
pub fn unwrap_array(ty: &Type) -> Option<Type> {
|
||||
if let Type::Array(a) = ty {
|
||||
Some((*a.elem).clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If `ty` is `BTreeMap<K, V>` or `HashMap<K, V>`, return `V` (the value).
|
||||
/// String-keyed maps land on the wire as JSON objects; the IR carries the
|
||||
/// value shape as a list element since KDL doesn't model dynamic-keyed maps
|
||||
/// distinctly yet.
|
||||
pub fn unwrap_btreemap_value(ty: &Type) -> Option<Type> {
|
||||
let path = match ty {
|
||||
Type::Path(TypePath { qself: None, path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let last = path.segments.last()?;
|
||||
let name = last.ident.to_string();
|
||||
if name != "BTreeMap" && name != "HashMap" {
|
||||
return None;
|
||||
}
|
||||
let args = match &last.arguments {
|
||||
PathArguments::AngleBracketed(a) => a,
|
||||
_ => return None,
|
||||
};
|
||||
// BTreeMap<K, V> — second type argument is V.
|
||||
let mut type_args = args.args.iter().filter_map(|a| {
|
||||
if let GenericArgument::Type(t) = a {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
None
|
||||
match classify(ty) {
|
||||
TypeForm::Optional(inner) => {
|
||||
let inner_shape = type_shape_expr(&inner);
|
||||
quote! {
|
||||
::mizan_core::TypeShape::Optional(::std::boxed::Box::new(#inner_shape))
|
||||
}
|
||||
}
|
||||
});
|
||||
type_args.next()?; // skip K
|
||||
type_args.next()
|
||||
}
|
||||
|
||||
/// Emit a `Primitive` const-expression for `ty`, or `None` if `ty` isn't a
|
||||
/// known primitive scalar.
|
||||
pub fn primitive_of(ty: &Type) -> Option<TokenStream> {
|
||||
let path = match ty {
|
||||
Type::Path(TypePath { qself: None, path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let last = path.segments.last()?;
|
||||
let name = last.ident.to_string();
|
||||
match name.as_str() {
|
||||
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
|
||||
| "usize" => Some(quote! { ::mizan_core::Primitive::Integer }),
|
||||
"f32" | "f64" => Some(quote! { ::mizan_core::Primitive::Number }),
|
||||
"bool" => Some(quote! { ::mizan_core::Primitive::Boolean }),
|
||||
"String" | "str" => Some(quote! { ::mizan_core::Primitive::String }),
|
||||
_ => None,
|
||||
TypeForm::Sequence(element) => {
|
||||
let inner_shape = type_shape_expr(&element);
|
||||
quote! {
|
||||
::mizan_core::TypeShape::List(::std::boxed::Box::new(#inner_shape))
|
||||
}
|
||||
}
|
||||
TypeForm::Primitive(primitive) => {
|
||||
quote! { ::mizan_core::TypeShape::Primitive(#primitive) }
|
||||
}
|
||||
TypeForm::Named(named) => ref_shape_expr(&named),
|
||||
}
|
||||
}
|
||||
|
||||
/// If `ty` is `Option<T>`, return `T`. Otherwise None.
|
||||
pub fn unwrap_option(ty: &Type) -> Option<Type> {
|
||||
let path = match ty {
|
||||
Type::Path(TypePath { qself: None, path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let last = path.segments.last()?;
|
||||
if last.ident != "Option" {
|
||||
return None;
|
||||
/// A `TypeShape::Ref` carrying both the referent's IR name and its shape
|
||||
/// constructor, so resolving the reference needs no registry lookup.
|
||||
pub fn ref_shape_expr(ty: &Type) -> TokenStream {
|
||||
quote! {
|
||||
::mizan_core::TypeShape::Ref {
|
||||
name: <#ty as ::mizan_core::MizanType>::TYPE_NAME,
|
||||
shape: <#ty as ::mizan_core::MizanType>::shape,
|
||||
}
|
||||
}
|
||||
extract_single_generic(&last.arguments)
|
||||
}
|
||||
|
||||
/// If `ty` is `Vec<T>`, return `T`. Otherwise None.
|
||||
pub fn unwrap_vec(ty: &Type) -> Option<Type> {
|
||||
let path = match ty {
|
||||
Type::Path(TypePath { qself: None, path }) => path,
|
||||
_ => return None,
|
||||
};
|
||||
let last = path.segments.last()?;
|
||||
if last.ident != "Vec" {
|
||||
return None;
|
||||
const INTEGER_IDENTS: &[&str] = &[
|
||||
"i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
|
||||
];
|
||||
|
||||
fn classify_scalar(ty: &Type, name: &str) -> TypeForm {
|
||||
if INTEGER_IDENTS.contains(&name) {
|
||||
return TypeForm::Primitive(quote! { ::mizan_core::Primitive::Integer });
|
||||
}
|
||||
extract_single_generic(&last.arguments)
|
||||
if name == "f32" || name == "f64" {
|
||||
return TypeForm::Primitive(quote! { ::mizan_core::Primitive::Number });
|
||||
}
|
||||
if name == "bool" {
|
||||
return TypeForm::Primitive(quote! { ::mizan_core::Primitive::Boolean });
|
||||
}
|
||||
if name == "String" || name == "str" {
|
||||
return TypeForm::Primitive(quote! { ::mizan_core::Primitive::String });
|
||||
}
|
||||
TypeForm::Named(ty.clone())
|
||||
}
|
||||
|
||||
fn extract_single_generic(args: &PathArguments) -> Option<Type> {
|
||||
let args = match args {
|
||||
/// The last path segment's identifier and its generic type arguments.
|
||||
pub fn path_head(ty: &Type) -> Head {
|
||||
if let Type::Path(TypePath { qself: None, path }) = ty {
|
||||
if let Some(last) = path.segments.last() {
|
||||
return Head::Path {
|
||||
name: last.ident.to_string(),
|
||||
generics: generic_types(&last.arguments),
|
||||
};
|
||||
}
|
||||
}
|
||||
Head::Unnamed
|
||||
}
|
||||
|
||||
fn generic_types(args: &PathArguments) -> Vec<Type> {
|
||||
let angled = match args {
|
||||
PathArguments::AngleBracketed(a) => a,
|
||||
_ => return None,
|
||||
PathArguments::None => return Vec::new(),
|
||||
PathArguments::Parenthesized(_) => return Vec::new(),
|
||||
};
|
||||
for arg in &args.args {
|
||||
let mut out = Vec::new();
|
||||
for arg in &angled.args {
|
||||
if let GenericArgument::Type(t) = arg {
|
||||
return Some(t.clone());
|
||||
out.push(t.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user