Dom Subsystem

ClaFi Framework Developer Manual

Introduction

The Dom subsystem is a generic, type-safe, schema-first representation of any hierarchical data. It provides automatic save capabilities, customizable file formats, and transactional changes.

Module Imports: To use the DOM subsystem and its declarative DSL, import the core DOM subsystem module in your C++20 code:
import ClaFi.Dom;
If you need built-in file format support, also import the corresponding format module:
import ClaFi.Dom.Formats.ClaFi; // or ClaFi.Dom.Formats.Json
Namespace Note: To keep code examples concise and readable, almost all examples in this manual assume that the main DOM runtime namespace is in scope:
using namespace ClaFi::Dom;
Blueprint construction nodes are explicitly qualified with the Dt:: namespace prefix to clearly differentiate them from active runtime memory nodes.
The "Document" Conceptual Model: A Document is simply a Section that can be saved to and loaded from disk. Any Section can be combined and nested with any other section, in any order, whether or not it's a Document's root. A Document can be stored in any file format — the format is a separate, pluggable concern.

Basic Usage Example

The Dom subsystem utilizes a declarative DSL blueprint pattern. This example shows how to initialize a serialized Document container on disk, bind native class methods as event listeners, load, alter, and save configuration or document settings.

import ClaFi.Dom;
import ClaFi.Dom.Formats.ClaFi;

class MyForm
{
public:
    void onColorChanged(ChangeEvent&);

    void configShowCase()
    {
        // 1. Create the document and pass its layout blueprint to the constructor.
        //    The outermost Dt::Section is anonymous (no key), acting as the document root.
        Document<FileFormat::ClaFi> configDoc
        {
            L"app.cfg",
            AutoSave::No,
            Dt::Section
            {
                Dt::Section
                {
                    L"Window",
                    Dt::Value{ L"Width", 1280 },
                    Dt::Value{ L"Height", 720 },
                    Dt::Value{ L"Title", L"My Application" }
                },
                Dt::Value
                {
                    L"ColorMode",
                    ColorMode::Dark,
                    Dt::OnChange{ this, &MyForm::onColorChanged }
                }
            }
        };

        // 2. Load from disk
        configDoc.load();

        // 3. Access and modify data
        int width = (configDoc / L"Window" / L"Width").get<int>();
        width += 100;
        (configDoc / L"Window" / L"Width").set(width);

        // 4. Save back out
        configDoc.save();
    }
};

Type Registration

Before using custom types within blueprints, the developer must declare their serialization rules. The system provides mapping architectures for primitives, structs (or classes), and enums.

Scalar Values

Basic types (int, float, bool, std::wstring) come with serializers, imported as described in Where a Serializer Must Be Imported. Custom scalar types can be registered by specializing the single-parameter template ScalarSerializer.

Each custom serializer can optionally expose an internal StorageType alias. Declaring this alias is only required when the type you pass to your serializer differs from the type that should be permanently stored in the DOM. For example, string-like views (such as std::wstring_view or const wchar_t*) use this mechanism to declare using StorageType = std::wstring;, forcing the DOM engine to instantiate an owned std::wstring node and preventing dangling pointer bugs. If no StorageType alias is declared, the engine seamlessly falls back to the decayed type of your template parameter.

template <>
struct ClaFi::Dom::ScalarSerializer<MyIntType>
{
    // Optional: omitted here as StorageType defaults directly to MyIntType

    static std::wstring toWString(const MyIntType& v)
    {
        return std::to_wstring(v.val);
    }

    static void fromWString(std::wstring_view str, MyIntType& val)
    {
        /* custom parsing logic */
    }
};

Structs & Classes

To make a C++ struct serializable, define a static constexpr auto serializedFields tuple inside the struct. This maps string keys to class properties.

The SerializedField supports both direct member pointers and getter/setter function pairs:

struct WindowConfig
{
    bool maximized{ false };
    int width{ 640 };

    int getWidth() const { return width; }
    void setWidth(int w) { width = w; }

