[Rust] derive 매크로 만들어보기 : JSON 직렬화
러스트는 갖가지 매크로를 통해 강력한 확장 기능을 제공한다.
특히 derive 매크로는 구조체나 enum 등 타입에 대한 재귀적인 특성 구현을 만들기 용이하다.
다른 언어들에서 런타임에 하는 것들을 컴파일타임에 처리해버릴 수 있는 것이다.
derive로 컴파일타임에 Json 변환 코드를 구현하는 법을 간단히 다뤄보겠다.
우선 crate는 2개 파서 연결해야한다. proc macro가 일반적인 코드 crate에서는 사용할 수 없다는 제약 때문이다.
proc macro는 따로 전용 crate를 만들어서 워크스페이스나 뭐 그런걸로 엮으면 된다.
derive 매크로는 일단 뭐 이런 식으로 만들 수 있다.
저렇게 하면 Json이라는 이름의 derive 매크로가 생기는거고
이렇게 쓸 수 있다.
컴파일은 잘 되지만 아무것도 동작하지 않는다.
이번에는 코드 파서인 syn을 갖다가 필드 정보들을 파싱해보겠다.
기본 TokenStream으로도 할 수 있긴 한데, 번거로워서 이렇게 하는게 낫다.
그럼 정보 자체는 잘 찍히는걸 확인할 수 있을 것이다.
이제부터 시작이다.
trait 변환에 대한 트레잇을 만든다.
이걸 기본 타입들에 대해 정의하고, derive에서 그걸 가져다가 구조체에 대한 impl 코드를 자동 생성하는 형태로 할 것이다.
일단 기본 타입들에 대해 적절한 구현을 미리 만들어준다.
예시라서 딱 2개만 넣었는데, 기본타입들 전부와 Option
그리고 이제 구조체에 구현을 만들어주면 된다.
"{}" 형태로 껍데기 만들고
내부에서 필드 돌면서 to_json을 호출, 값을 겹겹이 쌓는다.
전체코드
그리고 구조체에 대해 변환을 시도해보면
기대한대로 동작할 것이다.
attribute 구현
근데 변환에 대해서 필드마다 변주를 주고 싶을 수도 있다.
Json 변환 대상에서 제외하기 위한 용도의 ignore_json을 만들어보겠다.

그리고 이렇게 속성을 걸면 된다.
파싱은 이런 느낌으로 할 수 있다. 어차피 ast 추출에서 다 뽑아준다.
저거 들어가면 변환을 하지 않게 했다.


그럼 잘 무시될 것이다.
이런 느낌으로 쓰면 된다.
아래는 예제코드 전체다.
이건 매크로
#[proc_macro_derive(ToJson, attributes(ignore_json))]
pub fn derive_to_json(item: TokenStream) -> TokenStream {
let mut new_code = "".to_string();
let ast = syn::parse_macro_input!(item as syn::ItemStruct);
let struct_name = ast.ident.to_string();
new_code += format!(r#"impl ToJsonTrait for {struct_name} {{"#).as_str();
new_code += r#"fn to_json(&self) -> String {"#;
new_code += r#"let mut json_string = "{".to_string();"#;
let mut need_comma = false;
for field in ast.fields.iter() {
let field_name = field.ident.as_ref().unwrap().to_string();
let mut is_ignore = false;
let attributes = field.attrs.clone();
for attribute in attributes {
let metadata = attribute.meta;
let path = metadata.path().to_token_stream().to_string();
//let meta_value = metadata.require_name_value().ok().map(|e| e.value.clone());
match path.to_lowercase().as_str() {
"ignore_json" => {
is_ignore = true;
}
_ => {}
}
}
if is_ignore {
continue;
}
if need_comma {
new_code += r#"json_string += ",";"#;
}
new_code += format!(r#"json_string += "\"{field_name}\":";"#).as_str();
new_code += format!(r#"json_string += self.{field_name}.to_json().as_str();"#).as_str();
if !need_comma {
need_comma = true;
}
}
new_code += r#"json_string += "}";"#;
new_code += r#"json_string"#;
new_code += r#"}"#;
new_code += "}";
return TokenStream::from_str(new_code.as_str()).unwrap();
}
이건 non-매크로
#[derive(ToJson)]
pub struct JsonTest {
#[ignore_json]
pub name: String,
pub age: i32,
}
pub trait ToJsonTrait {
fn to_json(&self) -> String;
}
impl ToJsonTrait for i32 {
fn to_json(&self) -> String {
self.to_string()
}
}
impl ToJsonTrait for String {
fn to_json(&self) -> String {
format!(r#""{}""#, self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_json() {
let json = JsonTest {
name: "John".to_string(),
age: 20,
}
.to_json();
assert_eq!(json, r#"{"age":20}"#);
}
}