# lino-objects-codec (Rust)
[](https://github.com/link-foundation/lino-objects-codec/actions/workflows/rust.yml)
[](https://crates.io/crates/lino-objects-codec)
[](https://docs.rs/lino-objects-codec)
[](https://unlicense.org/)
Rust implementation of the Links Notation Objects Codec - a universal serialization library to encode/decode objects to/from Links Notation format.
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
lino-objects-codec = "0.8"
```
## Features
- **Universal Serialization**: Encode objects to Links Notation format
- **Type Support**: Handle all common types:
- `null` (Null)
- `bool` (Bool)
- `int` (Int - 64-bit signed)
- `float` (Float - 64-bit, including NaN, Infinity, -Infinity)
- `str` (String)
- `array` (Array)
- `object` (Object)
- **Special Float Values**: Full support for NaN, Infinity, -Infinity (which are not valid JSON)
- **Circular References**: Preserved by the compact format (`encode_compact`) via object ids; the readable format is a plain tree and returns a `CircularReference` error instead
- **Object Identity**: Shared references are preserved by the compact format
- **Opt-in Tracing**: Set `LINO_CODEC_DEBUG=1` to trace encoding and decoding, the same way in every language
- **Readable by Default**: `encode()` writes indented, plain-text Links Notation; keys and values stay legible and diffable
- **One Record per Line**: `encode_line()` writes the same document on one line and `decode_line()` reads it back exactly, so an append-only log stays greppable, tailable and countable by `wc -l`
- **UTF-8 Support**: Full Unicode string support written as text — a newline stays a newline and a tab stays a tab; only the characters a form cannot carry are percent-escaped, and each such value is marked individually
- **Simple API**: Easy-to-use `encode()` and `decode()` functions
## Quick Start
```rust
use lino_objects_codec::{encode, decode, LinoValue};
// Create an object
let data = LinoValue::object([
("name", LinoValue::String("Alice".to_string())),
("age", LinoValue::Int(30)),
("active", LinoValue::Bool(true)),
]);
// Encode to Links Notation
let encoded = encode(&data);
assert_eq!(encoded, "(\n name \"Alice\"\n age 30\n active true\n)");
// Decode back
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, data);
```
The encoded document reads as:
```lino
(
name "Alice"
age 30
active true
)
```
## Output Formats
| `encode(value)` | Readable, indented Links Notation (the default) |
| `encode_with_indent(value, "\t")` | Same, with a custom indentation string |
| `encode_line(value)` | The same readable document on one line, for append-only logs |
| `encode_compact(value)` | The previous single-line base64 form |
| `encode_obfuscated(value)` | Alias of `encode_compact` |
`decode()` accepts every one of them, so files written by older versions keep
working and are rewritten in the readable form the next time they are saved.
## API Reference
### Public value formatting
`format::format_value_single_line(&str)` returns one quoted or escaped readable
value with no CR/LF. `format::format_value_verbatim(&str)` keeps real newlines and
tabs, escaping only controls that the readable format cannot carry. Both can be
passed directly to `decode()` and recover the original string. Use these helpers
when composing records without encoding a temporary object first. The lower
level `format::quote` and `format::unescape` are public too; `quote` only selects
delimiters, so use the full formatter for arbitrary control characters.
```rust
use lino_objects_codec::{decode, format, LinoValue};
let text = "quoted \"value\"\nnext line";
let value = format::format_value_single_line(text);
assert!(!value.contains('\n'));
assert_eq!(decode(&value).unwrap(), LinoValue::from(text));
```
### Optional JSON bridge
The JSON bridge and public value helpers are introduced in 0.8.0. Enable
`features = ["serde_json"]` on the dependency. `LinoValue::from(Value)`
and `Value::try_from(LinoValue)` also accept borrowed inputs. Object insertion
order and the numeric text exposed by `serde_json::Number` survive conversion
and all three wire formats through a feature-gated `JsonNumber` variant. The
feature enables serde_json's `preserve_order` and `arbitrary_precision` settings.
The guarantee starts with the supplied `Value`; parsing can already normalize
original JSON spelling or discard duplicate keys.
`json::json_to_lino` and `json::lino_to_json` provide text conversions. Nulls and
empty containers are retained by default. Pass `JsonOptions { strip_empty: true }`
to `json_to_lino_with_options` or `from_json_with_options` to recursively remove
nulls and empty arrays/objects, while retaining empty strings, false and zero.
An entirely stripped root becomes null. Non-finite floats and duplicate LiNo
object keys return conversion errors. Run the complete example with
`cargo run --features serde_json --example json_bridge`.
The JSON number markers `(json-number "1.200")` and `(json_number 1.200)` are
Rust feature extensions; arbitrary-precision JSON numbers are not promised by
the other language implementations.
### Types
#### `LinoValue`
The main value type that can represent any serializable value:
```rust
pub enum LinoValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Array(Vec<LinoValue>),
Object(Vec<(String, LinoValue)>),
}
```
Helper methods:
- `LinoValue::object(iter)` - Create an object from key-value pairs
- `LinoValue::array(iter)` - Create an array from values
- `is_null()`, `as_bool()`, `as_int()`, `as_float()`, `as_str()`, `as_array()`, `as_object()` - Type checking and extraction
- `get(key)` - Get a value from an object by key
- `get_index(index)` - Get a value from an array by index
#### `CodecError`
Error type for codec operations:
```rust
pub enum CodecError {
ParseError(String),
DecodeError(String),
UnknownType(String),
}
```
### Functions
#### `encode(value: &LinoValue) -> String`
Encode a value to the readable, indented Links Notation format.
```rust
let value = LinoValue::Int(42);
let encoded = encode(&value);
assert_eq!(encoded, "42");
```
#### `encode_with_indent(value: &LinoValue, indent: &str) -> String`
Same as `encode()`, but with a custom indentation string (the default is two spaces).
#### `encode_line(value: &LinoValue) -> String`
Encode a value to the readable form on one line, so one record is one line of an
append-only log. An object names itself with the `o` marker, which is what keeps
`(key value)` unambiguous; a bare link on one line is always an array.
```rust
let value = LinoValue::object([("age", LinoValue::Int(30))]);
assert_eq!(encode_line(&value), "(o: (age 30))");
```
#### `decode_line(notation: &str) -> Result<LinoValue, CodecError>`
The exact inverse of `encode_line()`. Input spanning more than one line is
rejected, so two log records can never be merged into one value.
```rust
let decoded = decode_line("(o: (age 30))").unwrap();
assert_eq!(decoded, LinoValue::object([("age", LinoValue::Int(30))]));
```
#### `encode_compact(value: &LinoValue) -> String`
Encode a value to the single-line, base64 form used before version 0.3.
```rust
let value = LinoValue::String("hello".to_string());
assert_eq!(encode_compact(&value), "(str aGVsbG8=)");
```
#### `encode_obfuscated(value: &LinoValue) -> String`
Alias of `encode_compact()`, named after what the base64 form actually does to the text.
#### `decode(notation: &str) -> Result<LinoValue, CodecError>`
Decode Links Notation format to a value. The readable form, the single-line form
and the compact form are all accepted.
```rust
let decoded = decode("42").unwrap();
assert_eq!(decoded, LinoValue::Int(42));
let legacy = decode("(int 42)").unwrap();
assert_eq!(legacy, LinoValue::Int(42));
```
### `ObjectCodec`
For advanced use cases, you can create your own codec instance:
```rust
use lino_objects_codec::ObjectCodec;
let mut codec = ObjectCodec::new();
let encoded = codec.encode(&value);
let decoded = codec.decode(&encoded)?;
```
## Usage Examples
### Basic Types
```rust
use lino_objects_codec::{encode, decode, LinoValue};
// Null
let null = LinoValue::Null;
assert_eq!(encode(&null), "null");
// Boolean
let bool_val = LinoValue::Bool(true);
assert_eq!(encode(&bool_val), "true");
// Integer
let int_val = LinoValue::Int(42);
assert_eq!(encode(&int_val), "42");
// Float
let float_val = LinoValue::Float(3.14);
assert_eq!(encode(&float_val), "3.14");
// Special floats
let inf = LinoValue::Float(f64::INFINITY);
assert_eq!(encode(&inf), "Infinity");
let nan = LinoValue::Float(f64::NAN);
assert_eq!(encode(&nan), "NaN");
// Strings are quoted, not encoded
let str_val = LinoValue::String("hello".to_string());
assert_eq!(encode(&str_val), "\"hello\"");
// Numbers written as strings stay strings
let numeric = LinoValue::String("42".to_string());
assert_eq!(decode(&encode(&numeric)).unwrap(), numeric);
```
### Collections
```rust
use lino_objects_codec::{encode, decode, LinoValue};
// Array
let array = LinoValue::array([
LinoValue::Int(1),
LinoValue::Int(2),
LinoValue::Int(3),
]);
let encoded = encode(&array);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.as_array().unwrap().len(), 3);
// Object
let obj = LinoValue::object([
("name", LinoValue::String("Alice".to_string())),
("age", LinoValue::Int(30)),
]);
let encoded = encode(&obj);
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.get("name").unwrap().as_str(), Some("Alice"));
```
### Nested Structures
```rust
use lino_objects_codec::{encode, decode, LinoValue};
let data = LinoValue::object([
("users", LinoValue::array([
LinoValue::object([
("id", LinoValue::Int(1)),
("name", LinoValue::String("Alice".to_string())),
]),
LinoValue::object([
("id", LinoValue::Int(2)),
("name", LinoValue::String("Bob".to_string())),
]),
])),
("metadata", LinoValue::object([
("version", LinoValue::Int(1)),
("count", LinoValue::Int(2)),
])),
]);
let encoded = encode(&data);
let decoded = decode(&encoded).unwrap();
// Access nested values
let users = decoded.get("users").unwrap().as_array().unwrap();
assert_eq!(users.len(), 2);
```
### From Traits
`LinoValue` implements `From` for common Rust types:
```rust
use lino_objects_codec::LinoValue;
let null: LinoValue = ().into();
let bool_val: LinoValue = true.into();
let int_val: LinoValue = 42i64.into();
let float_val: LinoValue = 3.14f64.into();
let str_val: LinoValue = "hello".into();
let vec_val: LinoValue = vec![1i64, 2, 3].into();
let opt_val: LinoValue = Some(42i64).into();
let none_val: LinoValue = None::<i64>.into();
```
## How It Works
The codec encodes values using the [Links Notation](https://github.com/link-foundation/links-notation) format.
### Readable format (the default)
One `( )` construct carries both objects and arrays, at every level including
the root. Lines of the form `key value` make an object, bare-value lines make an
array:
```lino
(
type "RouterState"
server (
host "127.0.0.1"
port 18878
)
models (
"claude-haiku"
"claude-opus"
)
)
```
- Strings are double-quoted and written as text: `name "Alice"`
- Numbers, `true`, `false` and `null` are bare, so types survive a round trip
- `NaN`, `Infinity` and `-Infinity` are written as such
- An empty array is `()`; an empty object is `(` + newline + `)`
- A string is written as text whatever it holds: a newline stays a newline and
a tab stays a tab, so every word stays greppable
- A string containing the quote delimiter is written between a run of at least
three of them — `"""say "hi""""` — rather than by doubling the quote
- Only the characters this form cannot carry — a carriage return and the
remaining control characters — are percent-escaped, in a value marked on its
own as `(escaped "first%0D")`; everything around it stays readable, and
`(base64 "…")` written by earlier versions is still decoded
- A value that occurs more than once is written out every time: a shared
reference would make one record depend on another
Reading the format back requires `links-notation` 0.14 semantics, where a
parenthesis opens a nested indentation context.
### Compact format (`encode_compact`)
The previous single-line form, kept for compatibility and for cases where size
matters more than legibility:
- Basic types: `(int 42)`, `(str aGVsbG8=)`, `(bool true)`
- Strings are base64-encoded here, and only here: this is the one form that
asks for it by name, and `encode()` never reaches for it
- Arrays: `(array (int 1) (int 2) (int 3))`
- Objects: `(object ((str a2V5) (int 42)) ...)`
- Special floats: `(float NaN)`, `(float Infinity)`, `(float -Infinity)`
For structures with shared references or circular references, the compact form
uses object IDs:
- Format: `(obj_0: array ...)` or `(obj_0: object ...)`
- References: `obj_0`
`decode()` detects which of the two forms it is given, so previously written
files keep decoding.
## Debugging
Tracing is off by default. Turn it on to see what the codec does, either from
the environment or from code:
```bash
LINO_CODEC_DEBUG=1 cargo run --example basic_usage # 1, true, yes or on
```
```rust
use lino_objects_codec::debug;
debug::set_debug_enabled(Some(true)); // force on
debug::set_debug_enabled(None); // follow LINO_CODEC_DEBUG again
```
Trace lines are written to standard error, prefixed with `[lino-codec]`. The
same switch and the same `LINO_CODEC_DEBUG` variable exist in the JavaScript,
Python and C# implementations.
## Development
```bash
# Run tests
cargo test
# Run example
cargo run --example basic_usage
# Build documentation
cargo doc --open
```
## License
This project is licensed under the Unlicense - see the [LICENSE](../LICENSE) file for details.