    static constexpr auto serializedFields = std::make_tuple(
        // Direct member access
        SerializedField{ L"Maximized", &WindowConfig::maximized },

        // Getter and setter pair (types are deduced automatically)
        SerializedField{ L"Width", &WindowConfig::getWidth, &WindowConfig::setWidth }
        // Note: If you have an overloaded setter (e.g., void setWidth(float w);)
        // you can resolve it using a cast: static_cast<void (WindowConfig::*)(int)>(&WindowConfig::setWidth)
    );
};

In the ClaFi Dom, both primitive scalars and complex reflected structs are represented by the unified template class Value<T>. When you define a Value with a reflected C++ struct, the DOM engine expands the struct into a sub-tree of nested nodes. This allows you to traverse inside it (e.g., settings / L"Placement" / L"Bounds") exactly like inside a regular Section.

External Structs (Non-Intrusive)

If you need to serialize a C++ struct or class that you cannot modify, define a standalone constexpr auto serializedFields(Type) function in the same namespace.

struct ExternalPoint
{
    float x, y;
};

constexpr auto serializedFields(ExternalPoint)
{
    return std::make_tuple(
        SerializedField{ L"X", &ExternalPoint::x },
        SerializedField{ L"Y", &ExternalPoint::y }
    );
}

Enums

Enums require a constexpr auto enumNames(EnumType) function in the same namespace. If the enum ends with a Count element, it is used for compile-time size checking.

enum class ColorMode
{
    Dark,
    Light,
    Count
};

constexpr auto enumNames(ColorMode)
{
    return std::array{ L"Dark", L"Light" };
}

Where a Serializer Must Be Imported

A serializer serves the file that reads or writes the value - the file holding the Dt::Value, the get<T>(), the set() or the getTo(). That file imports the serializer's module itself, or imports a module that re-exports it. A plain import in a module further up the chain does not carry it.

The rule reaches every member of a struct. The engine looks for each member's serializer in that same file, and a member whose serializer is out of sight is taken for a struct of its own. The build then stops in the engine at:

static assertion failed: 'Type must define k_serializedFields, serializedFields member, or an ADL serializedFields() function.'

The instantiation context names the file the import belongs in, and further down, the member type whose serializer it could not see.

The built-in serializers live in three modules. ClaFi.Dom re-exports the first two.

The module that states serializedFields for a struct re-exports the serializers its members need. Every file that can see the fields can then read and write the struct, with no import of its own:

export module MyApp.Swatch;

export import ClaFi.Core.Dom_UiSerializers;

import ClaFi.Core.System.Serialization;
import ClaFi.Core.System.UiTypes;
import ClaFi.StdLib;

export struct Swatch
{
    Color fill{};
    Color stroke{};
};

export constexpr auto serializedFields(const Swatch&)
{
    return std::make_tuple(
        SerializedField{ L"Fill", &Swatch::fill },
        SerializedField{ L"Stroke", &Swatch::stroke }
    );
}

Blueprint Declaration

The Dom subsystem features a C++ Domain-Specific Language (DSL) to express document blueprints. To achieve zero allocation overhead, the Dt::Section class and other DSL nodes are strictly move-only.

Schema-First Evaluation

Critical Architectural Rule: The engine does not attempt to guess types from text. When a parser reads "Value=42" from a file, it does not know if 42 is an integer, a float, or a string. The only source of truth for types and structure is the defining DSL evaluation stage executed in C++ prior to loading.

Blueprint Nodes

Handlers are properties, not nodes. OnEvent, OnChange and OnNestedChange do not occupy a position in a section's child list. They attach to whatever node encloses them, and they connect as soon as that node exists — before its children are applied. Nothing in the Dom raises an event while a blueprint is being applied, so the ordering is not observable, but it is worth knowing when reasoning about a handler's lifetime.

OnEvent comes from ClaFi::Dt, the machinery the Dom and Grid declarative layers share, and Dom::Dt re-exports it. It reads the event type off the handler's own parameter, which is why there is no OnColorChange, no OnItemAdded, and no need for one. OnChange and OnNestedChange remain as thin named wrappers so existing blueprints keep compiling.

The following declarative constructs reside inside the ClaFi::Dom::Dt namespace:

Blueprint Memory & Lifetime Rules

