nested-text 0.1.0

A fully spec-compliant NestedText v3.8 parser and serializer
Documentation
# nested-text

[![crates.io](https://img.shields.io/crates/v/nested-text.svg)](https://crates.io/crates/nested-text)
[![docs.rs](https://docs.rs/nested-text/badge.svg)](https://docs.rs/nested-text)

A fully spec-compliant [NestedText](https://nestedtext.org) v3.8 parser and serializer for Rust.

## Compliance

nested-text passes **100% of the official NestedText test suite** ([KenKundert/nestedtext_tests](https://github.com/KenKundert/nestedtext_tests)):

- **146/146 load tests** — including full validation of error messages, line numbers, column numbers, and source line text for all 68 error cases
- **80/80 roundtrip dump tests** — every successfully loaded document survives a dump/load cycle with identical output
- 2 tests skipped (non-UTF-8 encoding tests — NestedText is a UTF-8 format)

The implementation mirrors the architecture of the [Python reference implementation](https://github.com/KenKundert/nestedtext) by Ken Kundert: a line-based lexer feeding a recursive descent parser.

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
nested-text = "0.1"
```

serde support is enabled by default. To disable it:

```toml
[dependencies]
nested-text = { version = "0.1", default-features = false }
```

## Usage

### Direct API

```rust
use nested_text::{loads, dumps, Top, Value, DumpOptions};

// Parse a NestedText string
let input = "name: Alice\nage: 30\nitems:\n  - one\n  - two\n";
let value = loads(input, Top::Any).unwrap().unwrap();

// Access values
assert_eq!(value.get("name").unwrap().as_str(), Some("Alice"));
assert_eq!(value.get("age").unwrap().as_str(), Some("30"));

// Serialize back to NestedText
let output = dumps(&value, &DumpOptions::default());
let roundtripped = loads(&output, Top::Any).unwrap().unwrap();
assert_eq!(value, roundtripped);
```

### Top-level type constraint

```rust
use nested_text::{loads, Top};

// Require a specific top-level type
let dict = loads("key: value", Top::Dict).unwrap();
let list = loads("- item", Top::List).unwrap();
let string = loads("> hello", Top::String).unwrap();

// Empty documents return a default value for the given type
let empty_dict = loads("", Top::Dict).unwrap();  // Some(Dict([]))
let empty_any = loads("", Top::Any).unwrap();     // None
```

### serde integration

```rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Config {
    name: String,
    debug: bool,
    port: u16,
    tags: Vec<String>,
}

let input = "\
name: my-app
debug: true
port: 8080
tags:
    - web
    - api
";

// Deserialize — numeric and boolean fields are parsed from strings
let config: Config = nested_text::from_str(input).unwrap();
assert_eq!(config.name, "my-app");
assert_eq!(config.debug, true);
assert_eq!(config.port, 8080);

// Serialize back to NestedText
let output = nested_text::to_string(&config).unwrap();
```

### Reading from files

```rust
use nested_text::{load, Top};
use std::fs::File;

let file = File::open("config.nt").unwrap();
let value = load(file, Top::Any).unwrap();
```

### Error handling

Errors include location information matching the reference implementation:

```rust
use nested_text::{loads, Top};

match loads("key: a\nkey: b", Top::Any) {
    Err(e) => {
        assert_eq!(e.message, "duplicate key: key.");
        assert_eq!(e.lineno, Some(1));  // 0-based
        assert_eq!(e.colno, Some(0));   // 0-based
        assert_eq!(e.line.as_deref(), Some("key: b"));
    }
    _ => panic!("expected error"),
}
```

### Building values directly

```rust
use nested_text::{Value, dumps, DumpOptions};

let value = Value::Dict(vec![
    ("name".into(), Value::String("Alice".into())),
    ("scores".into(), Value::List(vec![
        Value::String("95".into()),
        Value::String("87".into()),
    ])),
]);

let nt = dumps(&value, &DumpOptions::default());
// name: Alice
// scores:
//     - 95
//     - 87
```

## API Reference

### Parsing

| Function | Description |
|----------|-------------|
| `loads(input, top) -> Result<Option<Value>>` | Parse a NestedText string |
| `load(reader, top) -> Result<Option<Value>>` | Parse from any `Read` implementor |
| `from_str<T>(input) -> Result<T>` | Deserialize via serde (requires `serde` feature) |

### Serialization

| Function | Description |
|----------|-------------|
| `dumps(value, options) -> String` | Serialize a `Value` to NestedText |
| `dump(value, options, writer) -> Result<()>` | Serialize to any `Write` implementor |
| `to_string(value) -> Result<String>` | Serialize via serde (requires `serde` feature) |
| `to_string_with_options(value, options) -> Result<String>` | Serialize via serde with custom options |

### Types

- **`Value`**`String(String)`, `List(Vec<Value>)`, `Dict(Vec<(String, Value)>)`
  - Dicts preserve insertion order using `Vec<(String, Value)>`
  - Convenience methods: `as_str()`, `as_list()`, `as_dict()`, `get(key)`, `is_string()`, `is_list()`, `is_dict()`
- **`Top`**`Dict`, `List`, `String`, `Any`
- **`DumpOptions`**`indent: usize` (default 4), `sort_keys: bool` (default false)
- **`Error`**`message`, `lineno` (0-based), `colno` (0-based), `line`, `kind`

## NestedText format

NestedText is a data format similar to YAML but with no type ambiguity — all leaf values are strings. No quoting, no escaping, no type coercion surprises.

```
# This is a comment
server:
    host: localhost
    port: 8080
    features:
        - logging
        - metrics
    database:
        > postgres://localhost
        > :5432/mydb
```

For the full specification, see [nestedtext.org](https://nestedtext.org/en/latest/file_format.html).

## Validating compliance

Clone with the official test suite submodule and run:

```sh
git clone --recurse-submodules https://github.com/hansstimer/nested-text.git
cd nested-text
cargo test
```

To see detailed pass/fail counts:

```sh
cargo test --test official -- --nocapture
```

This runs the full [official NestedText test suite](https://github.com/KenKundert/nestedtext_tests) automatically:

- **Load tests** — decodes each test case from `tests.json`, parses the input, and compares the result against `load_out`. For error cases, validates the error message, line number, column number, and source line text against `load_err`.
- **Roundtrip dump tests** — for every successful load test, serializes the parsed value back to NestedText, parses it again, and verifies the result is identical.

If you already cloned without `--recurse-submodules`:

```sh
git submodule update --init
```

## License

Licensed under either of

- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT]LICENSE-MIT or <http://opensource.org/licenses/MIT>)

at your option.