A MoonBit port of the NestedText serialization format.
Dependencies
Notice: This project is a MoonBit port of the Rust nested-text crate. It inherits the Apache-2.0 OR MIT dual license.
moon add OrisGo/nestedtext///|
test "quick start" {
match @nestedtext.loads("name: Alice\nage: 30", @nestedtext.Top::Any) {
Ok(Some(v)) =>
println(@nestedtext.dumps(v, @nestedtext.DumpOptions::default()))
Ok(None) => println("(empty)")
Err(e) => println("error: \{e.to_string()}")
}
}let content = @fs.read_file_to_string("config.nt") catch {
IOError(msg) => { println(msg); return }
}
match @nestedtext.loads(content, @nestedtext.Top::Any) {
Ok(Some(v)) => { /* use v */ }
Err(e) => println(e.to_string())
}moonbitlang/core does not yet include a stable @fs module. See File I/O for options.
moon run cmd/main -- examples/config.nt=== examples/config.nt ===
PASS | 8 lines | 5ms | -> examples/config.nt.out$env:NESTEDTEXT_TOP = "list"; moon run cmd/main -- examples/data.nt| Approach | Status | Recommendation |
|---|---|---|
| moonbitlang/x/fs | Experimental (moonbitlang/x v0.4.x) | Use today — works on native target |
| moonbitlang/core/fs | Planned (beta-preview, ~Aug 2026) | Wait for stable release |
| External language (Python, etc.) | Always available | Bridge via subprocess or FFI |
import {
"OrisGo/nestedtext" @nestedtext,
"moonbitlang/x/fs" @fs,
}let content = @fs.read_file_to_string("data.nt") catch {
IOError(msg) => { println(msg); return }
}
match @nestedtext.loads(content, @nestedtext.Top::Any) {
Ok(Some(v)) => println(@nestedtext.dumps(v, @nestedtext.DumpOptions::default()))
Err(e) => println(e.to_string())
}///|
test "parse dictionary" {
let input = "name: Alice\nage: 30"
match @nestedtext.loads(input, @nestedtext.Top::Any) {
Ok(Some(@nestedtext.Value::Dict(pairs))) => {
@debug.assert_eq(pairs[0], ("name", @nestedtext.Value::String("Alice")))
@debug.assert_eq(pairs[1], ("age", @nestedtext.Value::String("30")))
}
_ => fail("unexpected result")
}
}
///|
test "parse nested list" {
let input = "fruits:\n - apple\n - banana"
match @nestedtext.loads(input, @nestedtext.Top::Dict) {
Ok(Some(@nestedtext.Value::Dict(pairs))) => {
@debug.assert_eq(pairs[0].0, "fruits")
let expected = @nestedtext.Value::List([
@nestedtext.Value::String("apple"),
@nestedtext.Value::String("banana"),
])
@debug.assert_eq(pairs[0].1, expected)
}
_ => fail("unexpected result")
}
}| Variant | Represents |
|---|---|
| String(String) | A scalar string value |
| List(Array[Value]) | An ordered list |
| Dict(Array[(String, Value)]) | Name-value pairs in insertion order |
///|
test "serialize to nestedtext" {
let value = @nestedtext.Value::Dict([
("name", @nestedtext.Value::String("Alice")),
("age", @nestedtext.Value::String("30")),
])
let output = @nestedtext.dumps(value, @nestedtext.DumpOptions::default())
@debug.assert_eq(output, "name: Alice\nage: 30\n")
}
///|
test "serialize with sorted keys" {
let value = @nestedtext.Value::Dict([
("z", @nestedtext.Value::String("last")),
("a", @nestedtext.Value::String("first")),
])
let opts = @nestedtext.DumpOptions::{ indent: 4, sort_keys: true }
@debug.assert_eq(@nestedtext.dumps(value, opts), "a: first\nz: last\n")
}
///|
test "roundtrip" {
let input = "name: Alice\nage: 30"
match @nestedtext.loads(input, @nestedtext.Top::Any) {
Ok(Some(value)) => {
let output = @nestedtext.dumps(value, @nestedtext.DumpOptions::default())
match @nestedtext.loads(output, @nestedtext.Top::Any) {
Ok(Some(rv)) => @debug.assert_eq(value, rv)
_ => fail("roundtrip parse failed")
}
}
_ => fail("initial parse failed")
}
}| Aspect | Rust serde | OrisGo/nestedtext |
|---|---|---|
| Mechanism | Deserialize trait + #[derive(Deserialize)] | Callback closure fn(Deserializer) -> Result[T, _] |
| Struct deserialization | Automatic via derive | Manual via get_field + expect_* |
| Visitor pattern | Visitor trait with visit_* methods | Direct method calls on Deserializer |
| Error propagation | serde::de::Error trait | Result[T, DeserializeError] chaining with try |
| Input format | Generic data model | NestedText AST only (Value enum) |
| All values are strings | N/A (format-dependent) | Yes — expect_int() etc. parse from Value::String |
///|
test "deserialize typed struct" {
fn person(
d : @nestedtext.Deserializer,
) -> Result[(String, Int), @nestedtext.DeserializeError] {
match d.get_field("name") {
Ok(nd) =>
match nd.expect_string() {
Ok(name) =>
match d.get_field("age") {
Ok(ad) =>
match ad.expect_int() {
Ok(age) => Ok((name, age))
Err(e) => Err(e)
}
Err(e) => Err(e)
}
Err(e) => Err(e)
}
Err(e) => Err(e)
}
}
let input = "name: Alice\nage: 30"
match @nestedtext.deserialize_str(input, @nestedtext.Top::Any, person) {
Ok((name, age)) => {
@debug.assert_eq(name, "Alice")
@debug.assert_eq(age, 30)
}
Err(e) => fail(e.to_string())
}
}
///|
test "deserialize list of ints" {
let d = @nestedtext.Deserializer::new(
@nestedtext.Value::List([
@nestedtext.Value::String("1"),
@nestedtext.Value::String("2"),
@nestedtext.Value::String("3"),
]),
)
match @nestedtext.deserialize_list(d, fn(d2) { d2.expect_int() }) {
Ok(ints) => @debug.assert_eq(ints, [1, 2, 3])
Err(_) => fail("unexpected error")
}
}
///|
test "deserialize optional field" {
let d = @nestedtext.Deserializer::new(@nestedtext.Value::String(""))
match d.expect_optional(fn(d2) { d2.expect_int() }) {
Ok(None) => ()
_ => fail("expected None")
}
}| Method | Target Type | Notes |
|---|---|---|
| expect_string() | String | Identity extraction |
| expect_int() | Int | Parses decimal representation |
| expect_int64() | Int64 | Parses decimal representation |
| expect_double() | Double | Parses decimal representation |
| expect_bool() | Bool | Accepts true/True/TRUE/yes/Yes/YES (and false/no equivalents) |
| expect_list() | Array[Value] | Raw list items |
| expect_dict() | Array[(String, Value)] | Raw dictionary pairs |
| get_field(key) | Deserializer | Look up a single field |
| expect_optional(f) | T? | "" → None, otherwise → Some(f(d)) |
| has_field(key) | Bool | Check key existence |
| field_names() | Array[String] | All keys in the dictionary |
///|
test "error location" {
match @nestedtext.loads("key: value", @nestedtext.Top::Any) {
Ok(value) =>
match
@nestedtext.deserialize_value(value.unwrap(), fn(d) { d.expect_int() }) {
Err(e) => @debug.assert_eq(e.message, "expected string, got dictionary")
Ok(_) => fail("expected error")
}
Err(e) => fail(e.to_string())
}
}
///|
test "parse error with location" {
match @nestedtext.loads(" key: value", @nestedtext.Top::Any) {
Err(err) => {
@debug.assert_eq(err.message, "top-level content must start in column 1.")
assert_true(err.lineno == Some(1))
}
Ok(_) => fail("expected error")
}
}fn Deserializer::expect_dict(self : Deserializer) -> Result[Array[(String, Value)], DeserializeError]fn[T] Deserializer::expect_optional(self : Deserializer, f : (Deserializer) -> Result[T, DeserializeError]) -> Result[T?, DeserializeError]fn Deserializer::get_field(self : Deserializer, key : String) -> Result[Deserializer, DeserializeError]fn NestedTextError::at(kind : ErrorKind, message : String, lineno : Int, colno : Int, line : String) -> NestedTextErrorfn[T] deserialize_list(d : Deserializer, f : (Deserializer) -> Result[T, DeserializeError]) -> Result[Array[T], DeserializeError]fn[T] deserialize_str(input : String, top : Top, f : (Deserializer) -> Result[T, DeserializeError]) -> Result[T, NestedTextError]fn[T] deserialize_value(value : Value, f : (Deserializer) -> Result[T, DeserializeError]) -> Result[T, DeserializeError]A MoonBit port of the NestedText serialization format.
Dependencies