| Crates.io | line_cutter |
| lib.rs | line_cutter |
| version | 1.0.1 |
| created_at | 2025-10-17 16:21:35.477591+00 |
| updated_at | 2025-10-17 20:56:38.390922+00 |
| description | A library to quickly derive structs that de/serialize positionally encoded text data. |
| homepage | https://gitlab.com/brunorobert/line_cutter |
| repository | https://gitlab.com/brunorobert/line_cutter |
| max_upload_size | |
| id | 1887871 |
| size | 35,957 |
A Rust library for parsing and encoding fixed-width positional text formats.
This crate provides:
PositionalEncoded trait for types that can be encoded/decoded from positional text#[derive(PositionalText)] macro for automatic implementationuse 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()?;
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)The macro supports the following Rust types out of the box:
bool - Encoded as "Y" or "N"i32, i64 - Zero-padded integers, negative values prefixed with "-"u8, u16, u32, u64 - Zero-padded unsigned integersString - Left-aligned, space-paddedchrono::NaiveDate - Format: YYYYMMDDchrono::NaiveDateTime - Format: YYYYMMDDHHMMSSchrono::NaiveTime - Format: HHMMSSchrono::TimeDelta - Format: HHMMSS (duration)All of the above types can be wrapped in Option<T>. Empty/whitespace fields decode to None.
For cases where the standard encoding/decoding logic doesn't fit your needs, you can provide custom encoder and decoder functions.
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:
Result<T, String> where the error message describes what went wrongA 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:
size attribute)None case appropriately (typically with spaces)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,
}
The decoder and encoder attributes accept string literals that are parsed as Rust paths. You can use:
"my_function""utils::parsing::custom_decoder""MyType::decode_positional"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>>;
}
Parses a fixed-width positional text string and returns a boxed instance of the struct. Returns an error if:
Converts the struct back into a fixed-width positional text string with proper padding and formatting.
Checks that all field values are valid for encoding. Currently validates:
String fields don't exceed their maximum lengthCustom validation logic is planned for a future release.
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.
Example error:
Failed to decode MyRecord record.
Invalid record length.
Expected 23 but found 20.
Full record: John Doe 20240101
This crate currently has no optional features. The chrono dependency is always included for date/time support.
Licensed under either of:
at your option.
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.