vvvvy

帆事豫則立


  • 首頁
  • 歸檔
  • 分類
  • 標籤
  •    

© 2025 Yvictor

Theme Typography by Makito

Proudly published with Hexo

一天內發佈一個解析舊有金融系統網路二進位資料的 Rust Crate

發佈於 2024-12-06 code rust 

在本教學中,我們將使用 Rust 建立並發佈一個 crate,用於解析舊有金融系統的二進位網路資料。我們會採用測試驅動開發(TDD)方法:先撰寫測試,再實作功能。每個小步驟都有測試作為品管,確保正確性。我們也會討論如何利用 Cursor 編輯器 與 Claude 這類大型語言模型(LLM)來加速開發。有了 TDD 作為安全防護,您可以自信地使用 AI 建議來快速迭代開發。

完整程式碼存放於: https://github.com/Yvictor/binary_mirror

為什麼使用 TDD 與 AI 協助?

使用 TDD 時,每個新功能的開發流程是:先寫一個預期失敗的測試(定義需求),再寫出剛好能通過該測試的程式碼,避免過度設計。同時搭配 Cursor 編輯器與 Claude AI,可快速產生程式碼片段與建議。TDD 確保即使 AI 給出不完全正確的建議,我們也能透過測試來驗證、修正,最終得到符合預期的結果。

第 1 步:初始化專案並撰寫首個測試

概念: 從建立一個 library crate 開始,並在實作任何功能前先撰寫一個會失敗的測試。例如,我們期望能有一個 BinaryMirror 派生巨集以及解析 str 欄位的功能,但尚未實作。

指令:

cargo new binary-mirror --lib
cd binary-mirror
mkdir tests
touch tests/derive_tests.rs

初始測試(tests/derive_tests.rs):

use binary_mirror::BinaryMirror;

#[repr(C)]
#[derive(Debug, BinaryMirror)]
struct TestStruct {
    #[bm(type = "str")]
    name: [u8; 10],
}

#[test]
fn test_str_field() {
    let record = TestStruct {
        name: *b"Hello    ",
    };
    assert_eq!(record.name(), "Hello");
}

現在執行 cargo test 會失敗,因為還沒有實作 BinaryMirror。這個測試明確告訴我們下一步該做什麼:實作 str 字串解析。

第 2 步:實作最基本的 str 欄位解析

透過 TDD:測試告訴我們需要解析 str 欄位。您可在 Cursor 中詢問 AI 如何建立基本的 proc macro,再根據測試結果進行微調。

加入必要套件:

cargo add syn --features full,extra-traits
cargo add quote
cargo add proc-macro2

src/lib.rs 實作:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};

#[proc_macro_derive(BinaryMirror, attributes(bm))]
pub fn binary_mirror_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;
    let fields = match input.data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => &fields.named,
            _ => panic!("只支援有命名欄位的結構"),
        },
        _ => panic!("只支援結構"),
    };

    let mut methods = Vec::new();
    for f in fields {
        let field_name = f.ident.as_ref().unwrap();
        let bm_type = f.attrs.iter().find_map(|attr| {
            let tokens = attr.tokens.to_string();
            if tokens.contains("\"str\"") {
                Some("str".to_string())
            } else {
                None
            }
        });

        if let Some("str") = bm_type.as_deref() {
            methods.push(quote! {
                pub fn #field_name(&self) -> String {
                    String::from_utf8_lossy(&self.#field_name).trim().to_string()
                }
            });
        }
    }

    let expanded = quote! {
        impl #name {
            #(#methods)*
        }
    };

    expanded.into()
}

再次測試:

cargo test

現在測試會通過。由於有 TDD 驗證,我們可確定 AI 協助的程式碼符合需求。

第 3 步:為整數欄位(i32)撰寫測試,再實作

接著我們需要解析數字欄位。在實作前先寫測試:

#[repr(C)]
#[derive(Debug, BinaryMirror)]
struct NumericStruct {
    #[bm(type = "i32")]
    value: [u8; 4],
}

#[test]
fn test_i32_field() {
    let rec = NumericStruct {
        value: *b"123 ",
    };
    assert_eq!(rec.value(), Some(123));
}

cargo test 現在失敗了,我們必須實作 i32 解析。

第 4 步:實作 i32 解析

更新 src/lib.rs 程式碼,加入 i32 支援:

if let Some("i32") = bm_type.as_deref() {
    methods.push(quote! {
        pub fn #field_name(&self) -> Option<i32> {
            String::from_utf8_lossy(&self.#field_name).trim().parse::<i32>().ok()
        }
    });
}

再次 cargo test,現在測試通過。

第 5 步:為別名(alias)功能撰寫測試,再實作

先寫測試,預期有 alias 能將方法命名改為 exchange():

#[repr(C)]
#[derive(Debug, BinaryMirror)]
struct AliasStruct {
    #[bm(type = "str", alias = "exchange")]
    exh: [u8; 10],
}

#[test]
fn test_alias_field() {
    let rec = AliasStruct {
        exh: *b"CME       ",
    };
    assert_eq!(rec.exchange(), "CME");
}

cargo test 現在失敗了,我們必須實作 alias 功能。
告訴 cursor 我們需要實作 alias 功能,直到 cargo test 通過。

第 6 步:為日期與時間解析撰寫測試,再實作

加上 chrono 套件:

cargo add chrono

寫測試解析日期與時間:

use chrono::{NaiveDate, NaiveTime};

#[repr(C)]
#[derive(Debug, BinaryMirror)]
struct DateTimeStruct {
    #[bm(type = "date", format = "%Y%m%d")]
    date_field: [u8; 8],
    #[bm(type = "time", format = "%H%M%S")]
    time_field: [u8; 6],
}

#[test]
fn test_date_time_fields() {
    let rec = DateTimeStruct {
        date_field: *b"20240101",
        time_field: *b"123456",
    };
    assert_eq!(rec.date_field(), Some(NaiveDate::from_ymd_opt(2024,1,1).unwrap()));
    assert_eq!(rec.time_field(), Some(NaiveTime::from_hms_opt(12,34,56).unwrap()));
}

跑測試失敗後,再讓 AI 幫你實作 date/time 格式解析,透過 TDD 確保功能正確。

第 7 步:為合併日期與時間(datetime_with)寫測試,再實作

撰寫測試期望有 datetime_with 能將日期與時間合併成 NaiveDateTime。測試失敗後實作此功能,再測試應可通過。

第 8 步:為錯誤資料寫最後的測試

測試無效資料能安全返回 None。通過後即可確信穩健性

第 9 步:發佈 Crate

所有測試通過且功能完整,準備發佈:

cargo login
cargo publish --dry-run
cargo publish

您的 crate 已經上架到 crates.io,其他人可用 cargo add binary_mirror 來使用。

結論

透過 TDD 與 AI 協助,我們在一天內成功發佈了 BinaryMirror crate。這不僅展示了 Rust 的強大功能,也展示了如何利用 AI 工具來加速開發過程。希望這個範例能激勵您在開發過程中探索更多可能性。

完整程式碼存放於: https://github.com/Yvictor/binary_mirror

分享到 

 上一篇: rsolace 的進化:一次關於 Rust FFI、異步與記憶體安全的深度實踐 下一篇: 擁抱未來:為什麼你應該從 Pandas 轉換到 Polars 

© 2025 Yvictor

Theme Typography by Makito

Proudly published with Hexo