وحدات الماكرو الإجرائية في Rust هي أداة قوية جدًا لتوليد الأكواد تسمح لك بالاستغناء عن كتابة الكثير من التعليمات البرمجية المتداخلة ، أو التعبير عن بعض المفاهيم الجديدة ، كما فعل مطورو الصندوق ، على سبيل المثال async_trait
.
ومع ذلك ، يخشى العديد من الأشخاص مبررًا استخدام هذه الأداة ، ويرجع ذلك أساسًا إلى حقيقة أن تحليل شجرة البنية والسمات الكلية غالبًا ما يتحول إلى "غروب الشمس يدويًا" ، حيث يجب حل المهمة على مستوى منخفض جدًا.
في هذه المقالة ، أود أن أشارك بعض الطرق الناجحة لكتابة وحدات الماكرو الإجرائية ، وأثبت أنه يمكن اليوم إنشاء وحدات الماكرو الإجرائية بشكل بسيط ومريح نسبيًا.
مقدمة
بادئ ذي بدء ، دعنا نحدد المشكلة التي سنحلها بمساعدة وحدات الماكرو: سنحاول تحديد بعض واجهات برمجة تطبيقات RPC المجردة في شكل سمة ، والتي تنفذ بعد ذلك كل من جزء الخادم وجزء العميل ؛ وستساعدنا وحدات الماكرو الإجرائية ، بدورها ، على الاستغناء عن مجموعة من التعليمات البرمجية المعيارية. على الرغم من حقيقة أننا سنقوم بتطبيق واجهة برمجة تطبيقات مجردة إلى حد ما ، فإن المهمة في الواقع حيوية للغاية ، ومن بين أمور أخرى ، مثالية لإثبات قدرات وحدات الماكرو الإجرائية.
سيتم تنفيذ واجهة برمجة التطبيقات نفسها وفقًا لمبدأ بسيط للغاية: هناك 4 أنواع من الطلبات:
- طلبات GET مع أية معلمات، على سبيل المثال:
/ping
. - طلبات GET مع المعلمات، وسيتم نقل المعلمات منها في شكل استعلام URL، على سبيل المثال:
/status?name=foo&count=15
. - طلبات POST بدون معلمات.
- طلبات POST مع المعلمات التي يتم تمريرها ككائنات JSON.
في جميع الحالات ، سيرد الخادم بكائن JSON صالح.
كخلفية للخادم ، سنستخدم الصندوق 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
، وبالتالي، كتابة مولد سوف تكون مريحة جدا وبسيطة. ومع ذلك ، هذه بالفعل أفكار بعيدة المدى :)
كود العمل الكامل ، والأمثلة التي تم تقديمها في هذه المقالة ، يمكن العثور عليها في هذا
الرابط .
شكرا للانتباه!