afastdata 0.0.9

A high-performance binary serialization/deserialization library with derive macros for Rust types
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# afastdata

[![CI](https://github.com/ahriknow/afastdata/actions/workflows/publish.yml/badge.svg)](https://github.com/ahriknow/afastdata/actions/workflows/publish.yml)
[![Crates.io](https://img.shields.io/crates/v/afastdata.svg)](https://crates.io/crates/afastdata)
[![docs.rs](https://docs.rs/afastdata/badge.svg)](https://docs.rs/afastdata)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)

English | **[中文]README_CN.md**

A high-performance Rust binary serialization/deserialization framework that automatically generates serialization code for custom types via derive macros.

## Features

- **Zero-config derive macros**`#[derive(AFastSerialize, AFastDeserialize)]` in one line
- **Rich type support** — Primitives, `String`, `Vec<T>`, `Option<T>`, `[T; N]`, `Box<T>`, tuples, `HashMap`, `HashSet`, `BTreeMap`, `BTreeSet`, nested structs, enums
- **Generic support** — Automatically adds trait bounds for generic parameters
- **Configurable length prefix** — Default `u32` (max 4GB), switchable to `u64` via feature flag
- **Configurable enum tags** — Default `u8`, switchable to `u16` or `u32` via feature flags
- **Configurable tuple support** — Default max 16 elements, switchable to 8 or 32 via feature flags
- **Uniform little-endian** — All multi-byte data uses little-endian encoding
- **Zero runtime dependencies** — No third-party dependencies at runtime

## Quick Start

### Installation

Add the dependency to your `Cargo.toml`:

```toml
[dependencies]
afastdata = "0.0.9"
```

For `u64` length prefix or custom enum tag type:

```toml
[dependencies]
afastdata = { version = "0.0.9", features = ["len-u64", "tag-u16"] }
```

### Basic Usage

```rust
use afastdata::{AFastSerialize, AFastDeserialize};

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct User {
    name: String,
    age: u32,
    email: Option<String>,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        age: 30,
        email: Some(String::from("alice@example.com")),
    };

    // Serialize
    let bytes = user.to_bytes();

    // Deserialize
    let (decoded, consumed) = User::from_bytes(&bytes).unwrap();
    assert_eq!(user, decoded);
    println!("Consumed {} bytes", consumed);
}
```

### Enum Example

```rust
use afastdata::{AFastSerialize, AFastDeserialize};

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
enum Command {
    Ping,
    Send { to: String, message: String },
    Broadcast(Vec<String>),
}

fn main() {
    let cmd = Command::Send {
        to: String::from("Bob"),
        message: String::from("Hello!"),
    };

    let bytes = cmd.to_bytes();
    let (decoded, _) = Command::from_bytes(&bytes).unwrap();
    assert_eq!(cmd, decoded);
}
```

### Generic Struct

```rust
use afastdata::{AFastSerialize, AFastDeserialize};

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Response<T> {
    code: u32,
    data: Option<T>,
    message: String,
}

fn main() {
    let resp = Response {
        code: 200,
        data: Some(vec![1i32, 2, 3]),
        message: String::from("ok"),
    };

    let bytes = resp.to_bytes();
    let (decoded, _) = Response::<Vec<i32>>::from_bytes(&bytes).unwrap();
    assert_eq!(resp, decoded);
}
```

## Validation

You can add validation rules to struct fields and enum variant fields using the `#[afast(...)]` attribute.

### Struct Validation Example

```rust
use afastdata::{AFastDeserialize, AFastSerialize, ValidateError};

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct A {
    #[afast(
        gt(10, 0, "${field} must be greater than 10"),
        lte(100, 0, "${field} must be less than or equal to 100")
    )]
    a: i64,

    #[afast(len(
        10,
        100,
        1,
        "${field} length must be between 10 and 100"
    ))]
    b: Option<String>,

    #[afast(func("v"))] // call v function to validate, parameter is `value` and `field name`
    c: i32,

    #[afast(skip("d"))] // call d function, #[afast(skip)] will call i64::default()
    e: i64,
}

fn v(value: &i32, field: &str) -> Result<(), ValidateError> {
    if *value % 2 == 0 {
        Ok(())
    } else {
        Err(ValidateError::new(
            2,
            format!("{} must be an even number, but got {}", field, value),
        ))
    }
}

fn d() -> i64 {
    123
}
```

### Enum Variant Validation Example

Validation rules also work on named-field and tuple enum variants:

```rust
use afastdata::{AFastDeserialize, AFastSerialize, ErrorKind};

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
enum Command {
    // Named-field variant
    Login {
        #[afast(len(1, 32, 1001, "username ${field} length must be 1-32"))]
        username: String,
        #[afast(len(6, 128, 1002, "password ${field} length must be 6-128"))]
        password: String,
    },
    // Tuple variant
    Send(#[afast(gte(0, 2001, "value must be >= 0"))] i64),
}

fn main() {
    // Validation passes
    let cmd = Command::Login {
        username: String::from("alice"),
        password: String::from("secret123"),
    };
    let bytes = cmd.to_bytes();
    assert!(Command::from_bytes(&bytes).is_ok());

    // Validation fails: empty username
    let cmd = Command::Login {
        username: String::new(),
        password: String::from("secret123"),
    };
    let bytes = cmd.to_bytes();
    let err = Command::from_bytes(&bytes).unwrap_err();
    assert!(matches!(err.kind(), ErrorKind::ValidateError(1001, _)));
}
```

### Validate Attribute

Validation rules apply to both struct fields and enum variant fields (named and tuple variants):

- `skip` or `skip("default_fn")` — Always skip this field during serialization and deserialization. On deserialize, fill with default value (call the given function, or `Default::default()`)
- `skip_with("marker")` or `skip_with("marker", "default_fn")` — Conditionally skip. When the marker passed to `to_bytes_with` / `from_bytes_with` matches this field's marker, the field is skipped. Otherwise it is serialized/deserialized normally. The marker automatically propagates to nested types — if a struct field contains another struct with `skip_with` fields, those inner fields also respond to the same marker
- `gt(value, code, message)`: field value must be greater than `value` (supports integer and float literals), otherwise return `ValidateError`. Applicable to numeric types and `Option<numeric>` (`None` passes validation)
- `gte(value, code, message)`: field value must be greater than or equal to `value`, otherwise return `ValidateError`. Applicable to numeric types and `Option<numeric>`
- `lt(value, code, message)`: field value must be less than `value`, otherwise return `ValidateError`. Applicable to numeric types and `Option<numeric>`
- `lte(value, code, message)`: field value must be less than or equal to `value`, otherwise return `ValidateError`. Applicable to numeric types and `Option<numeric>`
- `len(min, max, code, message)`: field length must be between `min` and `max`, applicable to `String`, `Vec<T>`, `[T; N]`, or `Option<T>` wrapping these types (`None` passes validation). Pass `-1` for `min` or `max` to leave that bound unrestricted, e.g. `len(3, -1, ...)` means min length 3 with no max limit
- `of([v1, v2, ...], code, message)`: field value must be one of `[v1, v2, ...]`, otherwise return `ValidateError`
- `func(name)` — Call external function for validation. Signature: `fn(value: &T, field: &str) -> Result<(), ValidateError>`

> **Type checking**: Validation rules check field type compatibility at compile time. For example, `gt`/`gte`/`lt`/`lte` only work on numeric types or `Option<numeric>`, and `len` only works on string and collection types. Incompatible combinations produce a compile error.

## Conditional Serialization (`skip_with`)

`skip_with` enables conditional field skipping based on a marker string, useful when the same type needs different serialization strategies in different contexts:

```rust
use afastdata::{AFastSerialize, AFastDeserialize};

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Packet {
    header: u32,
    #[afast(skip_with("cache"))]
    payload: Vec<u8>,
    #[afast(skip_with("cache", "default_checksum"))]
    checksum: u64,
}

fn default_checksum() -> u64 { 0 }

fn main() {
    let pkt = Packet {
        header: 1,
        payload: vec![1, 2, 3],
        checksum: 0xABCD,
    };

    // Full serialization (all fields included)
    let full = pkt.to_bytes();

    // Conditional serialization (skip fields with marker="cache")
    let cached = pkt.to_bytes_with("cache");
    // `cached` only contains the header — shorter than `full`

    // Conditional deserialization
    let (decoded, _) = Packet::from_bytes_with(&cached, "cache").unwrap();
    assert_eq!(decoded.header, 1);
    assert_eq!(decoded.payload, Vec::new());     // Default::default()
    assert_eq!(decoded.checksum, 0);             // default_checksum()
}
```

`to_bytes_with(marker)` and `from_bytes_with(data, marker)` accept a marker string:

- **Marker matches** the field's `skip_with` tag → serialize skips the field; deserialize fills with default value or custom function
- **Marker does not match** → behavior is identical to `to_bytes()` / `from_bytes()`
- **Nested propagation** — when serializing/deserializing a struct with `_with` methods, the marker automatically propagates to all nested types. If an inner struct also has `skip_with` fields, they will respond to the same marker
- For primitive types (`i32`, `String`, `Vec<T>`, etc.), the `_with` methods default to calling the regular methods

### Nested Type Example

`skip_with` works automatically across nested structs — no special configuration needed:

```rust
use afastdata::{AFastSerialize, AFastDeserialize};

#[derive(AFastSerialize, AFastDeserialize, Debug, Default, PartialEq)]
struct Inner {
    id: u32,
    #[afast(skip_with("cache"))]
    data: Vec<u8>,
    name: String,
}

#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Outer {
    header: u32,
    inner: Inner,
    footer: u32,
}

fn main() {
    let outer = Outer {
        header: 1,
        inner: Inner {
            id: 42,
            data: vec![1, 2, 3, 4, 5],
            name: String::from("test"),
        },
        footer: 99,
    };

    // Full serialization — includes inner.data
    let full = outer.to_bytes();

    // Conditional — inner.data is skipped via marker propagation
    let cached = outer.to_bytes_with("cache");
    assert!(cached.len() < full.len());

    // Deserialize — inner.data is filled with Default::default()
    let (decoded, _) = Outer::from_bytes_with(&cached, "cache").unwrap();
    assert_eq!(decoded.inner.id, 42);
    assert_eq!(decoded.inner.data, Vec::new());
    assert_eq!(decoded.inner.name, String::from("test"));
}
```

## Field Name Safety

All internal variables in generated code are prefixed with `__afast_` to avoid conflicts with your field names. You can safely use field names like `data`, `offset`, or `bytes` without any issues:

```rust
#[derive(AFastSerialize, AFastDeserialize, Debug, PartialEq)]
struct Record {
    data: Vec<u8>,    // No conflict with internal `data` parameter
    offset: u64,      // No conflict with internal `offset` variable
    bytes: String,    // No conflict with internal `bytes` variable
}
```

## Supported Types

| Type | Serialization | Size |
|---|---|---|
| `i8`, `u8` | little-endian | 1 byte |
| `i16`, `u16` | little-endian | 2 bytes |
| `i32`, `u32` | little-endian | 4 bytes |
| `i64`, `u64` | little-endian | 8 bytes |
| `usize` | u64 little-endian (cross-platform) | 8 bytes |
| `i128`, `u128` | little-endian | 16 bytes |
| `f32` | IEEE 754 little-endian | 4 bytes |
| `f64` | IEEE 754 little-endian | 8 bytes |
| `bool` | `0x00`=false, `0x01`=true | 1 byte |
| `String` | LenInt prefix + UTF-8 bytes | Variable |
| `&str` | LenInt prefix + UTF-8 bytes (serialize only) | Variable |
| `Vec<T>` | LenInt element count + element-wise encoding | Variable |
| `Option<T>` | 1-byte tag + data (only when Some) | Variable |
| `[T; N]` | Element-wise encoding, no length prefix | Fixed |
| `(A, B, ...)` | Element-wise encoding, no length prefix | Fixed/Variable |
| `Box<T>` | Same as `T` | Same as `T` |
| `HashMap<K, V>` | LenInt entry count + key-value pair encoding | Variable |
| `HashSet<T>` | LenInt element count + element-wise encoding | Variable |
| `BTreeMap<K, V>` | LenInt entry count + key-value pair encoding | Variable |
| `BTreeSet<T>` | LenInt element count + element-wise encoding | Variable |
| Struct | Field-by-field encoding, no extra prefix | Variable |
| Enum | Tag(u8/u16/u32) variant index + variant field data | Variable |

## Encoding Format

### Struct

All fields are serialized in declaration order with no additional prefix:

```
[field1 bytes][field2 bytes][field3 bytes]...
```

### Enum

Writes a variant index (Tag type, default `u8`, switchable to `u16` or `u32` via feature flags, starting from 0, incrementing by declaration order), followed by the variant's field data:

```
[Tag variant_index][field1 bytes][field2 bytes]...
```

Unit variants only write the index, with no field data.

### Length Prefix

Variable-length types like `String` and `Vec<T>` use `LenInt` as the length prefix:

- Default: `u32` little-endian (4 bytes, max ~4GB)
- With `len-u64` feature: `u64` little-endian (8 bytes)

## Feature Flags

| Feature | Description | Default |
|---|---|---|
| `len-u64` | Switch the length prefix from `u32` to `u64` | No |
| `tag-u8` | Enum variant tag uses `u8` (1 byte, max 256 variants) | Yes |
| `tag-u16` | Enum variant tag uses `u16` (2 bytes, max 65536 variants) | No |
| `tag-u32` | Enum variant tag uses `u32` (4 bytes, max ~4.2 billion variants) | No |
| `tuple-8` | Tuple support up to 8 elements | No |
| `tuple-16` | Tuple support up to 16 elements | Yes |
| `tuple-32` | Tuple support up to 32 elements | No |

## Project Structure

```
afastdata/
├── Cargo.toml              # Workspace configuration
├── README.md               # English documentation (this file)
├── README_CN.md            # 中文文档
├── afastdata/              # Core library + unified entry crate
│   ├── Cargo.toml          # Contains `len-u64`, `tag-*`, `tuple-*` features
│   ├── src/lib.rs          # Trait definitions + primitive type implementations + re-exports derive macros
│   └── tests/
│       ├── derive_tests.rs     # Derive macro integration tests
│       └── primitive_tests.rs  # Primitive type serialization tests
└── afastdata-macro/        # Proc-macro library
    ├── Cargo.toml
    └── src/lib.rs          # AFastSerialize / AFastDeserialize derive macros
```

## Running the Example

```bash
cargo run --example basic -p afastdata
```

## Running Tests

```bash
cargo test --workspace
```

## License

MIT