serde-structprop 0.3.0

Serde serializer and deserializer for the structprop config file format
Documentation

serde-structprop

CI crates.io docs.rs Rust version License

A serde serializer and deserializer for the structprop configuration file format — a simple, human-readable format for structured data.

Format overview

Structprop files are composed of three constructs:

# Lines beginning with # are comments (inline comments are also supported)

# Scalar key-value pair
key = value
key = "value with spaces or special chars"
key = 42
key = -7
key = true

# Nested object block
section {
  nested_key = value
  another    = 123
}

# Array of scalars
list = { a b c }
list = {
  a
  b
  c
}

Special characters in values (spaces, tabs, newlines, carriage returns, #, {, }, =) must be wrapped in double quotes. Empty strings are always quoted as "". The structprop format has no escape sequences. A " character in the interior of an otherwise-bare value is emitted as-is (e.g. hello"worldval = hello"world). Two cases are unrepresentable and produce a serialization error:

  • A value that requires quoting and contains " (no way to embed " inside a quoted term).
  • A value whose first character is " (the lexer treats a leading " as the start of a quoted string).

Keys follow the same rule.

Installation

Add to your Cargo.toml:

[dependencies]
serde-structprop = { version = "0.2", features = ["derive"] }

The derive feature enables serde's own derive macros. You still need a direct serde dependency in your crate so that Serialize and Deserialize are in scope. If you already depend on serde with features = ["derive"], you can omit the feature flag here:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde-structprop = "0.2"

Type mapping

Rust / serde type Structprop representation
bool true or false
i8 i16 i32 i64 u8 u16 u32 u64 bare integer scalar (e.g. 42, -7)
f32, f64 bare float scalar (e.g. 3.14)
char bare single-character scalar
String / &str bare scalar, or "quoted" when it contains special chars or is empty; a " mixed with other special chars is unrepresentable (serialization error)
Option<T> (Some) the inner value serialized normally
Option<T> (None) / () null
newtype struct (e.g. struct Meters(f64)) transparent — serializes as the inner type
unit struct (e.g. struct Marker;) null
struct / map key { … } block
Vec<T> / sequence key = { … } list
tuple / tuple struct key = { … } list of elements
unit enum variant bare variant name
newtype enum variant variant_name = <scalar or list>
tuple enum variant variant_name = { … } list
struct enum variant variant_name { … } block
raw bytes (serialize_bytes / deserialize_bytes) unsupported — returns Error::UnsupportedType

Quick start

Deserializing

use serde::Deserialize;
use serde_structprop::from_str;

#[derive(Debug, Deserialize)]
struct Config {
    hostname: String,
    port: u16,
    debug: bool,
}

fn main() {
    let input = "
        # server config
        hostname = localhost
        port     = 8080
        debug    = true
    ";

    let cfg: Config = from_str(input).unwrap();
    println!("{cfg:?}");
    // Config { hostname: "localhost", port: 8080, debug: true }
}

Serializing

use serde::Serialize;
use serde_structprop::to_string;

#[derive(Serialize)]
struct Config {
    hostname: String,
    port: u16,
    debug: bool,
}

fn main() {
    let cfg = Config {
        hostname: "localhost".into(),
        port: 8080,
        debug: true,
    };

    let out = to_string(&cfg).unwrap();
    println!("{out}");
    // hostname = localhost
    // port = 8080
    // debug = true
}

Nested structs

use serde::{Deserialize, Serialize};
use serde_structprop::{from_str, to_string};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Database {
    hostname: String,
    port: u16,
    name: String,
}

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Config {
    database: Database,
    tables: Vec<String>,
}

fn main() {
    let input = "
        database {
          hostname = db.example.com
          port     = 5432
          name     = myapp
        }

        tables = { users orders products }
    ";

    let cfg: Config = from_str(input).unwrap();
    assert_eq!(cfg.database.port, 5432);
    assert_eq!(cfg.tables, vec!["users", "orders", "products"]);

    // Round-trip back to structprop text
    let out = to_string(&cfg).unwrap();
    println!("{out}");
    // database {
    //   hostname = db.example.com
    //   port = 5432
    //   name = myapp
    // }
    // tables = {
    //   users
    //   orders
    //   products
    // }
}

Quoted values

Values containing spaces, tabs, newlines, or the special characters (#, {, }, =) are quoted automatically on output and must be quoted in the input. Empty strings are always quoted as "".

The structprop format has no escape sequences. A " in the interior of an otherwise-bare value is emitted as-is (e.g. hello"worldval = hello"world). Two cases are unrepresentable and return an error: a value that requires quoting and contains ", and a value whose first character is " (the lexer always interprets a leading " as the start of a quoted string).

use serde::{Deserialize, Serialize};
use serde_structprop::{from_str, to_string};

#[derive(Debug, Serialize, Deserialize)]
struct S {
    message: String,
    empty: String,
}

fn main() {
    let s: S = from_str(r#"message = "hello world"
empty = """#).unwrap();
    assert_eq!(s.message, "hello world");
    assert_eq!(s.empty, "");

    let out = to_string(&s).unwrap();
    assert_eq!(out, "message = \"hello world\"\nempty = \"\"\n");
}

Optional fields

Fields typed as Option<T> are serialized as null when None and as the inner value when Some. A missing key in the input also deserializes as None:

use serde::{Deserialize, Serialize};
use serde_structprop::{from_str, to_string};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct S {
    required: String,
    optional: Option<u32>,
}

fn main() {
    // Some value
    let s: S = from_str("required = hello\noptional = 42\n").unwrap();
    assert_eq!(s.optional, Some(42));

    // Explicit null
    let s: S = from_str("required = hello\noptional = null\n").unwrap();
    assert_eq!(s.optional, None);

    // Missing key also deserializes as None
    let s: S = from_str("required = hello\n").unwrap();
    assert_eq!(s.optional, None);

    // None serializes as null
    let out = to_string(&S { required: "hello".into(), optional: None }).unwrap();
    assert!(out.contains("optional = null"));
}

Error handling

All functions return serde_structprop::Result<T>, an alias for std::result::Result<T, serde_structprop::Error>.

use serde_structprop::{from_str, Error};

#[derive(serde::Deserialize)]
struct S { x: u32 }

match from_str::<S>("x = not_a_number\n") {
    Ok(s)                    => println!("x = {}", s.x),
    Err(Error::Parse(msg))   => eprintln!("parse error: {msg}"),
    Err(Error::Message(msg)) => eprintln!("serde error: {msg}"),
    Err(e)                   => eprintln!("other error: {e}"),
}

Error variants

Variant When
Error::Parse(String) Lexer or parser encountered unexpected input, or a scalar could not be coerced to the requested numeric type
Error::Message(String) serde-generated error (e.g. missing required field, unknown variant)
Error::UnsupportedType(&'static str) Type has no structprop equivalent (e.g. raw byte slices)
Error::KeyMustBeString A map was serialized with a non-string key

Module layout

Module Contents
serde_structprop::lexer Tokenizer: converts raw text to Tokens
serde_structprop::parse Recursive-descent parser: produces a Value tree
serde_structprop::de serde::Deserializer implementation; from_str entry point
serde_structprop::ser serde::Serializer implementation; to_string entry point
serde_structprop::error Error enum and Result<T> alias

License

Licensed under either of

at your option.