Because section blueprints are non-copyable, you must manage their lifetimes and composition using C++ standard value categories. Review the rules below to write clean, compile-safe configurations:

Do:

  • Do pass inline temporaries directly (rvalues): Defining schemas inline inside constructors allows the engine to transfer ownership of the nodes with zero allocation overhead.
  • Do mix references and moves dynamically: You can combine existing blueprint variables (lvalues) and temporary blueprints (rvalues) into a single layout in any order.
  • Do use std::move to migrate variables: When nesting an existing blueprint variable into a new one, pass it with std::move() to dissolve the source and transfer its memory ownership.
  • Do pass by reference for shared layouts: If you want a base blueprint variable to remain intact so you can reuse it across multiple schemas, pass it directly by reference instead of moving it.

Don't:

  • Don't do anything that won't compile. If you get an error, inspect it carefully and refer again to the Do section to resolve it.

Low-level Manual Construction

If you need to construct or augment the structure dynamically at runtime instead of using the declarative DSL constructor, use the manual builder APIs directly on any Section node:

A Complete Example

This example deliberately demonstrates every way of creating, merging, and transferring schemas between scopes. You won't need all of them at once in real code — a typical application is more likely to have a single const declaration with everything it needs in one place.

One rule must be obeyed without exception: the schema must be established before any file is ever read. This is worth spelling out for sequences specifically, since a sequence can look dynamic in a way that invites confusion: its item count changes freely as data comes and goes, but the sequence's own existence, and the shape each item takes, are declared once, up front, exactly like every other node in the schema. What varies at runtime is only which items are actually present — found by searching the already-declared sequence with childWhere, never by inventing new schema entries after the fact.

// AppSchemas.ixx (a small module the rest of the app shares)
export module MyProject.AppSchemas;

import ClaFi.Dom;

export namespace MyProject::Schemas
{
    // Declared once. Referencing it below does not copy or move it --
    // each use just points back to this same instance, so it stays intact
    // for every place it's used.
    export const Dt::Section DEFAULT_THEME
    {
        L"Theme",
        Dt::Value{ L"Background", std::wstring{ L"#1e1e1e" } },
        Dt::Value{ L"Accent", std::wstring{ L"#0066cc" } }
    };
}

// NetworkSchema.cpp (a schema fragment built through a function, not a
// static reference -- every value here is an ordinary, fixed default
// known at compile time, same as if it were written inline)
import ClaFi.Dom;

Dt::Section makeNetworkSection()
{
    return Dt::Section
    {
        L"Network",
        Dt::Value{ L"ProxyEnabled", false },
        Dt::Value{ L"TimeoutSeconds", 30 }
    };
}

// AppSettings.cpp (assembling everything into one document)
import ClaFi.Dom;
import ClaFi.FileFormat.ClaFi;
import MyProject.AppSchemas;

class SettingsManager
{
public:
    void onNetworkChanged(NestedChangeEvent&)
    {
        Networking::applySettings();
    }

