Code View

fson / source / fson-1.1.0.0 / libs / internal / sdk / fson / Fson.hh
// SPDX-License-Identifier: MIT
#pragma once

#include <cstdint>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>

#include "cparse/Parser.hh"

#include "Document.hh"

namespace fedem
{
  namespace fson
  {
    // ─────────────────────────────────────────────────────────────────────
    // Fson
    //
    // Convenience façade for the typical load → query/modify → save
    // cycle:
    //
    //   auto result = Fson::load( "config.json" );
    //   if( result.ok( ) )
    //   {
    //     auto port = result.document->getInteger( "database.port", 5432 );
    //     result.document->setPath( "database.host",
    //                               std::make_unique< String >( "db1" ) );
    //     Fson::save( *result.document, "config.json" );
    //   }
    //
    // Errors are returned, never thrown (project decision).
    // ─────────────────────────────────────────────────────────────────────
    class Fson final
    {
      public:
        struct LoadResult
        {
          // Null only when the file could not be opened; parse errors
          // still yield a (possibly partial) document.
          std::unique_ptr< Document >        document;

          // Everything the parser recorded, including warnings.
          std::vector< parser::ParseError >  errors;

          // Canonical paths of every file opened while resolving %include
          // directives (empty when the document has no includes). A caller
          // that wants to reload on change (Topic 11) watches these plus the
          // host file itself.
          std::vector< std::string >         dependencies;

          // The complete set of files needed to resolve the document: the
          // host file PLUS every included file, de-duplicated and sorted.
          // This is exactly what a reload watcher should observe — unlike
          // `dependencies`, it already includes the host file, so no manual
          // addition is needed. Empty only when the file could not be opened.
          std::set< std::string >            dependencySet;

          // True when a document was produced and nothing worse than a
          // warning was recorded. Comments must be checked individually
          // when warnings matter.
          bool ok( ) const noexcept;
        };

        // Parses the file; never throws. Check result.ok() / .errors.
        static LoadResult load( std::string const& filename );

        // Returns the complete set of files needed to resolve `filename` —
        // the file itself plus every file reachable through its %include
        // directives (transitively), de-duplicated and sorted by canonical
        // path. Intended for reload/watch callers that want the file set
        // WITHOUT keeping the resolved document. A missing host file yields
        // an empty set. Parse/resolution errors do not throw; whatever files
        // were successfully opened are still reported.
        static std::set< std::string > dependenciesOf( std::string const& filename );

        // ── reload / staleness (Topic 11) ──────────────────────────────
        // How isStale() decides a dependency changed:
        //   MtimeSize   — compare last-write-time and size only (cheap: a
        //                 stat per file, no re-read). May report a bare
        //                 touch (mtime bump, identical content) as stale.
        //   ContentHash — compare a content hash (exact: re-reads each file).
        //                 A touch with identical bytes is NOT stale.
        // A Snapshot records both, so one snapshot serves either mode.
        enum class StaleMode
        {
          MtimeSize,
          ContentHash
        };

        // A point-in-time record of every file needed to resolve a document
        // (host + includes), taken so a later isStale() call can tell whether
        // any of them changed. Produced by snapshot(); opaque to callers
        // beyond empty() / files().
        class Snapshot final
        {
          public:
            struct Entry
            {
              std::int64_t   mtime;   // seconds since epoch (0 if unknown)
              std::uintmax_t size;    // bytes (0 if unknown)
              std::uint64_t  hash;    // FNV-1a 64 of the file bytes
            };

            bool empty( ) const noexcept { return entries.empty( ); }

            // The canonical paths this snapshot covers (host + includes).
            std::set< std::string > files( ) const;

          private:
            friend class Fson;
            std::map< std::string, Entry > entries;
        };

        // Captures the current state of `filename` and all files needed to
        // resolve it (its dependencySet). A missing host file yields an
        // empty snapshot (empty() == true). Never throws.
        static Snapshot snapshot( std::string const& filename );

        // True when, relative to `snap`, any covered file changed under the
        // chosen mode — including a file that was deleted, or a change to
        // the include graph itself (a dependency added or removed since the
        // snapshot, detected by re-resolving the host's dependency set).
        // An empty snapshot is considered stale iff `filename` now resolves
        // to a non-empty set (i.e. a file that was missing now exists).
        // Never throws.
        static bool isStale( std::string const& filename,
                             Snapshot const& snap,
                             StaleMode mode = StaleMode::MtimeSize );

        // Pretty-prints the document into the file (see FsonWriter for
        // the format). Returns false on I/O failure.
        static bool save( Document const& document, std::string const& filename );

      private:
        Fson( ) = delete;
    };

    inline bool Fson::LoadResult::ok( ) const noexcept
    {
      if( !document )
        return false;
      for( auto const& error : errors )
        if( error.kind != parser::ParseError::Kind::Warning )
          return false;
      return true;
    }
  }  // end namespace fson
}  // end namespace fedem