// 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