    void initialize()
    {
        // A section built via a function call, with a listener attached to
        // it specifically -- append() merges the handler into Network's own
        // handler set, not into the whole document's.
        Dt::Section networkLayout = makeNetworkSection();
        networkLayout.append(Dt::Section
        {
            Dt::OnNestedChange{ this, &SettingsManager::onNetworkChanged }
        });

        Dt::Section rootLayout
        {
            // A key-less group: these two Values are merged in as direct
            // siblings of everything else here, not nested under a section
            // of their own.
            Dt::Section
            {
                Dt::Value{ L"Language", std::wstring{ L"en" } },
                Dt::Value{ L"FirstRun", true }
            },

            std::move(networkLayout),

            // The reusable Theme blueprint -- still fully intact, and used
            // again below as the root of its own separate file.
            Schemas::DEFAULT_THEME,

            // The set of installed plugins is data, and can grow or shrink --
            // but the sequence itself, and the shape each entry has, is fixed
            // schema, declared here like everything else.
            Dt::Sequence{ L"Plugins", Dt::Section
            {
                Dt::Value{ L"Path", std::wstring{} },
                Dt::Value{ L"Enabled", true }
            } }
        };

        Document<FileFormat::ClaFi> settings{ L"app_settings.cfg", AutoSave::Yes, rootLayout };
        settings.load();

        // Editing anything under Network -- here, from a settings UI, or on
        // the next load() -- calls onNetworkChanged.
        (settings / L"Network" / L"ProxyEnabled").set(true);

        // Reconcile the already-declared Plugins sequence against what's
        // actually on disk right now, by identity (Path) -- not by rebuilding
        // any part of the schema. First, drop entries for files that are gone:
        auto& pluginSeq = (settings / L"Plugins").as<Sequence<Section>>();

        for (size_t i = pluginSeq.size(); i-- > 0; )
        {
            const std::wstring path = (pluginSeq[i] / L"Path").get<std::wstring>();
            if (!std::filesystem::exists(path))
            {
                pluginSeq[i].deleteSelf();
            }
        }

        // Then add an entry for any file found that isn't tracked yet. An
        // entry that's already there keeps whatever Enabled value the user
        // set for it -- rescanning never resets it.
        for (const auto& pluginPath : Platform::scanPluginDirectory())
        {
            std::wstring pathStr = pluginPath.wstring();
            if (pluginSeq.childWhere<std::wstring>({ L"Path" }, pathStr))
            {
                continue;
            }

            Section& entry = pluginSeq.add();
            (entry / L"Path").set(pathStr);
            (entry / L"Enabled").set(true);
        }

        settings.save();

        // DEFAULT_THEME is the same object used above -- reusing it here
        // doesn't disturb the copy already embedded in "settings". Since its
        // own key is "Theme", this file's root has the exact same shape it
        // has when nested: accessed as (themeFile / L"Theme" / L"Background"),
        // not a bare L"Background".
        Document<FileFormat::ClaFi> themeFile{ L"themes/dark.clafi", AutoSave::Yes, Schemas::DEFAULT_THEME };
        themeFile.load();
    }
};

Reading & Writing Values

Reading, Writing & Casting Nodes

Dom provides several mechanisms to traverse and edit your values. For nested structures, the engine supports a directory-style path division operator (/) and index subscripts ([]).

In C++, the types map to your initial layout definitions. Both scalar primitives and reflected composite structs reside in the unified template class Value<T>. You can retrieve values directly or extract references to the node objects using the .as<T>() helper:

// 1. Direct retrieval (using .get<T>())
int width = (settings / L"Window" / L"Width").get<int>();
IntRect r = (settings / L"Window" / L"Placement" / L"Bounds").get<IntRect>();

// 2. Writing values
(settings / L"Window" / L"Width").set(1920);

// 3. Getting a typed reference to the node itself (using .as<Value<T>>())
auto& widthNode = (settings / L"Window" / L"Width").as<Value<int>>();
widthNode.set(1920);

// 4. Getting a typed reference to a composite value node
auto& placementNode = (settings / L"Window" / L"Placement").as<Value<WindowConfig>>();
WindowConfig win = placementNode.get();

Zero-Copy Extraction (getTo)

When working with large, complex structs or arrays, calling .get<T>() forces the engine to return a fully composed object by value, which incurs copy overhead. To eliminate this, the engine provides the .getTo(T&) method.

.getTo() recursively deserializes the sub-tree directly into your pre-allocated object. Furthermore, if you use direct member pointers in your serializedFields tuple (no setter), the DOM engine bypasses intermediate assignments and deserializes nested hierarchies directly into the target struct's physical memory footprint in-place. For Sequence nodes, it leverages .resize() on supported containers to pre-allocate capacity and deserialize elements sequentially into the underlying buffer.

// Zero-copy extraction of a complex configuration
WindowConfig win;
(settings / L"Window" / L"Placement").getTo(win);

// Zero-copy extraction of a sequence into a vector
std::vector<TabConfig> existingTabs;
(settings / L"OpenTabs").getTo(existingTabs);

Eager Type Evaluation & Zero-Overhead Resolution

The ClaFi DOM engine is strictly Schema-First. This allows the engine to bypass the severe CPU and memory overhead normally associated with loose text-parsing document trees.

