source-kv 0.3.0

A Serde serialization and deserialization library for Valve's Key-Value format.
Documentation

source-kv

A straightforward, high-performance serde implementation for parsing and serializing Valve's textual Key-Values formats (such as .vmf, .vmt, gameinfo.txt, GameConfig.txt, etc.).

Key Features

  • Full Serde Integration: Seamlessly deserialize into custom Rust structs and serialize back using standard #[derive(Deserialize, Serialize)] attributes.
  • Duplicate Key Support: Valve's format heavily relies on duplicate keys (especially in .vmf files). source-kv natively handles this—just map repeated keys to a Vec<T>.
  • Order Preservation: Internally uses IndexMap to guarantee that the order of properties and blocks is preserved during serialization.
  • Syntax Tolerance: Safely handles unquoted keys, C-style line comments (//), and implicitly converts integer/boolean strings into native Rust types.
  • Dynamic / Untyped API: Don't want to write static structs? Parse files directly into a generic source_kv::Value (AST) and easily convert specific nodes to typed structs later using from_value.

Quick Start

1. Parsing Strongly-Typed Structures (e.g., VMF Entities)

Repeated keys are easily captured using Vec<T> alongside #[serde(rename = "...")].

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct MapData {
    // VMF files can contain multiple 'entity' blocks
    #[serde(rename = "entity", default)]
    entities: Vec<Entity>,
}

#[derive(Debug, Deserialize)]
struct Entity {
    id: u32,
    classname: String,
    origin: Option<String>,
    #[serde(default)]
    connections: Vec<Connection>,
}

#[derive(Debug, Deserialize)]
struct Connection {
    target: String,
    action: String,
}

fn main() -> Result<(), source_kv::Error> {
    let vmf_data = r#"
    entity
    {
        "id" "1337"
        "classname" "prop_physics"
        "origin" "0 0 64"
        
        "connections"
        {
            "target" "door_1"
            "action" "Open"
        }
    }
    "#;

    let parsed: MapData = source_kv::from_str(vmf_data)?;
    println!("{:#?}", parsed.entities);
    
    Ok(())
}

2. Using the Dynamic AST (source_kv::Value)

Sometimes you don't know the exact structure upfront. You can parse the document into an untyped tree and deserialize specific parts on demand.

use serde::Deserialize;
use source_kv::{Deserializer, Value, from_value};

#[derive(Debug, Deserialize)]
struct ToolConfig {
    #[serde(rename = "GameExe")]
    game_exe: String,
}

fn main() -> Result<(), source_kv::Error> {
    let input = r#"
        "Configs"
        {
            "Games"
            {
                "Portal 2"
                {
                    "GameExe" "portal2.exe"
                }
            }
        }
    "#;

    // Parse into an untyped Value tree
    let mut de = Deserializer::from_str(input);
    let root: Value = de.parse_root()?;

    // Traverse the AST natively
    if let Some(portal_config) = root.get("Configs")
        .and_then(|c| c.get("Games"))
        .and_then(|g| g.get("Portal 2")) 
    {
        // Convert the specific untyped block into a typed Rust struct
        let config: ToolConfig = from_value(portal_config.clone())?;
        println!("Executable: {}", config.game_exe);
    }

    Ok(())
}

API Overview

  • source_kv::from_str — Deserialize a KeyValues string directly into a typed struct T.
  • source_kv::to_string — Serialize a typed struct T back into KeyValues format.
  • source_kv::Value — An enum representing the AST (Str or Obj), offering ergonomic getters like .get(key) and .get_string(key).
  • source_kv::from_value — Convert an existing AST Value node into a Serde-compatible struct T.

License

MIT License.