lino-objects-codec 0.8.0

A library to encode/decode objects to/from links notation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# lino-objects-codec (Rust)

[![Rust CI](https://github.com/link-foundation/lino-objects-codec/actions/workflows/rust.yml/badge.svg)](https://github.com/link-foundation/lino-objects-codec/actions/workflows/rust.yml)
[![crates.io](https://img.shields.io/crates/v/lino-objects-codec?label=crates.io&logo=rust)](https://crates.io/crates/lino-objects-codec)
[![docs.rs](https://img.shields.io/docsrs/lino-objects-codec?label=docs.rs)](https://docs.rs/lino-objects-codec)
[![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](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

| Function | Output |
| --- | --- |
| `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.