O(1) Fetching: The engine performs Eager Evaluation on leaf nodes. All primitives are parsed into their registered C++ types during document initialization. Structs, however, are purely ephemeral and composed on-the-fly. Calling .get<int>() on a leaf node is a direct O(1) memory fetch without string parsing, while calling .get<WindowConfig>() on a composite node instantly aggregates those pre-parsed leaf values into your struct with zero text-parsing overhead.
Zero-Allocation Paths: The DOM engine leverages C++20 heterogeneous map lookups. The / traversal operator does not allocate strings or copy keys. It compiles down to pure pointer operations passing std::wstring_view boundaries, ensuring your read loops and deep-path resolutions remain allocation-free.

Sequences

The Dom subsystem treats every array natively via the universal Sequence<T> component. It natively supports primitives, structured class layouts, and unlimited multidimensional nesting (e.g., arrays of arrays). All of these sequence configurations natively support allocation-free lazy evaluation and dynamic operations.

Every Sequence<T> is homogeneous by construction — every element shares the same type T, whether T is a scalar, a reflected composite, or Dom::Section itself. The two shapes below differ only in what T is and how you round-trip a value of that type.

Value Sequences

When working with standard scalar primitives or simple composites, you can easily extract or overwrite the entire sequence natively:

// 1. Read the entire sequence directly into a standard vector
std::vector<TabConfig> tabs = (settings / L"OpenTabs").get<std::vector<TabConfig>>();

// 2. Modify your collection locally
tabs.push_back(TabConfig{ .id = L"Tab3", .title = L"Settings" });

// 3. Overwrite the sequence node on the DOM in one step
(settings / L"OpenTabs").set(tabs);

Section Sequences

When you declare a structural array using a unique DSL layout (Dt::Sequence{ L"MyLayouts", Dt::Section{ ... } }), the engine creates a dynamic Sequence<Dom::Section> list node. Fresh elements added dynamically via `.add()` will instantly clone the layout blueprint's underlying runtime structures.

// 1. Retrieve the typed section sequence reference using the fluent .as() cast
auto& dynamicTabs = (settings / L"OpenTabs").as<Dom::Sequence<Dom::Section>>();

// 2. Clear all existing nested DOM nodes
dynamicTabs.clear();

// 3. Append a new nested structural node (automatically copy-initialized with the template layout)
auto& tabNode = dynamicTabs.add();
(tabNode / L"Id").set<std::wstring>(L"Tab1");
(tabNode / L"Title").set<std::wstring>(L"Home");

Search and Removal inside Sequences

The SequenceBase provides safe template search operations to locate elements matching a specific nested value, and NodeBase allows elements to destroy themselves from their parent containers:

// 1. Lookup a dynamic tab section by a nested value ("Id" inside each Section sequence element)
NodeBase* foundItem = (settings / L"OpenTabs").as<Dom::Sequence<Dom::Section>>().childWhere<std::wstring>({ L"Id" }, L"Tab1");

if (foundItem)
{
    // 2. Read, update, or completely erase the located element from the sequence
    foundItem->deleteSelf();
}

// 3. Empty paths can be used to search homogenous primitive sequences
auto& stringSeq = (settings / L"StringArray").as<Dom::Sequence<std::wstring>>();
NodeBase* foundPrimitive = stringSeq.childWhere<std::wstring>({}, L"MatchMe");

// 4. General predicate form: any boolean expression over the child, not just equality
auto& openTabs = (settings / L"OpenTabs").as<Dom::Sequence<Dom::Section>>();
Dom::Section* activeWideTab = openTabs.childWhere([](const Dom::Section& tab)
{
    return (tab / L"Active").get<bool>() && (tab / L"Width").get<int>() > 400;
});

Iterating Sequences of Large or Complex Values

For sequences of small scalars this distinction rarely shows up, but for a Sequence<Dom::Section> or a sequence of large composites, it matters which accessor you reach for. There are two genuinely different operations that look similar at the call site:

// Expensive: clones every element's whole sub-tree, on every single pass
for (const auto& tab : (settings / L"OpenTabs").get<std::vector<TabConfig>>())
{
    // tab is an independent, already-detached copy
}

