serde-libconfigasaurus 0.4.0

Serde serialization and deserialization for libconfig format
Documentation

serde-libconfigasaurus

A Rust serde implementation for the libconfig configuration format.

Supported Types

All libconfig value types are supported:

libconfig type Rust / Value type
boolean bool / Value::Bool
integer i32 / Value::Integer
integer64 i64 / Value::Integer64
float f64 / Value::Float
string String / Value::String
array [...] Vec<T> / Value::Array (homogeneous scalars)
list (...) tuples, Vec of aggregates / Value::List (heterogeneous values)
group {...} structs / Value::Group (named settings)

Integer literals support decimal, hex (0xFF), binary (0b1010), and octal (0o17 / 0q17) formats. Values exceeding the 32-bit range are automatically promoted to 64-bit, or use the L / LL suffix explicitly (100L).

Rust enums round-trip too: unit variants are written as strings ("Variant"), and data-carrying variants as single-setting groups ({ Variant = value; }).

Quick Start

[dependencies]
serde-libconfigasaurus = "0.4"
serde = { version = "1.0", features = ["derive"] }

Typed Deserialization

use serde::Deserialize;
use libconfig::from_str;

#[derive(Debug, Deserialize)]
struct Server {
    hostname: String,
    port: i32,
    ssl: bool,
}

let config = r#"
    hostname = "localhost";
    port = 8080;
    ssl = true;
"#;

let server: Server = from_str(config).unwrap();

Serialization

use serde::Serialize;
use libconfig::to_string;

#[derive(Serialize)]
struct AppSettings {
    name: String,
    version: i32,
}

let settings = AppSettings {
    name: "My App".to_string(),
    version: 1,
};

// The top level of a libconfig document is an implicit setting list;
// braces only appear around nested groups.
let output = to_string(&settings).unwrap();
// name = "My App";
// version = 1;

Dynamic Config

When you don't know the structure at compile time, use Config for dynamic access:

use libconfig::Config;

let cfg = Config::from_str(r#"
    title = "My HTTP server";
    listen_ports = [80, 443];
    misc = {
        owner = "Chuck Norris";
        contact = {
            phone = "415-256-9999";
            emails = ["chuck@norris.com", "chuck.norris@gmail.com"];
        };
    };
"#).unwrap();

// Index with dotted paths
assert_eq!(cfg["title"].as_str(), Some("My HTTP server"));
assert_eq!(cfg["listen_ports.0"].as_i32(), Some(80));
assert_eq!(cfg["misc.contact.phone"].as_str(), Some("415-256-9999"));

Reading from a File

use libconfig::Config;

let cfg = Config::from_file("settings.cfg").unwrap();
println!("{}", cfg["database.host"]);

More API

  • to_value / from_value convert between any serde type and Value
  • Config::to_file writes a document back to disk; from_reader/to_writer work with any Read/Write
  • Parse errors carry positions: err.line(), err.column()
  • Option fields round-trip naturally: None fields are omitted on write, and absent settings read back as None
  • set accepts anything convertible into a Value: cfg.set("port", 8080)
  • WriteOptions controls output style (float precision, scientific notation, tab width, : vs =, brace placement, separators) — all combinations emit grammar-valid text
  • Custom @include resolvers via Config::from_str_with_include_fn (wildcards, search paths — the config_set_include_func equivalent)
  • &str struct fields deserialize zero-copy when the string needs no escape decoding or concatenation

Setting Paths

Values can be read, written, and removed using libconfig-style dotted paths. Array and list elements are addressed with bracket notation ([index]) or bare numeric segments:

use libconfig::{Config, Value};

let mut cfg = Config::from_str(r#"ports = [80, 443];"#).unwrap();

// Read
assert_eq!(cfg.lookup("ports.[0]").unwrap().as_i32(), Some(80));
assert_eq!(cfg.lookup("ports.0").unwrap().as_i32(), Some(80)); // bare numeric also works

// Write (set existing) — anything convertible into a Value works
cfg.set("ports.[0]", 8080).unwrap();
assert_eq!(cfg["ports.[0]"], 8080);

// Write (auto-creates intermediate groups)
cfg.set("database.credentials.username", "admin").unwrap();
assert_eq!(cfg["database.credentials.username"], "admin");

// Remove (returns the removed value)
let mut cfg = Config::from_str(r#"a = 1; b = 2; c = 3;"#).unwrap();
let removed = cfg.remove("b");
assert_eq!(removed, Some(Value::Integer(2)));
assert!(cfg.lookup("b").is_none());

// Remove array element (subsequent elements shift down)
let mut cfg = Config::from_str(r#"items = [10, 20, 30];"#).unwrap();
cfg.remove("items.[0]").unwrap();
assert_eq!(cfg["items.[0]"], 20);

Format Features

  • Top-level implicit groups (no outer braces required)
  • Case-insensitive booleans (true, True, TRUE, FaLsE)
  • Adjacent string concatenation ("hello" " world" -> "hello world")
  • Comments: #, //, /* */
  • Setting separators: semicolons, commas, or whitespace (all optional)
  • Both = and : as key-value delimiters
  • @include "file.cfg" directives (via Config::from_str / Config::from_file, with *_with_include_dir variants to set the include directory)

Spec Conformance

Parsing and serialization follow the libconfig manual (v1.8.2), including its Appendix B grammar:

  • The document root is a setting list (<configuration> ::= <setting-list> | <empty>); top-level braces are rejected on parse and never written — braces delimit nested groups only
  • Setting names must match [A-Za-z*][-A-Za-z0-9_*]* (validated on read and write)
  • Duplicate setting names within a group are rejected
  • Arrays may only contain scalar values of a single type; anything else belongs in a list (serialization picks [...] vs (...) automatically)
  • Out-of-range integer conversions and float-to-integer coercions are errors, not silent truncations; non-finite floats refuse to serialize
  • Strings must be double-quoted; \xNN escapes take exactly two hex digits

The manual (including its Appendix B grammar) is the sole authority for this crate's behavior. Output has additionally been cross-checked against the C libconfig library as a comparison point; where an installed C version disagrees with the manual (e.g. pre-1.8 versions lacking binary/octal literals, trailing commas, or 64-bit auto-promotion), the manual wins.

Documented deviations: \xNN escapes ≥ 0x80 map to Unicode code points (Rust strings cannot hold bare bytes), integers are accepted where floats are expected (lossless), and the serde-level from_str does not process @include directives (use Config for that).

Examples

cargo run --example basic       # Typed serialization/deserialization round-trip
cargo run --example config      # Dynamic Config type: get/set all libconfig types
cargo run --example streaming   # Tokio TCP stream of libconfig packets

License

MIT OR Apache-2.0