# nested-text
[](https://crates.io/crates/nested-text)
[](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
| `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
| `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.