Code View

fson / source / fson-1.1.0.0 / docs / reference_manual.md
Preview
# fson Library Reference Manual

All types live in namespace `fedem::fson`; diagnostics reuse
`fedem::parser::ParseError` from the cparse library. Headers are included
as `#include "fson/<Name>.hh"`. The API is C++17-compatible.

Ownership model: values are owned through `std::unique_ptr<Value>`. The
polymorphic types are neither copyable nor movable — duplicate with
`clone()`, which deep-copies values *and* their attached comments.

---

## Comment, CommentList — `fson/Comment.hh`

A single comment, stored without its delimiters.

| Member | Description |
|---|---|
| `enum class Kind { SingleLine, MultiLine }` | `// …` vs `/* … */` |
| `Comment( Kind, std::string text )` | construct |
| `getKind()` | the kind |
| `getText()` / `setText(text)` | text between the delimiters (nested `/* */` pairs appear verbatim in a MultiLine text) |

`CommentList` is `std::vector<Comment>`. Comments attach to model nodes as
**leading** (before a member/element), **trailing** (end of the member's
line), **dangling** (before `}`/`]`) and document **header**/**footer**
lists; all are exposed by reference and freely editable.

---

## Value — `fson/Value.hh`

Abstract base of every FSON value.

| Member | Description |
|---|---|
| `enum class Type { Null, Boolean, Number, String, Array, Object }` | |
| `getType()` | dynamic type |
| `clone() → unique_ptr<Value>` | deep copy incl. comments |
| `isNull() … isObject()` | type predicates |
| `asNull() … asObject()` | checked casts; `nullptr` on mismatch (const and non-const overloads, no `dynamic_cast` needed) |

### Null — `fson/Null.hh`
The literal `null`. Default-constructible, no further state.

### Boolean — `fson/Boolean.hh`
| `Boolean( bool = false )`, `getValue()`, `setValue(bool)` |

### Number — `fson/Number.hh`
Stores the parsed `double`, the original source lexeme, **and** (since
v0.9.0.0) a `Kind` recording which lexical form it came from.

| Member | Description |
|---|---|
| `enum class Kind { Decimal, Hex, Infinity, NegativeInfinity, NaN }` | the lexical form |
| `Number( double )`, `Number( long long )` | lexeme generated (shortest round-trip via `std::to_chars` for finite values); a non-finite double sets the matching `Kind` and lexeme (`"Infinity"`/`"-Infinity"`/`"NaN"`) instead of throwing |
| `Number( double, std::string lexeme, Kind = Kind::Decimal )` | parser entry; lexeme must satisfy the number grammar |
| `asDouble()` / `asInteger()` | numeric access; `asInteger` truncates, and clamps to `LLONG_MAX`/`LLONG_MIN`/`0` for `+Infinity`/`-Infinity`/`NaN` rather than invoking undefined behaviour |
| `isIntegral()` | `Decimal`: no `.`/`e`/`E` in the lexeme; `Hex`: always true; `Infinity`/`NegativeInfinity`/`NaN`: always false |
| `getKind()` | the lexical form |
| `getLexeme()` | exact source spelling — written back verbatim in the `Fson` and `Json5` dialects |
| `setValue( double )` / `setValue( long long )` | sets value, regenerates the lexeme, and updates `Kind`; accepts non-finite doubles since v0.9.0.0 |

### String — `fson/String.hh`
Holds the **decoded** character sequence (escapes resolved, `\uXXXX`
decoded to UTF-8). The writer re-escapes on output.

| `String( std::string = "", Form = Form::Quoted )`, `getValue()`, `setValue(std::string)` |
| `enum class Form { Quoted, Multiline }`, `getForm()`, `setForm(Form)` | how the value is written back |

`Form::Multiline` marks a value that was written as a `|`-block (see
`docs/notation.md`). The writer re-emits it as a block **only** in the
Fson dialect and **only** when the content ends in `\n` (every block line
adds one); otherwise, and under the JSON/JSON5 dialects, it is written as
an ordinary quoted string. Presentation (indentation, any author
`\`-continuation) is normalised; decoded content is preserved exactly.

---

## Array, Element — `fson/Array.hh`

`Element` is one array slot with its trivia. Move-only; `clone()` to copy.

| Element member | Description |
|---|---|
| `Element( unique_ptr<Value> )` | throws `std::invalid_argument` on null |
| `getValue()` / `setValue(unique_ptr<Value>)` | the payload |
| `getLeadingComments()` | `CommentList&` |
| `getTrailingComment()` | `std::optional<Comment>&` |

`Array` is an ordered element sequence. Index errors throw
`std::out_of_range`.

| Array member | Description |
|---|---|
| `size()`, `empty()` | element count |
| `at( index )` | value at index |
| `getElement( index )` | element (for trivia access) |
| `append( value )`, `insert( index, value )` | return a reference to the stored value |
| `erase( index )`, `clear()` | removal |
| `getDanglingComments()` | comments before `]` |

---

## Key, Member, Object — `fson/Object.hh`

### Key
| Member | Description |
|---|---|
| `enum class Form { Quoted, Bare }` | written with or without quotes |
| `Key( Form, std::string )`, `Key::quoted(name)`, `Key::bare(name)` | `bare()` throws `std::invalid_argument` for grammar-invalid names |
| `static isValidName( text )` | the name grammar: letter start, single `_` separators, no leading/trailing `_` |
| `getForm()`, `getName()` | |

### Member
One key/value pair with trivia and the disabled flag. Move-only; `clone()`.

| Member | Description |
|---|---|
| `Member( Key, unique_ptr<Value>, bool disabled = false )` | |
| `getKey()` / `setKey(Key)` | |
| `isDisabled()` / `setDisabled(bool)` | the `--` flag |
| `getValue()` / `setValue(...)` | |
| `getLeadingComments()`, `getTrailingComment()` | trivia |

### Object
Ordered member list with **two access levels**.

**Query API — disabled members are invisible:**

| Member | Description |
|---|---|
| `size()`, `empty()` | active members only |
| `contains( name )` | |
| `find( name ) → Value*` | `nullptr` when absent |
| `at( name ) → Value&` | throws `std::out_of_range` |
| `set( Key, value )` / `set( name, value )` | replaces the active member of that name or appends a new one; the string overload picks `Bare` when the name is grammar-valid, else `Quoted`; never touches a disabled member |
| `remove( name )` | removes the active member only |
| `disable( name )` / `enable( name )` | flip the flag; `enable` fails while an active member of the same name exists |

**Member API — full structural access (parser/writer level):**

| Member | Description |
|---|---|
| `memberCount()` | including disabled members |
| `getMember( index )` | throws `std::out_of_range` |
| `appendMember( Member )` | returns the stored member |
| `eraseMember( index )` | |
| `getDanglingComments()` | comments before `}` |

`Member` additionally exposes provenance set by include resolution:
`getOriginFile()` (empty for a host member; the source file for a
merged-in one), `getOriginLocalPath()` (the value's path within that
file), `setOrigin( file, localPath )`, and `isMerged()`.

**Include API — `%include` directives (`fson/Include.hh`):**

Directives are structural nodes kept in source order alongside members.
The `Include` class carries `getPath()`, `hasAlias()`/`getAlias()`,
`isDisabled()`/`setDisabled()`, leading/trailing comments, and `clone()`.

| Member | Description |
|---|---|
| `includeCount()` | number of `%include` directives in this object |
| `getInclude( index )` | throws `std::out_of_range` |
| `appendInclude( Include )` | returns the stored directive |
| `eraseInclude( index )` | |
| `entryCount()` / `getEntry( pos )` | the object body as an ordered list of `Entry{ EntryKind kind, size_t index }` — `kind` is `Member` or `Include`, `index` points into the respective list; used by the writer to round-trip interleaved order |

**Provenance API — where a resolved value comes from:**

After `Fson::load` resolves `%include` directives, a query path may
address a host member or one merged/aliased in from an included file.
Write policy is **host-only** — `setPath` on a merged value promotes it
to a host member, never editing the included file — so these queries let
a caller instead locate the origin file when it wants to edit it there.

| Member | Description |
|---|---|
| `isLocal( path )` | `true` when the path resolves to a member physically in the host file |
| `provenanceOf( path ) → optional<Provenance>` | `nullopt` when the path resolves to nothing; otherwise `{ queryPath, sourceFile, localPath, isHost }` — `sourceFile` empty ⇒ host; `localPath` differs from `queryPath` for aliases |
| `provenanceMap() → vector<Provenance>` | provenance of every active top-level member |

**Path API — dotted-string addressing** (also available on `Document`):

Path syntax: keys separated by `.`, array elements as `[N]` suffixes —
`"database.host"`, `"retries[2]"`, `"servers[0].port"`, `"matrix[1][2]"`.
Only `.`, `[`, `]` are special; keys containing them cannot be addressed.
Disabled members are invisible. Malformed paths resolve to nothing.

| Member | Description |
|---|---|
| `findPath( path ) → Value*` | `nullptr` when absent/malformed |
| `containsPath( path )` | |
| `atPath( path ) → Value&` | throws `std::out_of_range` |
| `setPath( path, value ) → Value*` | replaces or appends; creates missing intermediate **objects**; never creates/grows arrays; never resurrects disabled members; on failure returns `nullptr` and leaves the document untouched |
| `removePath( path )` | removes an active member or erases an array element |
| `getString( path, fallback = "" )` | fallback on missing path or non-String |
| `getInteger( path, fallback = 0 )` | any Number; truncates |
| `getDouble( path, fallback = 0.0 )` | any Number |
| `getBoolean( path, fallback = false )` | Booleans only |

---

## Document — `fson/Document.hh`

A complete FSON file: header comments + a never-null root `Value` +
footer comments. Non-copyable, non-movable. Through v0.8.0.0 the root was
always an `Object`; since v0.9.0.0 (JSON5-superset decision A.2a) it may
be any value.

| Member | Description |
|---|---|
| `Document()` | empty root object |
| `getRoot() → Object&` | throws `std::out_of_range` when the root is not an `Object` (kept for source compatibility with object-rooted documents — see the migration note in `CHANGELOG.md`) |
| `getRootObject() → Object*` | `nullptr` when the root is not an `Object`, instead of throwing |
| `getRootValue() → Value&` | the root, whatever its type |
| `setRoot( unique_ptr<Value> )` | throws `std::invalid_argument` on null; a `unique_ptr<Object>` still converts implicitly, so existing call sites are unaffected |
| `getHeaderComments()`, `getFooterComments()` | `CommentList&` |
| *path API* | every Object path/getter method above, forwarded through `getRootObject()`: `findPath`/`setPath` return `nullptr`, `containsPath`/`removePath` return `false`, the typed getters return their fallback, and `atPath` throws `std::out_of_range` whenever the root is not an `Object` |

---

## FsonParser — `fson/FsonParser.hh`

Recursive-descent parser built on `fedem::parser::Parser`.

| Member | Description |
|---|---|
| `parseFile( filename ) → unique_ptr<Document>` | null **only** when the file cannot be opened; parse errors still yield a (partial) document |
| `errors() → vector<ParseError> const&`, `hasErrors()` | inherited diagnostics collector |

Behavior:
- **Collect-and-continue**: on a syntax error the parser records a
  diagnostic and resynchronises at the next `,`/`}`/`]`; a failed member
  value becomes a `Null` placeholder. Always check `hasErrors()` before
  trusting or re-writing the document.
- **Duplicate active keys** are an error (`Kind::Faulty`); the duplicate
  is still stored structurally. Disabled duplicates are legal.
- The document root may be any value (v0.9.0.0); see "Document" above.
- A trailing comma is accepted in object and array bodies (v0.9.0.0).
- Strings may be single- or double-quoted; the escape set includes
  `\xXX`, `\v`, `\0`, and a line-continuation (`\` + line terminator);
  any other character after `\` is itself, with no error (v0.9.0.0).
- Numbers accept a leading `+`, `0x`/`0X` hex literals, a leading or
  trailing `.`, and the literal tokens `Infinity`/`-Infinity`/`NaN`
  (v0.9.0.0); see `fson/Number.hh`'s `Kind`.
- Whitespace additionally recognises NBSP, the BOM, U+2028/2029, and the
  rest of Unicode category Zs (v0.9.0.0).
- `\uXXXX` decodes to UTF-8 (surrogate pairs combined; invalid sequences
  are reported and replaced with U+FFFD).
- Comments are captured with their attachment points; a comment starting
  on the same line as a value's end becomes that member's/element's
  trailing comment. Nested block comments remain supported (the one
  deliberate exception to the JSON5-superset claim — see
  `docs/notation.md`).

`ParseError` fields (from `cparse/Parser.hh`): `kind` (`Syntax`, `Type`,
`Faulty`, `Internal`, `Warning`), `message`, `filename`, `line`, `col`.

---

## FsonWriter — `fson/FsonWriter.hh`

Pretty-printing serializer with three dialects.

| Member | Description |
|---|---|
| `enum class Dialect { Fson, Json, Json5 }` | output dialect, default `Fson` |
| `setDialect( Dialect )` / `getDialect()` | |
| `enum class DisabledMemberPolicy { ConvertToComment, Keep, Drop }` | governs `--` members in the `Json`/`Json5` dialects, default `ConvertToComment`; no effect on `Fson`, which always shows them literally |
| `setDisabledMemberPolicy( DisabledMemberPolicy )` / `getDisabledMemberPolicy()` | |
| `setKeepDisabledMembers( bool )` / `getKeepDisabledMembers()` | deprecated compatibility shim over `DisabledMemberPolicy`: `true` ↔ `Keep`, `false` ↔ `ConvertToComment` |
| `setPreferBareKeys( bool )` / `getPreferBareKeys()` | `Fson`/`Json5`: write quoted keys without quotes whenever the name satisfies the bare-name grammar; no effect on `Json`, which always quotes |
| `write( document, ostream )` | serialize to a stream; dispatches on the root value's actual type (v0.9.0.0) |
| `write( document ) → std::string` | serialize to a string |
| `writeFile( document, filename ) → bool` | false on I/O failure |
| `static encodeString( text )` | string escaping without the quotes; the result is valid in all three dialects |

**Fson dialect** (default) loses no information: comments, member order,
key form, `--` markers and number lexemes (including hex and
`Infinity`/`-Infinity`/`NaN`) are all written back. Format: 2-space
indentation; `key: value`, one member/element per line; compact `{}`/`[]`
for empty containers *unless* they carry dangling comments; disabled
members adjacent as `--key: value`; strings escape only `"`, `\` and
control characters (raw UTF-8, including U+2028/2029, passes through);
the output ends with a newline. Pretty output is a fixed point:
`write(parse(write(x))) == write(parse(x))`.

**Json dialect** produces fully JSON-compliant output: comments are
omitted, every key is quoted, `Number::Kind::Hex` lexemes are rewritten
as plain decimal, and `--` disabled members follow
`DisabledMemberPolicy` — `ConvertToComment` behaves like `Drop` here,
since strict JSON cannot carry comments; `Keep` emits them as ordinary
members (dropping only the prefix, which may produce duplicate keys when
a disabled member shadows an active one). Writing a
`Number::Kind::Infinity`/`NegativeInfinity`/`NaN` value throws
`std::domain_error` — strict JSON cannot represent it. Containers left
empty after filtering are written compact.

**Json5 dialect** (v0.9.0.0) produces JSON5-compliant output. Unlike
`Json`, it keeps comments (header, leading, trailing, dangling, footer)
and bare keys (same rule as `Fson`), and writes number lexemes verbatim
— hex stays hex, `Infinity`/`-Infinity`/`NaN` stay as their literal
tokens, since JSON5 natively supports all of this. `--` disabled members
follow `DisabledMemberPolicy`: `ConvertToComment` (default) synthesizes a
comment rendering `key: value` — one `//` line for a scalar value, one
`//` line per source line for a container value; `Keep` emits the member
ordinarily (no prefix); `Drop` emits nothing. Any multi-line block
comment written in this dialect — original or synthesized — is
flattened into one `//`-prefixed line per source line, since JSON5
forbids nested block comments (the one place FSON and JSON5 diverge; see
`docs/notation.md`).

---

## Fson — `fson/Fson.hh`

Static façade for the common cycle. Errors are returned, never thrown.

| Member | Description |
|---|---|
| `Fson::load( filename ) → LoadResult` | parse a file |
| `LoadResult::document` | `unique_ptr<Document>`; null only when the file cannot be opened |
| `LoadResult::errors` | everything the parser recorded, warnings included |
| `LoadResult::dependencies` | canonical paths of every file opened while resolving `%include` directives (empty when there are none); does NOT include the host file |
| `LoadResult::dependencySet` | the complete set of files needed to resolve the document — the host file PLUS every included file, de-duplicated and sorted; this is the ready-made watch list for reload (no manual host addition needed) |
| `Fson::dependenciesOf( filename ) → set<string>` | the same complete file set, computed WITHOUT keeping the resolved document; for reload/watch callers that only need the file list. A missing host file yields an empty set; errors never throw |
| `Fson::snapshot( filename ) → Snapshot` | records mtime, size and an FNV-1a content hash of every file needed to resolve `filename` (host + includes); empty when the host is missing; never throws |
| `Fson::isStale( filename, snapshot, mode = MtimeSize ) → bool` | true when any covered file changed since the snapshot, including a deleted dependency or a changed include graph (a file added/removed). `StaleMode::MtimeSize` compares mtime+size (cheap, a bare touch counts as stale); `StaleMode::ContentHash` compares the hash (exact, a touch with identical bytes is not stale). One snapshot serves either mode. Never throws |
| `Snapshot::empty()` / `Snapshot::files()` | whether the snapshot is empty, and the canonical paths it covers |
| `LoadResult::ok()` | document produced and nothing worse than a `Warning` |
| `Fson::save( document, filename ) → bool` | pretty-print to a file |

`Fson::load` resolves `%include` directives before returning: it merges
defaults (host wins, shallow), binds aliases, resolves relative targets
against the including file's directory, and reports missing targets and
include cycles as errors (never throwing). See `docs/notation.md` for the
directive semantics and the `Object` provenance API above for locating a
resolved value's origin file.

**Reload pattern.** fson does not run a file-watcher thread (that is
platform-specific and a policy for the application). Instead, take a
`snapshot` after loading and later ask `isStale`; reload only when it
returns true:

```cpp
auto result = Fson::load( "config.fson" );
auto snap   = Fson::snapshot( "config.fson" );
// ... later, on a timer or an OS file event ...
if( Fson::isStale( "config.fson", snap, Fson::StaleMode::ContentHash ) )
{
  result = Fson::load( "config.fson" );
  snap   = Fson::snapshot( "config.fson" );
}
```

---

## fson-check — command-line tool

```
fson-check <file>                  read, report, pretty-print to stdout
fson-check -i <file>               rewrite in place when error-free
fson-check --output <out> <file>   write to <out> when error-free
fson-check --json <file>           convert to fully JSON-compliant output
fson-check --json5 <file>          convert to JSON5-compliant output
fson-check --json5 --keep <file>   ... keeping disabled members (without '--')
fson-check --json5 --drop-disabled <file>  ... dropping them entirely
fson-check --bare <file>           unquote keys that are grammar-valid names
```

`--json` removes comments and quotes every key; `--json5` keeps comments
and bare keys, and keeps hex/`Infinity`/`-Infinity`/`NaN` number lexemes
verbatim. Both map `--` disabled members through `DisabledMemberPolicy`:
by default they become a comment under `--json5` (and are dropped under
`--json`, since plain JSON cannot carry comments); `--keep` retains them
as ordinary members instead; `--drop-disabled` removes them entirely.
`--keep` and `--drop-disabled` are mutually exclusive, and each requires
`--json` or `--json5`. `--bare` normalizes quoted keys to the bare form
where the name grammar allows it; it cannot be combined with `--json`
(JSON keys are always quoted) but works with `--json5` (which supports
bare keys). `--json` and `--json5` are mutually exclusive with each
other and with `-i` (an in-place conversion would still lose the `--`
disabled-member structure even under `--json5` — use `--output` or
stdout). A document that cannot be represented in the chosen dialect
(e.g. an `Infinity` value under `--json`) is reported as a write failure
rather than crashing.

Diagnostics go to stderr as `file:line:col: severity: message`. Warnings
do not block rewriting. Exit status: `0` nothing worse than warnings,
`1` parse errors, `2` usage, I/O, or write failure.