Code View

fson / source / fson-1.1.0.0 / docs / notation.md
Preview
# The FSON Notation

FSON is a superset of JSON designed for hand-edited files such as
configurations: every valid JSON document is a valid FSON document, and
FSON adds the things humans need when they maintain files by hand —
comments, unquoted keys, and a way to switch a member off without deleting
it.

Since v0.9.0.0, FSON is also a pragmatic superset of
[JSON5](https://json5.org/): single- and double-quoted strings, trailing
commas, extended number literals (hex, leading/trailing dot, `Infinity`/
`-Infinity`/`NaN`), and extended whitespace are all accepted. See
"FSON and JSON5" below for the exact relationship and its one deliberate
exception.

The normative grammar lives in [`docs/notation/`](notation/) as railroad
diagrams. Each rule is stored as a `.rrd` source (the
[railroad-diagrams](https://github.com/tabatkins/railroad-diagrams) DSL)
with a rendered `.svg` next to it. The numeric prefix in a file name is
ordering only; the rest of the name is the non-terminal the file defines.
This document is the prose companion to those diagrams — when in doubt,
the diagrams win.

## Document structure

A FSON file is a single **value**, optionally surrounded by comments and
whitespace (`00005-fson_file`). Through v0.8.0.0 that value had to be an
object; since v0.9.0.0 it may be any value — an object is still by far the
most common choice for configuration files, but a bare array, string,
number, boolean, or `null` is also a valid FSON file.

```
// header comments may precede the value
{
  ...members...
}
// and footer comments may follow it
```

## Comments

Two comment forms exist anywhere whitespace may appear
(`10000`–`10002`, `10101-skip_comments_and_blanks`):

- `// single line` — runs to the end of the line.
- `/* block */` — may span lines and **may nest**: `/* outer /* inner */
  still outer */` is one comment. This makes commenting-out regions that
  already contain block comments safe.

Comments are first-class citizens of the FSON data model: the library
records where each comment is attached (before a member, at the end of its
line, before a closing brace, or around the document) and writes every one
of them back. A read–modify–write cycle never loses a comment.

## Objects and members

An object is a `{ }`-delimited, comma-separated list of members
(`00010-object`). A trailing comma after the last member is allowed (since
v0.9.0.0). A member is:

```
[--] key : value
```

### Keys

A key is either a **string** (any text, quoted — `00040-string`) or a bare
**name** (`00200-name`): a letter followed by letters and digits,
optionally separated by *single* underscores. A name never starts or ends
with an underscore and never contains two in a row.

| Valid names | Invalid as names (quote them instead) |
|---|---|
| `host`, `maxRetries`, `a_1_b2` | `_private`, `name_`, `a__b`, `1st`, `my key` |

This is narrower than JSON5's bare-key grammar, which follows the full
ECMA-262 `IdentifierName` production (so JSON5 additionally accepts keys
like `$foo`, `_x`, or Unicode identifier characters bare). FSON does not
widen its `name` grammar to match — this is the one place a JSON5 file
using such a key needs its key quoted to also be valid FSON. See "FSON and
JSON5" below.

### Disabled members: the `--` prefix

Prefixing a member with `--` *disables* it:

```
--debug: true,
--"verbose logging": true,
```

A disabled member is invisible to queries — for the consuming program it
does not exist — but it is preserved verbatim and written back with its
prefix. Think of it as a structured, machine-readable alternative to
commenting a line out. The prefix works with quoted and bare keys alike.
A disabled member may even share its name with an active one.

`--` disables the **whole definition that follows it**, not just its first
line. A disabled member whose value spans several lines (an array, a
nested object, or a multi-line `|`-block) has that entire value parsed and
skipped — the parser does not process the first line and then try to make
sense of the rest:

```
--dizi: [
  1, 2, 3
],
```

disables the complete `dizi` array. The same applies to a **keyless**
multi-line `|`-block: `--` before a `|`-block disables the entire block
(all its contiguous `|`-lines), and parsing resumes at the next member.

```
str: | active
--|this whole
  |block is
  |disabled
pi: 3.14
```

Here `str` and `pi` are active; the three `|`-lines in between form one
disabled, keyless block — preserved structurally, invisible to queries,
and written back as `--|…` lines (no key, no `:`). This is the one place a
value may appear without a key, and only when disabled.

When converting to JSON or JSON5 (`FsonWriter::Dialect::Json`/`Json5`),
a disabled member has no `--` syntax to fall back on; see
`docs/reference_manual.md` for `DisabledMemberPolicy`, which controls
whether it becomes a comment, an ordinary member, or is dropped.

## Include directives: `%include`

A `%include` directive stands where a member stands, letting one file
draw defaults from another:

```
{
  %include "base.fson",              // merge base's members as defaults
  %include "secrets.fson" as vault,  // bind secrets' root under "vault"
  timeout: 60                        // host value
}
```

Two forms:

| Form | Effect |
|---|---|
| `%include "path"` | **Merge.** The target's root-object members are added to the including object as defaults. |
| `%include "path" as name` | **Alias.** The target's whole root value is bound under the key `name` (the target root need not be an object). |

The rules:

- **Host wins.** A key already present in the including object is not
  overwritten by a merged default; an alias key already present is left
  untouched.
- **Shallow merge.** Only top-level keys are compared. A host `logging`
  object wholly shadows the base's `logging` — nested objects are not
  deep-merged.
- **Relative paths** are resolved against the directory of the file that
  contains the directive, not the process working directory, so a file
  tree stays relocatable.
- **Transitive.** A target's own `%include` directives resolve before its
  members are merged in, so defaults chain through.
- **Cycles** (`a` includes `b` includes `a`) are reported as an error and
  broken; the document still loads with the non-cyclic content intact.
- **Disabled directive.** `--%include "…"` is preserved structurally but
  not resolved — its defaults are not applied — mirroring `--` members.

Directives are structural: on write-back in the FSON dialect the
`%include` line is reproduced verbatim, and the values it merged in are
**not** written into the host file (they live in the included file).
Programmatic overrides via the query API are written only to the host
file; see `docs/reference_manual.md` for the provenance API
(`isLocal`/`provenanceOf`/`provenanceMap`) that reports where a resolved
value came from.

Why `%`? The `@` character is reserved for the FFS `@placeholder@`
mechanism, and `#` is deliberately left unused (FSON needs no third
comment syntax beyond `//` and `/* */`). `%` collides with nothing in the
FSON grammar and carries a template/directive connotation.

## Values

A value (`00030-value`) is one of: `string`, `number`, `object`, `array`,
`true`, `false`, `null`.

- **Strings** (`00040-string`) are delimited by either `"` or `'`; the
  closing delimiter must match the opening one, so an unescaped quote of
  the *other* kind is just a literal character. The escape set
  (`00045-escape_sequence`) is `\" \' \\ \/ \b \f \n \r \t \v \0`,
  `\xXX` (two hex digits), `\uXXXX` (UTF-16 code units; surrogate pairs
  encode characters beyond U+FFFF), a line-terminator sequence right
  after the backslash (a *line continuation*: the break is removed from
  the decoded string, letting a string literal span source lines), and —
  as a deliberate FSON simplification of the stricter JSON5 rule — any
  other character after `\` is simply itself, with no error. Raw U+2028
  (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) may also appear
  unescaped inside a string. The library remembers nothing about which
  quote character was used on read; strings are always written back
  double-quoted.
- **Multi-line strings** (`00040-string`, third form) are written as a
  block of `|`-lines instead of a quoted literal:

  ```
  greeting: |Hello,
            |world!
  ```
  is the string `"Hello,\nworld!\n"`. Each source line begins with `|`
  (the `|` is not part of the value); the line's content runs to the end
  of the line, and a `\n` is appended for **every** line, the last
  included. Escapes are decoded as in a quoted string. A `\` immediately
  before the line break is a *continuation*: that break is dropped, so the
  next `|`-line's content joins without a newline. `|`-lines must be
  contiguous — only whitespace (and, in principle, a `/* */` block comment
  closing on the same line before the `|`) may precede the `|`; a blank
  line, a `//` comment, or any other content ends the block. Because a
  `|`-line runs to end-of-line, the separating comma is **optional** after
  a block (a comma would otherwise be read as content). On write-back the
  block form is preserved: the writer re-emits one `|`-line per newline at
  the value's indentation (presentation is normalised, content is exact).
  The `|` form exists only in FSON; under the JSON/JSON5 writer dialects
  such a value is written as an ordinary quoted string.
- **Numbers** (`00050-number`) are an optional sign (`-` or `+`) followed
  by `Infinity`, `NaN`, a hex integer (`00046-hex_integer`: `0x`/`0X` plus
  one or more hex digits), or a decimal number (`00047-decimal_number`):
  the classic JSON shape (no leading zeros, optional fraction, optional
  exponent), a leading-dot form (`.5`), or a trailing-dot form (`5.`). The
  library remembers each number exactly as written, so `1.50` stays
  `1.50`, `0xFF` stays `0xFF`, and `Infinity` stays `Infinity` on rewrite.
- **Arrays** (`00020-array`) are `[ ]`-delimited, comma-separated values.
  A trailing comma after the last element is allowed (since v0.9.0.0).

## Whitespace

Anywhere whitespace may appear (`10100-white_space_character`), FSON
recognises the ASCII set (space, `\r`, `\n`, `\t`, `\v`, `\f`) plus, since
v0.9.0.0, NBSP (U+00A0), the BOM/ZWNBSP (U+FEFF), U+2028, U+2029, and the
rest of Unicode category Zs.

## FSON and JSON5

> FSON, JSON5'in pragmatik bir supersetidir; tek istisnası nested block
> yorumlardır — JSON5 onları yasaklar, FSON destekler. Yorumsuz veya
> yalnız tek-derinlikli `/* */` kullanan her JSON5 dosyası geçerli
> FSON'dur.

In English: FSON is a pragmatic superset of [JSON5](https://json5.org/):
every JSON5 file that is comment-free, or uses only single-depth `/* */`
comments, is a valid FSON file. Its only exception is nested block
comments — JSON5 forbids them, FSON supports them (see "Comments" above)
— and the one narrower spot noted under "Keys" above (FSON's bare-key
grammar does not go as far as JSON5's full `IdentifierName` production).
Everywhere else, FSON's grammar is JSON5's grammar plus FSON's own
additions (`--` disabled members, nested block comments, the
dotted-string path API, and so on).

## A complete example

```
// service configuration
{
  service_name: 'billing',
  database: {
    host: "db.internal.example", // production endpoint
    --user: "root", /* disabled credential */
    "connection timeout": 2.50,
  },
  retries: [1, 2, 5,],
  max_connections: 0x40,
  timeout: Infinity,
  --debug: false
  /* end of configuration */
}
```

More examples live in [`examples/`](../examples/).