// Zero-copy: iterates the live child nodes directly, nothing is cloned
auto& openTabs = (settings / L"OpenTabs").as<Dom::Sequence<Dom::Section>>();
for (const Dom::Section& tab : openTabs)
{
    // tab is a reference to the actual child Section in the tree
    auto title = (tab / L"Title").get<std::wstring>();
}

The same tradeoff applies to a single composite or section field, not just sequences: .get<T>() always returns a detached value, while childSection(key) and the / path operator give you a direct reference to the live node with no copy.

Events & Transactions

The Dom subsystem dispatches a ChangeEvent synchronously on the node whose value changed, and a NestedChangeEvent that bubbles from that node up through each ancestor in turn. Without batching, each field update in a set of related changes produces its own independent NestedChangeEvent bubble.

The Hierarchical Transaction System batches this bubbling. Wrapping a set of updates in an RAII Transaction targeting any sub-tree separates local state updates from structural tree updates for its duration:

void MyForm::applyCustomLayout()
{
    {
        // 1. Open a transaction directly on the MainForm node using the fluent API.
        //    Any nested bubbling is suspended within this sub-tree.
        auto t = (settings / L"MainForm").startTransaction();

        // 2. Perform multiple updates (each firing local ChangeEvents instantly).
        (settings / L"MainForm" / L"Maximized").set(true);
        (settings / L"MainForm" / L"Placement").set(WindowConfig{ .bounds = { 0, 0, 800, 600 } });

        // AutoSave (if enabled) is safely suspended. No disk IO occurs yet.
    }
    // 3. Transaction out of scope! 
    //    A single, unified NestedChangeEvent is bubbled up from the MainForm node.
    //    Our registered onUIConfigurationChanged() handler fires exactly once,
    //    and the configuration Document flushes to the disk exactly once!
}

The Document Wrapper

The Document<FileFormat> template handles parsing, serializing, file lock rules, and stream delegation.

Blueprint Lifetimes: Because the document's internal DOM structure is fully allocated within the Document constructor stage (via layout.apply()), the source Dt::Section blueprint does not need to outlive the Document wrapper. It can be safely destroyed immediately after construction completes.

Supported File Formats

The core DOM engine operates independently of specific file syntaxes. File format modules convert plain text into the DOM node structure. The ClaFi framework ships with three built-in, dependency-free format modules.

Format Module Syntax Best Use Case
FileFormat::ClaFi ClaFi Extremely fast parsing with highly readable syntax tailored for human-edited config files.
FileFormat::Json JSON Interoperability with web formats or third-party tools while maintaining a strict zero-dependency C++ architecture.
FileFormat::Xml XML Interoperability with XML-based tooling or legacy systems. Sequence items are written as repeated <Item> elements under the sequence's own key.
Spec Conformance: none of these three modules is a general-purpose, standalone parser for its format — each is schema-bound, resolving every key against the DOM schema already defined in C++ rather than inferring structure from the file itself. An unrecognized key emits a schema-error event and is discarded, not preserved. A few further deviations worth knowing before relying on interop with external tools:
  • FileFormat::Json does not decode \uXXXX Unicode escapes, and requires the document root to be a JSON object ({...}).
  • FileFormat::Xml has no support for CDATA sections, comments, DOCTYPE/DTD, namespaces, or numeric character references, and only recognizes double-quoted attribute values (not single-quoted).
  • FileFormat::ClaFi follows the ClaFi 1.0 Handbook closely; malformed input degrades via schema-error events rather than a hard parse failure.
See each module's own header comment for the complete list.

Implementing a Custom File Format

To integrate an external parser, implement a custom parser class inheriting from FileFormatBase. This example uses C++20 modules and the nlohmann/json library to parse and serialize configurations.

Implementation (ThisApp.DomFormats.JsonNlohmann)

module;
#include <../../../nlohmann/json.hpp>
export module ThisApp.DomFormats.JsonNlohmann;

import ClaFi.Dom;
import ClaFi.StdLib;

namespace ThisApp::DomFormats
{
    using namespace ClaFi::Dom;
    using json = nlohmann::json;

    export class JsonNlohmann : public FileFormatBase
    {
    public:
        void loadSectionFromStream(Section& section, std::wistream& stream) override;
        void saveSectionToStream(const Section& section, std::wostream& stream) override;

