// SPDX-License-Identifier: MIT
#pragma once
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "Comment.hh"
#include "Include.hh"
#include "Value.hh"
namespace fedem
{
namespace fson
{
// ─────────────────────────────────────────────────────────────────────
// Key
//
// An object member key (docs/notation/00010-object.rrd):
// Quoted : a JSON string key — written back with quotes
// Bare : an unquoted identifier per the name grammar (00200)
// ─────────────────────────────────────────────────────────────────────
class Key final
{
public:
enum class Form
{
Quoted,
Bare
};
// Throws std::invalid_argument for Form::Bare when name does not
// satisfy the name grammar.
Key( Form form, std::string name );
static Key quoted( std::string name );
static Key bare ( std::string name );
// name grammar (00200-name.rrd): starts with a letter; letters and
// digits optionally separated by single underscores; no leading,
// trailing, or doubled underscore.
static bool isValidName( std::string const& text ) noexcept;
Form getForm( ) const noexcept;
std::string const& getName( ) const noexcept;
private:
Form form;
std::string name;
};
// ─────────────────────────────────────────────────────────────────────
// Member
//
// One key/value pair of an object, with its trivia and disabled flag.
// A disabled member ("--" prefix in the source) is invisible to the
// query API but is preserved and written back with its prefix.
// Move-only; duplicate with clone().
// ─────────────────────────────────────────────────────────────────────
class Member final
{
public:
Member( Key key, std::unique_ptr< Value > value, bool disabled = false );
Member( Member&& other ) noexcept = default;
Member& operator=( Member&& other ) noexcept = default;
Member clone( ) const;
Key const& getKey( ) const noexcept;
void setKey( Key key );
bool isDisabled( ) const noexcept;
void setDisabled( bool disabled ) noexcept;
Value& getValue( ) noexcept;
Value const& getValue( ) const noexcept;
void setValue( std::unique_ptr< Value > value );
CommentList& getLeadingComments( ) noexcept;
CommentList const& getLeadingComments( ) const noexcept;
std::optional< Comment >& getTrailingComment( ) noexcept;
std::optional< Comment > const& getTrailingComment( ) const noexcept;
// Provenance: empty sourceFile for a member that physically appears
// in this file; otherwise the canonical path of the %include target
// the member was merged in from, plus originLocalPath — the path of
// the value WITHIN that target file (which differs from the host
// query path for alias includes: host "vault.timeout" ↔ target
// "timeout"). Set by the loader's resolution step. The writer never
// emits merged-in members — they are query-view only — so a resolved
// document does not round-trip its merged members into the host file.
std::string const& getOriginFile( ) const noexcept;
std::string const& getOriginLocalPath( ) const noexcept;
void setOrigin( std::string sourceFile, std::string localPath );
bool isMerged( ) const noexcept;
private:
Key key;
bool disabled;
CommentList leading;
std::unique_ptr< Value > value;
std::optional< Comment > trailing;
std::string originFile;
std::string originLocalPath;
Member( Member const& ) = delete;
Member& operator=( Member const& ) = delete;
};
// ─────────────────────────────────────────────────────────────────────
// Object (docs/notation/00010-object.rrd)
//
// Ordered member list. Two access levels:
// • query API (contains/find/at/set/remove/size) — skips disabled
// members entirely; they are "yok hükmünde".
// • member API (memberCount/getMember/appendMember/eraseMember) —
// full structural access including disabled members, used by the
// parser, the writer, and enable()/disable().
// ─────────────────────────────────────────────────────────────────────
class Object final : public Value
{
public:
Object( ) = default;
Type getType( ) const noexcept override;
std::unique_ptr< Value > clone( ) const override;
Object* asObject( ) noexcept override;
Object const* asObject( ) const noexcept override;
// ── query API — disabled members are invisible ─────────────────
std::size_t size ( ) const noexcept;
bool empty( ) const noexcept;
bool contains( std::string const& name ) const noexcept;
Value* find( std::string const& name ) noexcept;
Value const* find( std::string const& name ) const noexcept;
// Throws std::out_of_range when no active member has this name.
Value& at( std::string const& name );
Value const& at( std::string const& name ) const;
// Replaces the value of the active member with the same name, or
// appends a new member. Returns a reference to the stored value.
Value& set( Key key, std::unique_ptr< Value > value );
// Convenience: bare key when the name is grammar-valid,
// quoted key otherwise.
Value& set( std::string const& name, std::unique_ptr< Value > value );
// Removes the active member with this name. False when absent.
bool remove( std::string const& name ) noexcept;
// Active → disabled. False when no active member has this name.
bool disable( std::string const& name ) noexcept;
// First disabled member with this name → active. Fails (false)
// when absent or when an active member with the same name exists.
bool enable( std::string const& name ) noexcept;
// ── path API — dotted-string addressing ────────────────────────
// Path syntax: keys separated by '.', array elements addressed
// with "[N]" suffixes, e.g. "database.host", "retries[2]",
// "servers[0].port", "matrix[1][2]".
// '.', '[' and ']' are the only special characters, so keys
// containing spaces work ("my key.sub"); keys containing '.' or
// '[' cannot be addressed via paths (known limitation of the
// dotted-string decision). Disabled members are invisible, as in
// the rest of the query API. Malformed paths resolve to nothing.
// nullptr when the path is malformed or nothing is there.
Value* findPath( std::string const& path ) noexcept;
Value const* findPath( std::string const& path ) const noexcept;
bool containsPath( std::string const& path ) const noexcept;
// Throws std::out_of_range when the path resolves to nothing.
Value& atPath( std::string const& path );
Value const& atPath( std::string const& path ) const;
// Stores value at the path, creating missing intermediate
// OBJECTS for key segments. Array segments never create or grow
// arrays: the array must exist and the index must be in range.
// Returns the stored value, or nullptr when the path is malformed,
// a segment mismatches (e.g. traversing through a number), an
// index is out of range, or value is null.
Value* setPath( std::string const& path, std::unique_ptr< Value > value );
// Removes the value at the path (active member or array element).
// False when the path is malformed or resolves to nothing.
bool removePath( std::string const& path ) noexcept;
// ── provenance API — where does a resolved path come from? ──────
// After Fson::load has resolved %include directives, a query path
// may address either a member physically present in the host file
// or one merged/aliased in from an included file. These queries
// report that provenance. Write policy is host-only (setPath always
// writes to the host), so a caller that instead wants to edit the
// origin file uses provenanceOf() to locate it and its local path.
struct Provenance
{
std::string queryPath; // the host query path (filled by map())
std::string sourceFile; // canonical file the value comes from;
// empty ⇒ the host file itself
std::string localPath; // the value's path within sourceFile
// (differs from queryPath for aliases)
bool isHost; // true ⇒ physically in the host file
};
// True when the path resolves to a member physically present in the
// host file (not merged in from an include). False when the path is
// merged/aliased in, is malformed, or resolves to nothing.
bool isLocal( std::string const& path ) const noexcept;
// Provenance of a single active path. nullopt when the path resolves
// to nothing. For a host value: { path, "", path, true }. For a
// merged/aliased value: the ultimate source file and its local path.
std::optional< Provenance > provenanceOf( std::string const& path ) const;
// Provenance of every active top-level-reachable member, as a flat
// list (host and merged alike). One entry per resolvable query path
// at object-member granularity (nested objects are described by
// their own entries via the dotted path).
std::vector< Provenance > provenanceMap( ) const;
// ── typed convenience getters ───────────────────────────────────
// Return the fallback when the path resolves to nothing or the
// value has a different type. getInteger()/getDouble() accept any
// Number (getInteger truncates).
std::string getString ( std::string const& path, std::string const& fallback = std::string( ) ) const;
long long getInteger( std::string const& path, long long fallback = 0LL ) const noexcept;
double getDouble ( std::string const& path, double fallback = 0.0 ) const noexcept;
bool getBoolean( std::string const& path, bool fallback = false ) const noexcept;
// ── member API — full access including disabled members ────────
std::size_t memberCount( ) const noexcept;
// Throws std::out_of_range on invalid index.
Member& getMember( std::size_t index );
Member const& getMember( std::size_t index ) const;
Member& appendMember( Member member );
void eraseMember( std::size_t index );
// ── include API — %include directives ──────────────────────────
// Directives are structural nodes kept alongside members in source
// order (see getEntryOrder). They are not visited by the query API;
// the loader resolves them (Fson::load) into merged defaults and
// aliases. Disabled directives are preserved but not resolved.
std::size_t includeCount( ) const noexcept;
// Throws std::out_of_range on invalid index.
Include& getInclude( std::size_t index );
Include const& getInclude( std::size_t index ) const;
Include& appendInclude( Include include );
void eraseInclude( std::size_t index );
// ── entry order — interleaved members and includes ─────────────
// Records the exact source order of the object body so the writer
// can round-trip members and %include directives in place. Each
// entry names a kind and an index into members[] or includes[].
enum class EntryKind
{
Member,
Include
};
struct Entry
{
EntryKind kind;
std::size_t index;
};
std::size_t entryCount( ) const noexcept;
Entry const& getEntry( std::size_t position ) const;
CommentList& getDanglingComments( ) noexcept;
CommentList const& getDanglingComments( ) const noexcept;
private:
std::vector< Member > members;
std::vector< Include > includes;
std::vector< Entry > entries;
CommentList dangling;
std::size_t findActiveIndex( std::string const& name ) const noexcept;
void rebuildEntryIndices( EntryKind kind, std::size_t removedIndex ) noexcept;
// Resolves a dotted path to the Member that owns its final value,
// when that final segment is an object key (not an array index).
// Returns nullptr when the path is malformed, resolves to nothing,
// or ends in an array element (which has no owning Member).
Member const* findOwningMember( std::string const& path ) const noexcept;
static constexpr std::size_t const NPOS = static_cast< std::size_t >( -1 );
};
inline Key::Form Key::getForm( ) const noexcept
{
return form;
}
inline std::string const& Key::getName( ) const noexcept
{
return name;
}
inline Value::Type Object::getType( ) const noexcept
{
return Type::Object;
}
inline Object* Object::asObject( ) noexcept
{
return this;
}
inline Object const* Object::asObject( ) const noexcept
{
return this;
}
inline bool Object::empty( ) const noexcept
{
return size( ) == 0U;
}
inline bool Object::contains( std::string const& name ) const noexcept
{
return findActiveIndex( name ) != NPOS;
}
inline std::size_t Object::memberCount( ) const noexcept
{
return members.size( );
}
inline std::size_t Object::includeCount( ) const noexcept
{
return includes.size( );
}
inline std::size_t Object::entryCount( ) const noexcept
{
return entries.size( );
}
inline CommentList& Object::getDanglingComments( ) noexcept
{
return dangling;
}
inline CommentList const& Object::getDanglingComments( ) const noexcept
{
return dangling;
}
} // end namespace fson
} // end namespace fedem