eon 0.2.0

Use the Eon config format with serde
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
# Eon - The simple and friendly config format
This repository contains the definition of _Eon_, a simple config format designed for human editing.

Eon uses the `.eon` file ending.

Eon is aimed to be a replacement for [Toml](https://toml.io/en/) and Yaml.

This repository also contains a Rust crate `eon` for using Eon with `serde`, and a `eonfmt` binary for formatting Eon files.


## Overview
```yaml
// Comment
string: "Hello Eon!"
list: [1, 2, 3]
map: {
    boolean: true
    literal_string: 'Can contain \ and "quotes"'
}
any_map: {
    42: "This key is an integer"
	[]: "This key is an empty list"
}
hex: 0xdead_beef
special_numbers: [+inf, -inf, +nan]

// Strings come in four flavors:
basic_strings: [
	"I'm a string."
	"I contain \"quotes\"."
	"Newline:\nUnicode: \u{262E} (☮)"
]
multiline_basic_strings: [
	"""\
		It was the best of strings.
		It was the worst of strings."""

	// The above is equivalent to:
	"It was the best of strings.\n\t\tIt was the worst of strings."
]
literal_strings: {
	// What you see is what you get:
	windows_path: 'C:\System32\foo.dll'
	quotes: 'I use "quotes" in this string'
	regex: '[+\-0-9\.][0-9a-zA-Z\.+\-_]*'
}
multiline_literal_strings: {
	python: '''
    # The first newline is ignored, but everything else is kept
    def main():
        print('Hello world!')
    '''
}
```

### Design goals
- **Familiar**: strongly resembles JSON
- **Clean**: forgoes unnecessary commas and quotes
- **Powerful**: supports arbitrary map keys and sum types (e.g. Rust enums)


### Main differences from JSON
* No need to wrap entire document in `{}`
* Quotes around map keys are optional for identifiers
* Map keys can be any value (not just string)
* Commas are optional
* Eon adds:
    * `// Comments`
    * Special floats: `+inf`, `-inf`, `+nan`
    * Named sum-type variants


## Formatter
You can format any Eon file using the `eonfmt` binary

```sh
cargo install eonfmt
eonfmt *.eon
```


## Why another config format?
I wanted a format designed for human eyes with
- Indented hierarchy using `{ }` and `[ ]` (like JSON, C, Rust, …). Rules out YAML and TOML.
- No top-level `{ }` wrapping the whole file. Rules out JSON5, RON, and others.

### Why not [JSON5]https://json5.org/?
JSON5 is _almost_ great, but requires wrapping the whole file in an extra `{ }` block, and indent that. That's too ugly for me.
It also has a bunch of unnecessary commas between values.

RON has the same problem.

### Why not [RON]https://github.com/ron-rs/ron?
The goal of RON is to perfectly map the Rust datatypes, which is cool, but it means it leans more towards being verobse and complex while Eon wants to be lean and simple.

### Why not [Toml]https://toml.io/en/?
Toml is a very nice and clean language.
However, unlike almost every other programming language known, it does not use any indentation to aid the reader, leading to very confusing hierarchies.
This means that when (visually) scanning a Toml document it's hard to figure out where one section starts and another ends.

It also means Toml is a bad candidate whenever you have deeply nested structures.

The `[[array.of.tables]]` syntax is also quite confusing to newcomers.

### Why not [YAML]https://yaml.org/?
Yaml is clean, but over-complicated, inconsistent, and filled with foot guns. [It is known](https://ruudvanasseldonk.com/2023/01/11/the-yaml-document-from-hell).


## Performance
The Eon language (and library) are designed for config files that humans edit by hand.

There is nothing in the Eon spec that would not make it as fast (or as slow, depending on your perspective) as JSON, but the library has not been optimized for performance (no crazy SIMD stuff etc).
It still parses a chunky 1MB file in under 10ms on an M3 MacBook.

I would not recommend using Eon as a data transfer format. For that, use a binary format (like MsgPack or protobuffs), or JSON (which has optimized parser/formatters for every programming language).


## Roadmap
Future work, which I may or may not get to (contributions welcome!)

### Additional tools
- VSCode extension for
    - Syntax highlighting
    - Formatting

### Extending the spec
- Add special types?
    - ISO 8601
        - datetimes
        - local times
        - Durations? But ISO 8601 durations are so ugly
    - UUID?
- Hex floats?


## Eon specification
An Eon document is always encoded as UTF-8.

A document can be one of:
- A single value, e.g. `42` or `{foo: 1337, bar: 32}`
- The contents of a Map, e.g. `foo: 42, bar: 32` (this is syntactic sugar so you don't have to wrap the document in `{}`)
- The contents of a List, e.g. `32 46 12` (useful for a stream of values, e.g. like [ndjson]https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson)

Commas are optional in Eon, so `[1, 2, 3]` is the same as `[1  2  3]`.
By convention, commas are included when multiple values are on the same line, but omitted for multi-line maps and lists.

Whitespace is not significant (other than as a token separator).

By convention, we indent everything wrapped in `[]`, `{}`, `()`.

Comments are prefixed with `//`.

### Supported types
An Eon value is one of:

#### `null`
A special `null` value, signifying nothingness.

#### Boolean
`true` or `false`.

#### Number
Numbers in Eon can have an optional sign (`+` or `-`) followed by either:
- a decimal (`42`)
- a hexadecimal (`0xbeef`, case insensitive)
- a binary (`0b0101`)
- a decimal (`3.14`, `6.022e23` etc)

Numbers can use `_` anywhere in them as a visual separator, e.g. `123_456` or `0xdead_beef`.

There is not explicit bounds on the precision and magnitude of supported numbers in an Eon file,
but the `eon` crate is currently only support integers in the `[-2^127, 2^128)` range, and decimals are limited to the precision of `f64`.

Eon also support the special values:
- `+nan`: [IEEE 754 `NaN`]https://en.wikipedia.org/wiki/NaN
- `+inf`: positive infinity
- `-inf`: negative infinity

Note that these special values MUST be prefixed with a sign (they are not keywords like `true/false/null` are).

#### Strings
Text in Eon comes in four flavors:
- `"basic string"`
- `"""multiline basic string"""`
- `'literal string'`
- `'''multiline literal string'''`

##### `"Basic strings"`
Basic strings uses double-quoted, and can contain escape sequences:

```py
"I'm a string."
"I contain \"quotes\"."
"Newline:\nUnicode: \u{262E} (☮)"
```

#### `"""Multi-line basic strings"""`
When you have long text it can be useful to have a string span multiple lines, using triple-quotes.

Escape sequences still work, and a line ending with `\` removes the newline and any whitespace at the start of the next line.

This means these two strings are equivalent:

```py
str1: """\
    It was the best of strings.
    It was the worst of strings."""

str2: "It was the best of strings.\n\tIt was the worst of strings."
```


#### `'Literal strings'`
Literal strings uses single quotes, and no escaping is performed. What you see is exactly what you get:

```py
windows_path: 'C:\System32\foo.dll'
quotes: 'I use "quotes" in this string'
regex: '[+\-0-9\.][0-9a-zA-Z\.+\-_]*'
```

There is no way to put a single quote inside of a literal strings, but you can in a…

#### `'''Multi-line literal strings'''`
A multi-line literal string uses three single quotes. Within that, everything is taken verbatim, except that the very first newline (if any) is ignored:

```py
quote_re: '''(["'])[^"']*$1'''
python: '''
# The first newline is ignored, but everything else is kept
def main():
    print('Hello world!')
'''
```


#### List
Lists are written as `[ … ]`, with _optional_ commas between values.
Usually the commas are omitted for lists that span multiple lines,
and included for lists that are on a single line.

```yaml
long_list: [
    2
    3
    5
    7
    11
]

short_list: [1, 2, 3]

also_fine: [
    1,
    2,
    3,
]

very_terse: [1, 2, 3]
```

A list can contain any Eon value (including other lists).

#### Map
Maps are written as `{ key: value, … }`, again with optional commas between key-value pairs.
Usually the commas are omitted for maps that span multiple lines,
and included for maps that are on a single line.

Maps are used to represent either a record type (like a `struct`) or a table type (e.g. a hash map).

For instance, say you have a hash map for looking up country code based on the name of the country.
This can be serialized to Eon as:

```C
country_codes: {
    "United States": 1
    "France": 33
    "United Kingdom": 44
    "Sweden": 46
    "Germany": 49
    "Australia": 61
    "Japan": 81
    "China": 86
    "India": 91
}
```

Both keys and values of an Eon map can be any Eon value.
This is significantly different from e.g. JSON, where keys can only be strings.
So, a reverse map (from code to country name) can we written as:


```yaml
country_from_code: {
    1:  "United States"
    33: "France"
    44: "United Kingdom"
    46: "Sweden"
    49: "Germany"
    61: "Australia"
    81: "Japan"
    86: "China"
    91: "India"
}
```

You can even use other maps as keys:

```yaml
complex_map: {
    {"foo": 42, "bar": "mushroom"}: "Yes, this is fine"
}
```

You are allowed to omit the quotes around map keys (NOT values!) when the keys are _identifiers_.

An identifier is defined in Eon as any string matching the regex `[a-zA-Z_][a-zA-Z0-9_]*`.
This definition matches most programming languages (e.g. what is allowed as a variable name in C).

This means these two are equivalent:

```yaml
explicit: {
    "a": 1
    "b": 2
}
same: {
    a: 1
    b: 2
}
```

The convention is to use quotes for general strings (e.g. when representing a `HashMap<String, …>`
and to use identifiers when representing struct fields.

Examples of what is and isn't allowed:

```yaml
snake_case: true       // OK!
kebab-case: false      // ERROR! 'kebab-case' is not an identifier, and must be quoted
"kebab-case": true     // OK!
string: Hello          // ERROR! "Hello" should be in quotes
key: null              // OK! 'null' is a special value (it's NOT a string)
42: "forty-two"        // OK! Maps the integer 42 to a string
"42": "forty-two"      // OK! Maps the string "42" to a string (which is different from the above!)
true: "confusing"      // ⚠️ Confusing, but OK. Uses a boolean as key (not a string!).
"true": "fine"         // OK! Uses the string "true" as key
```

#### `null/true/false` as map keys
The last two lines in the above example show that you need to be careful when using key names that matches one of the three keywords (`true/false/null`). This is is a (small) footgun in Eon, but hopefully these key names are rare (they are already forbidden identifiers in many programming languages).

Alternative designs I've considered:
- Always require quotes for map keys (like JSON). Con: very ugly.
- Only allow strings as map keys. Con: can't express general maps (limiting).
- Only allow identifiers as map keys (like a Rust `struct`). Con: can't serialize a `HashMap<String, …>`.
- Change `null/true/false` to something else (e.g. `%null`, `%true`, `%false`). Would be both surprising and ugly.
- Forbid using unquoted `null/true/false` as map keys. Con: can't serialize `HashMap<bool, …>` or a general `HashMap<Value, …>`. Feels arbitrary.

### Named sum-type variants
Let's first consider a simple `enum`, like one you would find in C or Java:

```c
enum Side {
    Left,
    Middle,
    Right,
}
```

The variants are encoded as strings in Eon, e.g.

```yaml
side: "Middle"
```

In other words, there is no difference between a string and an enum variant, as far as Eon is concerned.

Now consider this more complicated Rust `enum`:

```rs
enum Color {
    Black,
    Gray(u8),
    Hsl(u8, u8, u8),
    Rgb { r: u8, g: u8, b: u8 },
}
```

Here a simple string will not suffice, as some of the enum variants contain data.

There are several competing techniques of encoding this in JSON (external tagging, internal tagging, adjaceny tagging, …) all with their own shortcomings.

In Eon, enum variants are written as `"Variant"(data)`.

So different values for the above choice would be written as:

- `"Black"` (equivalent to `"Black"()`)
- `"Gray"(128)`
- `"Hsl"(0, 100, 200)`
- `"Rgb"({r: 255, g: 0, b: 0})`

#### Digression: why this syntax for sum types?

Why the quotes, and not just `Black`, `Gray(128)`, etc?
Consider this hypothetical Eon file:

```yaml
color: Black
name: Emil
```

Is `name` really a multiple-choice enum? Maybe. Maybe not.
But the Eon parser wouldn't know, so must accept it.
And this will leave the door open to weirdness where _some_ strings need quotes and not others (and we'll end up similar to YAML).
So instead we explicitly forbid the above, and always require quotes for strings (…except for map keys that are identifiers).