2 stable releases
| 1.0.1 | Oct 17, 2025 |
|---|
#779 in Encoding
13KB
52 lines
line_cutter
A Rust library for parsing and encoding fixed-width positional text formats.
Overview
This crate provides:
- The
PositionalEncodedtrait for types that can be encoded/decoded from positional text - The
#[derive(PositionalText)]macro for automatic implementation - Support for standard Rust types and custom encoding/decoding logic
Basic Usage
use line_cutter::PositionalText;
#[derive(PositionalText)]
pub struct MyRecord {
#[positional_field(start = 1, size = 10)]
name: String,
#[positional_field(start = 11, size = 8)]
birth_date: chrono::NaiveDate,
#[positional_field(start = 19, size = 5)]
count: Option<u32>,
}
// Decoding from positional text
let text = "John Doe 2024010100042";
let record = MyRecord::decode(text)?;
// Encoding back to positional text
let encoded = record.encode();
// Validation
record.validate()?;
Field Attributes
Each field must be annotated with #[positional_field()] containing:
start(required): The 1-based starting position in the text (first character is position 1)size(required): The number of characters the field occupiesdecoder(optional): Path to a custom decoder function (see Custom Encoders/Decoders)encoder(optional): Path to a custom encoder function (see Custom Encoders/Decoders)
Supported Types
The macro supports the following Rust types out of the box:
Numeric Types
bool- Encoded as "Y" or "N"i32,i64- Zero-padded integers, negative values prefixed with "-"u8,u16,u32,u64- Zero-padded unsigned integers
String Types
String- Left-aligned, space-padded
Date/Time Types (chrono)
chrono::NaiveDate- Format:YYYYMMDDchrono::NaiveDateTime- Format:YYYYMMDDHHMMSSchrono::NaiveTime- Format:HHMMSSchrono::TimeDelta- Format:HHMMSS(duration)
Optional Types
All of the above types can be wrapped in Option<T>. Empty/whitespace fields decode to None.
Custom Encoders and Decoders
For cases where the standard encoding/decoding logic doesn't fit your needs, you can provide custom encoder and decoder functions.
Custom Decoder
A custom decoder function takes a string slice and returns a Result:
// For non-optional types
fn custom_decoder(s: &str) -> Result<MyType, String> {
// Your custom parsing logic
MyType::parse(s.trim())
.ok_or_else(|| format!("Failed to parse: {}", s))
}
// For optional types
fn custom_optional_decoder(s: &str) -> Result<Option<MyType>, String> {
if s.trim().is_empty() {
Ok(None)
} else {
Ok(Some(MyType::parse(s.trim())?))
}
}
Important:
- The decoder receives the exact substring from the positional text, including any padding
- It must return
Result<T, String>where the error message describes what went wrong - The macro will automatically add field name context to error messages
Custom Encoder
A custom encoder function takes a reference to your type and returns a String:
// For non-optional types
fn custom_encoder(val: &MyType) -> String {
format!("{:010}", val.to_string()) // Must return exact width!
}
// For optional types
fn custom_optional_encoder(val: &Option<MyType>) -> String {
match val {
Some(v) => format!("{:010}", v.to_string()),
None => " ".repeat(10), // Must match field size!
}
}
Important:
- The encoder must return a string of exactly the right width (matching the
sizeattribute) - For optional types, you must handle the
Nonecase appropriately (typically with spaces)
Usage Example
use line_cutter::PositionalText;
use chrono::NaiveDate;
// Custom type
#[derive(Debug, PartialEq)]
struct CustomDate(NaiveDate);
// Custom decoder: DDMMYYYY format instead of YYYYMMDD
fn decode_custom_date(s: &str) -> Result<CustomDate, String> {
let trimmed = s.trim();
if trimmed.len() != 8 {
return Err(format!("Expected 8 characters, got {}", trimmed.len()));
}
let day: u32 = trimmed[0..2].parse()
.map_err(|e| format!("Invalid day: {}", e))?;
let month: u32 = trimmed[2..4].parse()
.map_err(|e| format!("Invalid month: {}", e))?;
let year: i32 = trimmed[4..8].parse()
.map_err(|e| format!("Invalid year: {}", e))?;
NaiveDate::from_ymd_opt(year, month, day)
.map(CustomDate)
.ok_or_else(|| "Invalid date".to_string())
}
// Custom encoder: DDMMYYYY format
fn encode_custom_date(date: &CustomDate) -> String {
date.0.format("%d%m%Y").to_string()
}
#[derive(PositionalText)]
pub struct Record {
#[positional_field(start = 1, size = 3)]
record_type: String,
// Using custom decoder and encoder
#[positional_field(
start = 4,
size = 8,
decoder = "decode_custom_date",
encoder = "encode_custom_date"
)]
custom_date: CustomDate,
// Only custom decoder (uses standard encoder)
#[positional_field(start = 12, size = 10, decoder = "my_decoder")]
custom_field: MyType,
// Only custom encoder (uses standard decoder)
#[positional_field(start = 22, size = 8, encoder = "my_encoder")]
special_date: NaiveDate,
}
Function Paths
The decoder and encoder attributes accept string literals that are parsed as Rust paths. You can use:
- Function names in the same module:
"my_function" - Paths to functions in other modules:
"utils::parsing::custom_decoder" - Paths to associated functions:
"MyType::decode_positional"
The PositionalEncoded Trait
The #[derive(PositionalText)] macro generates an implementation of the PositionalEncoded trait:
pub trait PositionalEncoded {
/// Decode a fixed-width string into a struct
fn decode(s: &str) -> Result<Box<Self>, String>
where
Self: Sized;
/// Encode a struct into a fixed-width string
fn encode(&self) -> String;
/// Validate field values before encoding
fn validate(&self) -> Result<(), Vec<ValidationError>>;
}
Decode
Parses a fixed-width positional text string and returns a boxed instance of the struct. Returns an error if:
- The input string length doesn't match the expected record length
- Any field fails to parse
- Custom decoder functions return errors
Encode
Converts the struct back into a fixed-width positional text string with proper padding and formatting.
Validate
Checks that all field values are valid for encoding. Currently validates:
Stringfields don't exceed their maximum length
Custom validation logic is planned for a future release.
Validation
Currently, validation is automatically generated for String fields to ensure they don't exceed their maximum length. Custom validation logic via a validator attribute is planned for a future release.
Error Handling
- Decode errors include the field name and specific error message
- Length errors are detected automatically when the input string doesn't match the expected total length
- Custom decoder errors are wrapped with field name context for better debugging
Example error:
Failed to decode MyRecord record.
Invalid record length.
Expected 23 but found 20.
Full record: John Doe 20240101
Features
This crate currently has no optional features. The chrono dependency is always included for date/time support.
Repository
- Main crate: line_cutter
- Derive macros: line_cutter-macros
- Source code: GitLab
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Dependencies
~1–1.8MB
~32K SLoC