Code View

fson / source / fson-1.1.0.0 / libs / internal / sdk / fson / Object.hh
// 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