Thanks to visit codestin.com
Credit goes to lib.rs

#encoded #text #struct #line #positional #deserialize #serialization #text-data #date-time

line_cutter

A library to quickly derive structs that de/serialize positionally encoded text data

2 stable releases

1.0.1 Oct 17, 2025

#779 in Encoding

MIT/Apache

13KB
52 lines

line_cutter

Crates.io Documentation License

A Rust library for parsing and encoding fixed-width positional text formats.

Overview

This crate provides:

  • The PositionalEncoded trait 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 occupies
  • decoder (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: YYYYMMDD
  • chrono::NaiveDateTime - Format: YYYYMMDDHHMMSS
  • chrono::NaiveTime - Format: HHMMSS
  • chrono::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 size attribute)
  • For optional types, you must handle the None case 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:

  • String fields 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

License

Licensed under either of:

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