JavaのMapStructをRustで実装する
Olivia Novak
Dev Intern · Leapcell

title: "Java's MapStruct Implemented in Rust" summary: "Rust macro-based alternative to Java's MapStruct for automatic struct conversion."
Javaのエコシステムには、MapStructと呼ばれるBean変換ツールがあり、Bean間の変換を非常に便利にします。その原理は、コンパイル時に変換メソッドを生成することです。Rustのマクロもコンパイル時のコード生成をサポートしているため、属性マクロを使用してMapStructの簡単なバージョンを実装することにしました。
Rustのマクロの基本
Rustのマクロは、主に宣言マクロ(macro_rules!
)と、次の3種類のプロシージャルマクロに分けられます。
- Deriveマクロ:これらは、
Debug
トレイトなど、ターゲットのstructまたはenumに対して特定のコードを導出するために一般的に使用されます。 - 属性のようなマクロ:これらは、ターゲットにカスタム属性を追加するために使用されます。
- 関数のようなマクロ:これらは関数呼び出しに似ています。
実装原理の分析
RustでBean間の変換を行う場合、非常に簡単です。From
トレイトを実装し、from
メソッド内で変換ロジックを定義できます。
pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } impl From<Person> for PersonDto { fn from(item: Person) -> PersonDto { PersonDto { name: item.name, age: item.age, } } } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // Use the auto-generated From implementation for conversion println!("dto: name:{}, age:{}", dto.name, dto.age); }
したがって、これをマクロを使用してRustで実装するには、マクロにFrom
メソッドを自動的に生成させ、自動変換を有効にする必要があります。
使いやすくするために、Dieselの#[diesel(table_name = blog_users)]
のような構文からヒントを得ました。私たちのマクロは、structの上に#[auto_map(target = "PersonDto")]
を追加するだけで使用できます。非常にクリーンでエレガントです。
#[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, }
コードの実装
マクロの使用法は#[auto_map(target = "PersonDto")]
であるため、マクロのワークフローはほぼ固定されています。Person
とPersonDto
を例にとると、プロセスは次のようになります。
- マクロから
"target"
パラメーターを抽出します。 - 入力struct(
Person
)を解析します。 - 入力structからフィールド名と型を抽出します。
- ターゲット型を解析します。
- 元のstructを再生成し、
From
メソッドを実装します。
ステップ1:プロジェクトを作成し、依存関係を追加します
cargo new rust_mapstruct --lib cd rust_mapstruct
マクロのコード生成にはRustのASTの解析が必要なため、quote
とsyn
という2つのキーライブラリが必要です。また、マクロを作成するため、proc-macro = true
を指定する必要があります。
依存関係の完全なリスト:
[lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0.17", features = ["full"] }
ステップ2:lib.rs
のコアコードを変更します
1. コア機能を定義します
#[proc_macro_attribute] pub fn auto_map(args: TokenStream, input: TokenStream) -> TokenStream { }
2. "target"パラメーターを抽出して解析します
これは複数のパラメーターをサポートするように拡張できますが、MapStructのようなツールに必要なパラメーターは1つだけなので、target
文字列を直接照合します。後でこれを拡張して、さらにパラメーターを追加できます。
let args = parse_macro_input!(args as AttributeArgs); // Extract and parse the "target" parameter let target_type = args .iter() .find_map(|arg| { if let NestedMeta::Meta(Meta::NameValue(m)) = arg { if m.path.is_ident("target") { if let Lit::Str(lit) = &m.lit { return Some(lit.value()); } } } None }) .expect("auto_map requires a 'target' argument");
3. 入力struct(Person
)を解析します
// Parse the input struct let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; let struct_data = match input.data { Data::Struct(data) => data, _ => panic!("auto_map only supports structs"), };
4. Person
からフィールド名と型を抽出します
let (field_names, field_mappings): (Vec<_>, Vec<_>) = struct_data.fields.iter().map(|f| { let field_name = f.ident.as_ref().unwrap(); let field_type = &f.ty; (field_name.clone(), quote! { #field_name: #field_type }) }).unzip();
5. ターゲット型(PersonDto
)を解析します
syn::parse_str
は、文字列をRustの型に変換できます。
// Parse the target type let target_type_tokens = syn::parse_str::<syn::Type>(&target_type).unwrap();
6. 元のstructとFrom
の実装を生成します
quote
内のコードは、単純なテンプレートエンジンとして機能します。Webページのテンプレートを作成したことがある場合は、これによく似ているはずです。最初の部分は元のPerson
structを再生成し、2番目の部分はFrom
メソッドを生成します。解析されたパラメーターをテンプレートにプラグインするだけです。
// Regenerate original struct and conversion implementation let expanded = quote! { // Note: this generates the original struct `Person` pub struct #struct_name { #( #field_mappings, )* } impl From<#struct_name> for #target_type_tokens { fn from(item: #struct_name) -> #target_type_tokens { #target_type_tokens { #( #field_names: item.#field_names, )* } } } }; expanded.into()
ステップ3:プロジェクトでマクロをテストします
まず、cargo build
でマクロプロジェクトをコンパイルします。次に、新しいテストプロジェクトを作成します。
cargo new test-mapstruct cd test-mapstruct
Cargo.toml
の依存関係を変更します
[dependencies] rust_mapstruct = { path = "../rust_mapstruct" }
main.rs
に簡単なテストを記述します
use rust_mapstruct::auto_map; #[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // Use the auto-generated From implementation for conversion println!("dto: name:{}, age:{}", dto.name, dto.age); }
コードを実行して結果を確認します
test-mapstruct
プロジェクトで、cargo build
、cargo run
を実行し、結果を確認してください!
❯ cargo build Compiling test-mapstruct v0.1.0 (/home/maocg/study/test-mapstruct) Finished dev [unoptimized + debuginfo] target(s) in 0.26s test-mapstruct on master ❯ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/test-mapstruct` dto: name:Alice, age:30
私たちはLeapcellです。Rustプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストも料金も追加料金はありません。
比類のないコスト効率
- アイドル状態の料金なしで従量課金制。
- 例:25ドルで、平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI / CDパイプラインとGitOpsの統合。
- 実用的な洞察のためのリアルタイムの指標とロギング。
簡単なスケーラビリティと高性能
- 高い並行処理を簡単に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください:@LeapcellHQ