Rust中的过程宏是一个非常强大的代码生成工具,例如,您无需编写大量样板代码或表达一些新概念(如板条箱的开发人员),就可以做到这一点async_trait
。
但是,许多人理所当然地害怕使用此工具,这主要是由于解析语法树和宏属性经常变成“手动日落”的事实,因为必须在非常低的水平上解决问题。
我认为,在本文中,我想分享一些成功的过程宏编写方法,并说明当今可以相对简单方便地创建过程宏。
前言
首先,让我们定义要在宏的帮助下解决的任务:我们将尝试以特征形式定义一些抽象的RPC API,然后再实现服务器部分和客户端部分。反过来,程序宏将帮助我们在没有大量样板代码的情况下完成操作。尽管我们将实现某种程度上抽象的API,但该任务实际上是至关重要的,除其他外,它是演示程序宏功能的理想选择。
API本身将根据非常简单的原则执行:有4种类型的请求:
- 没有参数的GET请求,例如:
/ping
。 - 带有参数的GET请求,其参数将以URL查询的形式传输,例如:
/status?name=foo&count=15
。 - 没有参数的POST请求。
- 带有以JSON对象传递的参数的POST请求。
在所有情况下,服务器都将使用有效的JSON对象进行响应。
作为服务器后端,我们将使用crate warp
。
理想情况下,我想得到这样的东西:
#[derive(Debug, FromUrlQuery, Deserialize, Serialize)]
struct Query {
first: String,
second: u64,
}
#[http_api(warp = "serve_ping_interface")]
trait PingInterface {
#[http_api_endpoint(method = "get")]
fn get(&self) -> Result<Query, Error>;
#[http_api_endpoint(method = "get")]
fn check(&self, query: Query) -> Result<bool, Error>;
#[http_api_endpoint(method = "post")]
fn set_value(&self, param: Query) -> Result<(), Error>;
#[http_api_endpoint(method = "post")]
fn increment(&self) -> Result<(), Error>;
}
#[derive(Debug, Default)]
struct ServiceInner {
first: String,
second: u64,
}
#[derive(Clone, Default)]
struct ServiceImpl(Arc<RwLock<ServiceInner>>);
impl ServiceImpl {
fn new() -> Self {
Self::default()
}
fn read(&self) -> RwLockReadGuard<ServiceInner> {
self.0.read().unwrap()
}
fn write(&self) -> RwLockWriteGuard<ServiceInner> {
self.0.write().unwrap()
}
}
impl PingInterface for ServiceImpl {
fn get(&self) -> Result<Query, Error> {
let inner = self.read();
Ok(Query {
first: inner.first.clone(),
second: inner.second,
})
}
fn check(&self, query: Query) -> Result<bool, Error> {
let inner = self.read();
Ok(inner.first == query.first && inner.second == query.second)
}
fn set_value(&self, param: Query) -> Result<(), Error> {
let mut inner = self.write();
inner.first = param.first;
inner.second = param.second;
Ok(())
}
fn increment(&self) -> Result<(), Error> {
self.write().second += 1;
Ok(())
}
}
#[tokio::main]
async fn main() {
let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
serve_ping_interface(ServiceImpl::new(), addr).await
}
, Rust' , , , .
: derive-, - ( serde
), , .
, : http_api
, , http_api_derive
.
FromUrlQuery
, — , , . , - , .
, . URL query. , . :
pub trait FromUrlQuery: Sized {
fn from_query_str(query: &str) -> Result<Self, ParseQueryError>;
}
, . derive :
#[proc_macro_derive(FromUrlQuery, attributes(from_url_query))]
pub fn from_url_query(input: TokenStream) -> TokenStream {
from_url_query::impl_from_url_query(input)
}
— syn
, quote
. Rust
, , .
quote!
, Rust , .
, , , darling
. ( , ).
, ** **. :AST , , .
fn get_field_names(input: &DeriveInput) -> Option<Vec<(Ident, Action)>> {
let data = match &input.data {
Data::Struct(x) => Some(x),
Data::Enum(..) => None,
_ => panic!("Protobuf convert can be derived for structs and enums only."),
};
data.map(|data| {
data.fields
.iter()
.map(|f| {
let mut action = Action::Convert;
for attr in &f.attrs {
match attr.parse_meta() {
Ok(syn::Meta::List(ref meta)) if meta.ident == "protobuf_convert" => {
for nested in &meta.nested {
match nested {
syn::NestedMeta::Meta(syn::Meta::Word(ident))
if ident == "skip" =>
{
action = Action::Skip;
}
_ => {
panic!("Unknown attribute");
}
}
}
}
_ => {
}
}
}
(f.ident.clone().unwrap(), action)
})
.collect()
})
}
fn get_field_names_enum(input: &DeriveInput) -> Option<Vec<Ident>> {
let data = match &input.data {
Data::Struct(..) => None,
Data::Enum(x) => Some(x),
_ => panic!("Protobuf convert can be derived for structs and enums only."),
};
data.map(|data| data.variants.iter().map(|f| f.ident.clone()).collect())
}
fn implement_protobuf_convert_from_pb(field_names: &[(Ident, Action)]) -> impl quote::ToTokens {
let mut to_convert = vec![];
let mut to_skip = vec![];
for (x, a) in field_names {
match a {
Action::Convert => to_convert.push(x),
Action::Skip => to_skip.push(x),
}
}
let getters = to_convert
.iter()
.map(|i| Ident::new(&format!("get_{}", i), Span::call_site()));
let our_struct_names = to_convert.clone();
let our_struct_names_skip = to_skip;
quote! {
fn from_pb(pb: Self::ProtoStruct) -> std::result::Result<Self, _FailureError> {
Ok(Self {
#( #our_struct_names: ProtobufConvert::from_pb(pb.#getters().to_owned())?, )*
#( #our_struct_names_skip: Default::default(), )*
})
}
}
}
darling
, .
, FromUrlQuery
, , . , - :
#[derive(FromUrlQuery)]
struct OptionalQuery {
first: String,
opt_value: Option<u64>,
}
darling
' , .
— FromField
, :
#[derive(Clone, Debug, FromField)]
struct QueryField {
ident: Option<syn::Ident>,
ty: syn::Type,
}
, , , :
#[derive(Clone, Debug, FromField)]
struct QueryField {
ident: Option<syn::Ident>,
ty: syn::Type,
vis: syn::Visibility,
}
— FromDeriveInput
, :
#[derive(Debug, FromDeriveInput)]
#[darling(supports(struct_named))]
struct FromUrlQuery {
ident: syn::Ident,
data: darling::ast::Data<(), QueryField>,
}
, .
let input: DeriveInput = syn::parse(input).unwrap();
let from_url_query = match FromUrlQuery::from_derive_input(&input) {
Ok(parsed) => parsed,
Err(e) => return e.write_errors().into(),
};
.
, URL query serde
. serde
, . Deserialize
, serde_urlencoded
. serde
, .
#[doc(hidden)]
pub mod export {
pub use serde;
pub use serde_derive;
pub use serde_urlencoded;
}
, FromUrlQuery
:
impl FromUrlQuery {
fn serde_wrapper_ident(&self) -> syn::Ident {
let ident_str = format!("{}Serde", self.ident);
syn::Ident::new(&ident_str, proc_macro2::Span::call_site())
}
fn impl_serde_wrapper(&self) -> impl ToTokens {
let fields = self.data.clone().take_struct().unwrap();
let wrapped_fields = fields.iter().map(|field| {
let ident = &field.ident;
let ty = &field.ty;
quote! { #ident: #ty }
});
let from_fields = fields.iter().map(|field| {
let ident = &field.ident;
quote! { #ident: v.#ident }
});
let wrapped_ident = self.serde_wrapper_ident();
let ident = &self.ident;
quote! {
use http_api::export::serde_derive::Deserialize;
#[derive(Deserialize)]
#[serde(crate = "http_api::export::serde")]
struct #wrapped_ident {
#( #wrapped_fields, )*
}
impl From<#wrapped_ident> for #ident {
fn from(v: #wrapped_ident) -> Self {
Self {
#( #from_fields, )*
}
}
}
}
}
}
, , , , , . , , ; , , .
http_api
FromDeriveInput
, darling
' , AST. , , :
:
#[proc_macro_attribute]
pub fn http_api(attr: TokenStream, item: TokenStream) -> TokenStream {
http_api::impl_http_api(attr, item)
}
, : , (, http_api_endpoint
), . , TokenStream
, "cannot find attribute http_api_endpoint
in this scope", . , , , . , , , .
, http_api_endpoint
, , .
#[proc_macro_attribute]
#[doc(hidden)]
pub fn http_api_endpoint(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
, , .
, , :
#[http_api_endpoint(method = "#method_type")]
fn #method_name(&self) -> Result<$ResponseType, Error>;
#[http_api_endpoint(method = "#method_type")]
fn #method_name(&self, query: $QueryType) -> Result<$ResponseType, Error>;
HTTP , :
#[derive(Debug)]
enum SupportedHttpMethod {
Get,
Post,
}
impl FromMeta for SupportedHttpMethod {
fn from_string(value: &str) -> Result<Self, darling::Error> {
match value {
"get" => Ok(SupportedHttpMethod::Get),
"post" => Ok(SupportedHttpMethod::Post),
other => Err(darling::Error::unknown_value(other)),
}
}
}
, :
#[derive(Debug, FromMeta)]
struct EndpointAttrs {
method: SupportedHttpMethod,
#[darling(default)]
rename: Option<String>,
}
, syn::Signature
, darling' : , FromMeta
.
http_api_endpoint
. syn::NestedMeta
, (foo = "bar", boo(first, second))
.
fn find_meta_attrs(name: &str, args: &[syn::Attribute]) -> Option<syn::NestedMeta> {
args.as_ref()
.iter()
.filter_map(|a| a.parse_meta().ok())
.find(|m| m.path().is_ident(name))
.map(syn::NestedMeta::from)
}
. ,
— :
fn invalid_method(span: &impl syn::spanned::Spanned) -> darling::Error {
darling::Error::custom(
"API method should have one of `fn foo(&self) -> Result<Bar, Error>` or \
`fn foo(&self, arg: Foo) -> Result<Bar, Error>` form",
)
.with_span(span)
}
impl ParsedEndpoint {
fn parse(sig: &syn::Signature, attrs: &[syn::Attribute]) -> Result<Self, darling::Error> {
let mut args = sig.inputs.iter();
if let Some(arg) = args.next() {
match arg {
syn::FnArg::Receiver(syn::Receiver {
reference: Some(_),
mutability: None,
..
}) => {
}
_ => {
return Err(invalid_method(&arg));
}
}
} else {
return Err(invalid_method(&sig));
}
let arg = args
.next()
.map(|arg| match arg {
syn::FnArg::Typed(arg) => Ok(arg.ty.clone()),
_ => unreachable!("Only first argument can be receiver."),
})
.transpose()?;
let ret = match &sig.output {
syn::ReturnType::Type(_, ty) => Ok(ty.clone()),
_ => Err(invalid_method(&sig)),
}?;
let attrs = find_meta_attrs("http_api_endpoint", attrs)
.map(|meta| EndpointAttrs::from_nested_meta(&meta))
.unwrap_or_else(|| Err(darling::Error::custom("todo")))?;
Ok(Self {
ident: sig.ident.clone(),
arg,
ret,
attrs,
})
}
}
. , , .
, :
#[derive(Debug)]
struct ParsedApiDefinition {
item_trait: syn::ItemTrait,
endpoints: Vec<ParsedEndpoint>,
attrs: ApiAttrs,
}
#[derive(Debug, FromMeta)]
struct ApiAttrs {
warp: syn::Ident,
}
impl ParsedApiDefinition {
fn parse(
item_trait: syn::ItemTrait,
attrs: &[syn::NestedMeta],
) -> Result<Self, darling::Error> {
let endpoints = item_trait
.items
.iter()
.filter_map(|item| {
if let syn::TraitItem::Method(method) = item {
Some(method)
} else {
None
}
})
.map(|method| ParsedEndpoint::parse(&method.sig, method.attrs.as_ref()))
.collect::<Result<Vec<_>, darling::Error>>()?;
let attrs = ApiAttrs::from_list(attrs)?;
Ok(Self {
item_trait,
endpoints,
attrs,
})
}
}
, warp
.
, , , warp
. warp' , Filter
. and
, map
, and_then
, , .
, , GET JSON, - :
pub fn simple_get<F, R, E>(name: &'static str, handler: F) -> JsonReply
where
F: Fn() -> Result<R, E> + Clone + Send + Sync + 'static,
R: ser::Serialize,
E: Reject,
{
warp::get()
.and(warp::path(name))
.and_then(move || {
let handler = handler.clone();
async move {
match handler() {
Ok(value) => Ok(warp::reply::json(&value)),
Err(e) => Err(warp::reject::custom(e)),
}
}
})
.boxed()
}
GET , , :
pub fn query_get<F, Q, R, E>(name: &'static str, handler: F) -> JsonReply
where
F: Fn(Q) -> Result<R, E> + Clone + Send + Sync + 'static,
Q: FromUrlQuery,
R: ser::Serialize,
E: Reject,
{
warp::get()
.and(warp::path(name))
.and(warp::filters::query::raw())
.and_then(move |raw_query: String| {
let handler = handler.clone();
async move {
let query = Q::from_query_str(&raw_query)
.map_err(|_| warp::reject::custom(IncorrectQuery))?;
match handler(query) {
Ok(value) => Ok(warp::reply::json(&value)),
Err(e) => Err(warp::reject::custom(e)),
}
}
})
.boxed()
}
.
and
, ,
or
, , ,
, .
:
use std::net::SocketAddr;
use warp::Filter;
warp::path::param::<u32>()
.or(warp::path::param::<SocketAddr>());
serve_ping_interface
. warp , service , -.
impl ParsedEndpoint {
fn impl_endpoint_handler(&self) -> impl ToTokens {
let path = self.endpoint_path();
let ident = &self.ident;
match (&self.attrs.method, &self.arg) {
(SupportedHttpMethod::Get, None) => {
quote! {
let #ident = http_api::warp_backend::simple_get(#path, {
let out = service.clone();
move || out.#ident()
});
}
}
(SupportedHttpMethod::Get, Some(_arg)) => {
quote! {
let #ident = http_api::warp_backend::query_get(#path, {
let out = service.clone();
move |query| out.#ident(query)
});
}
}
(SupportedHttpMethod::Post, None) => {
quote! {
let #ident = http_api::warp_backend::simple_post(#path, {
let out = service.clone();
move || out.#ident()
});
}
}
(SupportedHttpMethod::Post, Some(_arg)) => {
quote! {
let #ident = http_api::warp_backend::params_post(#path, {
let out = service.clone();
move |params| out.#ident(params)
});
}
}
}
}
}
or
.
impl ToTokens for ParsedApiDefinition {
fn to_tokens(&self, out: &mut proc_macro2::TokenStream) {
let fn_name = &self.attrs.warp;
let interface = &self.item_trait.ident;
let (filters, idents): (Vec<_>, Vec<_>) = self
.endpoints
.iter()
.map(|endpoint| {
let ident = &endpoint.ident;
let handler = endpoint.impl_endpoint_handler();
(handler, ident)
})
.unzip();
let mut tail = idents.into_iter();
let head = tail.next().unwrap();
let serve_impl = quote! {
#head #( .or(#tail) )*
};
let tokens = quote! {
fn #fn_name<T>(
service: T,
addr: impl Into<std::net::SocketAddr>,
) -> impl std::future::Future<Output = ()>
where
T: #interface + Clone + Send + Sync + 'static,
{
use warp::Filter;
#( #filters )*
warp::serve(#serve_impl).run(addr.into())
}
};
out.extend(tokens)
}
}
, derive ,
.
, ,
RPC, , Rust'.
, - HTTP
reqwest
.
使用宏,没有人会花更长时间显示
接口类型的openapi或swagger
规范。但是在我看来,在这种情况下,最好按照相反的方法编写一个Rust代码生成器,这将为操作留出更多的空间。
如果以生成依赖关系的形式编写此生成器,则可以使用库
syn
和quote
,因此编写生成器将非常方便和简单。但是,这已经是深远的想法了:)
完整的工作代码(本文提供了示例)可以在此
链接上找到。
感谢您的关注!