derive
매크로를 사용 시 어떻게 파싱하고 TokenStream
으로 반환하여 코드를 만드는지 알아보겠다. 매번 사용은 하지만 정확하게 짚고 넘어가고 싶다.
attributes
에는 종류가 크게 3가지 있다.
1. Path
2. List
3. NameValue
테스트를 하기위해서 필드 어트리뷰트는 제외하고 했다.
fn print_attribute_parsing(attrs: &Vec<Attribute>) -> TokenStream {
attrs.iter().for_each(|attr| {
match &attr.meta {
syn::Meta::Path(path) => {
let Some(ident) = path.get_ident() else {
panic!("fail to parse ident");
};
println!("path attribute name: {}", ident.to_string());
},
syn::Meta::List(meta_list) => {
let Some(ident) = meta_list.path.get_ident() else {
panic!("fail to parse ident");
};
println!("list path attribute name: {}", ident.to_string());
},
syn::Meta::NameValue(meta_name_value) => {
let Some(ident) = meta_name_value.path.get_ident() else {
panic!("fail to parse ident");
};
println!("namevalue path attribute name: {}", ident.to_string());
},
}
});
quote! {
}.into()
}
attribute
를 파싱하여 화면에 출력하는 코드를 만들었다.
#[derive(Test)]
#[a]
#[b(1,2,3)]
#[c = "test"]
struct TestStruct {
name: String,
age: u8,
}
trait Test {}
위처럼 테스트할 attribute를 달아준 뒤 실행을 했다.
각 속성 타입에 맞게 잘 나온걸 볼 수 있다. Path
이 경우 간단히 파싱하고 끝이 나지만 List
, NameValue
의 경우 추가적으로 파싱을 해주어야한다.
List
부터 파싱 해보도록 하겠다.
#[validate(range(min = 18, max = 100))]
위처럼 범위 속성을 부여하여 유효성 검사를 할 수 있다.
사용 방법은 아주 다양한 것 같다.
#[derive(Test)]
#[b(name = "kim", test)]
struct TestStruct {
name: String,
age: u8,
}
syn::Meta::List(meta_list) => {
match meta_list.parse_nested_meta(|nested_meta| {
if nested_meta.path.is_ident("name") {
println!("nested data name");
} else if nested_meta.path.is_ident("test") {
println!("nested data test");
} else {
println!("nested data nothing");
return Err(nested_meta.error("fail"));
}
Ok(())
}) {
Ok(_) => {},
Err(e) => println!("e: {}", e),
}
// parsing_meta_list(meta_list);
},
위 코드로 파싱을 하면 출력값이 아래와 같이 출력된다.
name
은 잘 읽었지만 test
를 읽는 과정에서 실패한 것이다. 이유는 name
이 단순 path로 예상하여 ,
가 올 것으로 예상한 것이다.
let lit_str: syn::LitStr = nested_meta.value()?.parse()?;
println!("nested data name: {}", lit_str.value());
위 코드를 추가하여 Token을 소비할 때, NameValue
인 것을 인지 시켜줘야 한다. 그러면 출력 값이 아래처럼 정상으로 나온다.
Token! 매크로는 특정 기호나 키워드를 Rust 타입으로 변환 해주는 마법 같은 도구이다. 이를 통해 syn을 사용한 구문 분석 코드를 더 쉽고 효율적으로 작성할 수 있다.
use syn::token::Comma
Token![,]
,
라는 키워드를 rust에서 syn::token::Comma
로 인식을 하는데 개발자가 일일이 알 필요가 없다. Token
매크로를 활용해서 원하는 기호나 키워드를 넣어주면 자동으로 타입을 반환해 주는 매크로이다. 가독성과 효율성을 높일 수 있다고 이해하면 될 것 같다.
먼저 attributes에 대해서 알아보도록 하겠다. struct나 enum에 붙일 수 있고 각 필드들에도 붙일 수 있다. 해당 attribute를 통해서 매크로 내에서 원하는 코드를 생성해 줄 수 있다고 이해하면 된다.
다음에 추가로 NameValue
속성에 대해서 알아보겠다.
TokenStream
으로 받아서 DeriveInput
으로 파싱 하여 다양하게 코드를 생성하는 것이 흥미롭다. 예전에 ai의 도움을 받아 proc_macro를 작성해 봤지만, 정확히 어떻게 파싱이 되고 Token들이 사용이 되는지 확인할 수 있었다.