Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions servify_macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ quote = "1.0.37"
syn = { version = "2.0.77", features = ["full"] }

[dev-dependencies]
insta = "1.41.1"
pretty_assertions = "1.4.1"
rust-format = "0.3.4"
thiserror = "1.0.67"
10 changes: 10 additions & 0 deletions servify_macro/src/export_macro/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use syn::parse::{Parse, ParseStream};

/// Represents the arguments of the `#[servify::export]` attribute.
pub(crate) struct ServifyExportArgs {}

impl Parse for ServifyExportArgs {
fn parse(_input: ParseStream) -> syn::Result<Self> {
Ok(Self {})
}
}
8 changes: 8 additions & 0 deletions servify_macro/src/export_macro/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub(super) const ERR_MULTIPLE_FN_IN_EXPORT: &str =
"Only one function can be exported in one impl block with servify::export";

pub(super) const ERR_NO_FN_IN_EXPORT: &str =
"One function must be exported in one impl block with servify::export";

pub(super) const ERR_UNEXPECTED_ITEM_IN_EXPORT: &str =
"Supported items are only exporting function, associated function, associated constants, and Request struct";
63 changes: 63 additions & 0 deletions servify_macro/src/export_macro/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
mod args;
mod errors;
mod tests;
mod to_token;

use proc_macro2::Span;
use syn::parse::{Parse, ParseStream};
use syn::{braced, Ident, ImplItemFn, Token, TypePath};

use crate::export_macro::errors::{
ERR_MULTIPLE_FN_IN_EXPORT, ERR_NO_FN_IN_EXPORT, ERR_UNEXPECTED_ITEM_IN_EXPORT,
};

pub(crate) use self::args::ServifyExportArgs;

/// Represents the `#[servify::export]` content.
pub(crate) struct ServifyExport {
service_module_path: TypePath,
fn_item: ImplItemFn,
fn_name: Ident,
}

impl Parse for ServifyExport {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let mut fn_item = None;

// Parse head impl block
let _: Token![impl] = input.parse()?;
let module_path: TypePath = input.parse()?;
braced!(content in input);

// Parse all items in the export block
while !content.is_empty() {
if let Ok(item) = content.parse::<ImplItemFn>() {
if fn_item.is_some() {
return Err(syn::Error::new_spanned(item, ERR_MULTIPLE_FN_IN_EXPORT));
}

fn_item.replace(item);
continue;
}

// Error if there is any other item in the export block
return Err(syn::Error::new_spanned(
content.parse::<syn::Item>()?,
ERR_UNEXPECTED_ITEM_IN_EXPORT,
));
}

// Ensure that there is a function in the export block
let fn_item =
fn_item.ok_or_else(|| syn::Error::new(Span::call_site(), ERR_NO_FN_IN_EXPORT))?;

// TODO: combine multiple error

Ok(Self {
service_module_path: module_path,
fn_name: fn_item.sig.ident.clone(),
fn_item,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: servify_macro/src/export_macro/tests.rs
expression: "test(quote! {},\n quote! {\n impl super::counter\n {\n pub fn increment(&mut self, amount: u32) -> u32\n { self.count += amount; self.amount }\n }\n }).unwrap()"
---
mod counter_increment {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: servify_macro/src/export_macro/tests.rs
expression: "test(quote! {},\n quote! {\n impl super::counter\n {\n pub fn increment(&mut self, amount: u32) -> u32\n { self.count += amount; self.amount } struct\n ThisMustBeUnexpected {};\n }\n }).unwrap_err()"
---
Failed to parse item: :: core :: compile_error ! { "Supported items within the servify::export macro are only exporting function, associated function, associated constants, and Request struct" }
65 changes: 65 additions & 0 deletions servify_macro/src/export_macro/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#![cfg(test)]
use insta::assert_snapshot;
use proc_macro2::TokenStream;
use quote::quote;
use rust_format::{Formatter, RustFmt};
use thiserror::Error;

use crate::export_macro::{ServifyExport, ServifyExportArgs};

#[derive(Debug, Error)]
enum Error {
#[error("Failed to parse args: {0}")]
ParseArgs(TokenStream),
#[error("Failed to parse item: {0}")]
ParseItem(TokenStream),
#[error("Failed to format: {0}")]
RustFmt(#[from] rust_format::Error),
}

fn test(args: TokenStream, item: TokenStream) -> Result<String, Error> {
let args = syn::parse2::<ServifyExportArgs>(args)
.map_err(|e| e.to_compile_error())
.map_err(Error::ParseArgs)?;
let item = syn::parse2::<ServifyExport>(item)
.map_err(|e| e.to_compile_error())
.map_err(Error::ParseItem)?;

let generated = item.to_tokens(args).to_string();

let formatted = RustFmt::default().format_str(generated)?;
Ok(formatted)
}

#[test]
fn test_snapshot_export_all_written() {
assert_snapshot!(test(
quote! {},
quote! {
impl super::counter {
pub fn increment(&mut self, amount: u32) -> u32 {
self.count += amount;
self.amount
}
}
}
)
.unwrap());
}

#[test]
fn test_snapshot_export_unwanted_struct() {
assert_snapshot!(test(
quote! {},
quote! {
impl super::counter {
pub fn increment(&mut self, amount: u32) -> u32 {
self.count += amount;
self.amount
}
struct ThisMustBeUnexpected { };
}
}
)
.unwrap_err());
}
24 changes: 24 additions & 0 deletions servify_macro/src/export_macro/to_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;

use crate::util::ident_ext::IdentExt as _;

use super::{ServifyExport, ServifyExportArgs};

impl ServifyExport {
pub(crate) fn to_tokens(self, args: ServifyExportArgs) -> TokenStream {
let ServifyExport {
service_module_path,
fn_item,
fn_name,
} = self;

let service_name = &service_module_path.path.segments.last().unwrap().ident;
let mod_name = Ident::new_with_call_site(&format!("{service_name}_{fn_name}"));

quote! {
mod #mod_name { }
}
}
}
16 changes: 15 additions & 1 deletion servify_macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
mod util;
use proc_macro::TokenStream;

use export_macro::ServifyExport;
use export_macro::ServifyExportArgs;

pub(crate) mod export_macro;
pub(crate) mod util;

#[proc_macro_attribute]
pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = syn::parse_macro_input!(attr as ServifyExportArgs);
syn::parse_macro_input!(item as ServifyExport)
.to_tokens(attr)
.into()
}
12 changes: 12 additions & 0 deletions servify_macro/src/util/ident_ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use proc_macro2::Span;
use syn::Ident;

pub(crate) trait IdentExt {
fn new_with_call_site(name: &str) -> Ident;
}

impl IdentExt for Ident {
fn new_with_call_site(name: &str) -> Ident {
Ident::new(name, Span::call_site())
}
}
5 changes: 3 additions & 2 deletions servify_macro/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod return_type_ext;
pub mod type_path_ext;
pub(crate) mod ident_ext;
pub(crate) mod return_type_ext;
pub(crate) mod type_path_ext;