    private:
        void populateNode(const json& j, NodeBase& target);
        json buildJson(const NodeBase& node);

        static std::wstring utf8ToWString(const std::string& str);
        static std::string wStringToUtf8(const std::wstring& str);
    };

    // ---------------------------------------------------------
    // Implementation
    // ---------------------------------------------------------

    std::wstring JsonNlohmann::utf8ToWString(const std::string& str)
    {
        std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;
        return myconv.from_bytes(str);
    }

    std::string JsonNlohmann::wStringToUtf8(const std::wstring& str)
    {
        std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;
        return myconv.to_bytes(str);
    }

    void JsonNlohmann::loadSectionFromStream(Section& section, std::wistream& stream)
    {
        std::wstringstream wss;
        wss << stream.rdbuf();
        std::string utf8Str = wStringToUtf8(wss.str());

        json j = json::parse(utf8Str, nullptr, false);
        if (!j.is_discarded())
        {
            populateNode(j, section);
        }
    }

    void JsonNlohmann::saveSectionToStream(const Section& section, std::wostream& stream)
    {
        json j = buildJson(section);
        stream << utf8ToWString(j.dump(4)) << L"\n";
    }

    void JsonNlohmann::populateNode(const json& j, NodeBase& target)
    {
        if (j.is_object())
        {
            if (target.type() == NodeType::Section || target.type() == NodeType::CompositeValue)
            {
                auto& sec = static_cast<Section&>(target);
                for (auto& [key, value] : j.items())
                {
                    std::wstring wkey = utf8ToWString(key);
                    NodeBase* childPtr = sec.child(wkey);

                    if (childPtr)
                    {
                        populateNode(value, *childPtr);
                    }
                    else
                    {
                        sec.emitSchemaError(sec, L"Schema mismatch: Unknown key in JSON object.");
                    }
                }
            }
            else
            {
                target.emitSchemaError(target, L"Schema mismatch: JSON expects an object structure here.");
            }
        }
        else if (j.is_array())
        {
            if (target.type() == NodeType::Sequence)
            {
                auto& seq = static_cast<SequenceBase&>(target);
                seq.clear();

                for (const auto& item : j)
                {
                    populateNode(item, seq.addNode());
                }
            }
            else
            {
                target.emitSchemaError(target, L"Schema mismatch: JSON expects an array sequence here.");
            }
        }
        else
        {
            if (target.type() == NodeType::ScalarValue)
            {
                std::string rawVal = j.is_string() ? j.get<std::string>() : j.dump();
                static_cast<ScalarValueBase&>(target).setFromRaw(utf8ToWString(rawVal));
            }
            else
            {
                target.emitSchemaError(target, L"Schema mismatch: JSON provided a primitive, but struct expected.");
            }
        }
    }

    json JsonNlohmann::buildJson(const NodeBase& node)
    {
        if (node.type() == NodeType::ScalarValue)
        {
            std::string raw = wStringToUtf8(static_cast<const ScalarValueBase&>(node).getAsRaw());

            if (raw == "true")
            {
                return true;
            }
            if (raw == "false")
            {
                return false;
            }

            try
            {
                size_t pos;
                int i = std::stoi(raw, &pos);
                if (pos == raw.size())
                {
                    return i;
                }

                double d = std::stod(raw, &pos);
                if (pos == raw.size())
                {
                    return d;
                }
            }
            catch (...)
            {
            }
            return raw;
        }
        else if (node.type() == NodeType::Section || node.type() == NodeType::CompositeValue)
        {
            json j = json::object();
            auto& sec = static_cast<const Section&>(node);

            for (const auto& wkey : sec.getKeys())
            {
                j[wStringToUtf8(std::wstring(wkey))] = buildJson(*sec.child(wkey));
            }
            return j;
        }
        else if (node.type() == NodeType::Sequence)
        {
            json j = json::array();
            auto& seq = static_cast<const SequenceBase&>(node);

            for (size_t i = 0; i < seq.size(); ++i)
            {
                j.push_back(buildJson(*seq.child(i)));
            }
            return j;
        }
        return json{};
    }
}
All manuals Top