feat: Implement complete IDataNode/IDataTree system with JSON backend

Major feature: Unified config/data/runtime tree system

**New System Architecture:**
- Unified data tree for config, persistent data, and runtime state
- Three separate roots: config/ (read-only + hot-reload), data/ (read-write + save), runtime/ (temporary)
- Support for modding, saves, and hot-reload in single system

**Interfaces:**
- IDataValue: Abstract data value interface (type-safe access)
- IDataNode: Tree node with navigation, search, and modification
- IDataTree: Root container with config/data/runtime management

**Concrete Implementations:**
- JsonDataValue: nlohmann::json backed value
- JsonDataNode: Full tree navigation with pattern matching & queries
- JsonDataTree: File-based JSON storage with hot-reload

**Features:**
- Pattern matching search (wildcards support)
- Property-based queries with predicates
- SHA256 hashing for validation/sync
- Hot-reload for config/ directory
- Save operations for data/ persistence
- Read-only enforcement for config/

**API Changes:**
- All namespaces changed from 'warfactory' to 'grove'
- IDataTree: Added getConfigRoot(), getDataRoot(), getRuntimeRoot()
- IDataTree: Added saveData(), saveNode() for persistence
- IDataNode: Added setChild(), removeChild(), clearChildren()
- CMakeLists.txt: Added OpenSSL dependency for hashing

**Usage:**
```cpp
auto tree = DataTreeFactory::create("json", "./gamedata");
auto config = tree->getConfigRoot();     // Read-only game config
auto data = tree->getDataRoot();         // Player saves
auto runtime = tree->getRuntimeRoot();   // Temporary state

// Hot-reload config on file changes
if (tree->reloadIfChanged()) { /* refresh modules */ }

// Save player progress
data->setChild("progress", progressNode);
tree->saveData();
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-10-28 15:36:25 +08:00
parent c01e00559b
commit fad105afb2
46 changed files with 1242 additions and 90 deletions

View File

@ -37,12 +37,20 @@ if(GROVE_BUILD_IMPLEMENTATIONS)
add_library(grove_impl STATIC
src/ImGuiUI.cpp
src/ResourceRegistry.cpp
src/JsonDataValue.cpp
src/JsonDataNode.cpp
src/JsonDataTree.cpp
src/DataTreeFactory.cpp
)
target_link_libraries(grove_impl PUBLIC
GroveEngine::core
OpenSSL::Crypto
)
# Find OpenSSL for SHA256 hashing
find_package(OpenSSL REQUIRED)
# If imgui is available from parent project, link it
if(TARGET imgui_backends)
target_link_libraries(grove_impl PUBLIC imgui_backends)

View File

@ -4,7 +4,7 @@
#include <string>
#include <memory>
namespace warfactory {
namespace grove {
class SerializationRegistry;
@ -26,4 +26,4 @@ protected:
void unregisterFromSerialization();
};
} // namespace warfactory
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <memory>
#include "IDataTree.h"
namespace warfactory {
namespace grove {
/**
* @brief Factory for creating data tree instances
@ -20,4 +20,4 @@ public:
static std::unique_ptr<IDataTree> create(const std::string& type, const std::string& sourcePath);
};
} // namespace warfactory
} // namespace grove

View File

@ -15,7 +15,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
/**
* @brief Debug engine implementation with comprehensive logging
@ -86,4 +86,4 @@ public:
void setLogLevel(spdlog::level::level_enum level);
};
} // namespace warfactory
} // namespace grove

View File

@ -8,7 +8,7 @@
#include "IEngine.h"
#include "DebugEngine.h"
namespace warfactory {
namespace grove {
/**
* @brief Factory for creating engine implementations
@ -102,4 +102,4 @@ private:
static std::string toLowercase(const std::string& str);
};
} // namespace warfactory
} // namespace grove

View File

@ -8,12 +8,12 @@
#include "IDataTree.h"
// Forward declarations
namespace warfactory {
namespace grove {
class IEngine;
class IModuleSystem;
}
namespace warfactory {
namespace grove {
/**
* @brief Global system orchestrator - First launched, last shutdown
@ -158,4 +158,4 @@ public:
virtual json getSystemHealthReport() = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -6,7 +6,7 @@
#include <functional>
#include "IDataValue.h"
namespace warfactory {
namespace grove {
/**
* @brief Interface for a single node in the data tree
@ -226,6 +226,36 @@ public:
* @return Node type identifier
*/
virtual std::string getNodeType() const = 0;
// ========================================
// TREE MODIFICATION (For data/ and runtime/ nodes)
// ========================================
/**
* @brief Add or update a child node
* @param name Child name
* @param node Child node to add/replace
*
* If a child with this name already exists, it will be replaced.
* Only works for data/ and runtime/ nodes. Config nodes are read-only.
*/
virtual void setChild(const std::string& name, std::unique_ptr<IDataNode> node) = 0;
/**
* @brief Remove a child node
* @param name Child name to remove
* @return true if child was found and removed
*
* Only works for data/ and runtime/ nodes. Config nodes are read-only.
*/
virtual bool removeChild(const std::string& name) = 0;
/**
* @brief Clear all children from this node
*
* Only works for data/ and runtime/ nodes. Config nodes are read-only.
*/
virtual void clearChildren() = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -5,12 +5,18 @@
#include <functional>
#include "IDataNode.h"
namespace warfactory {
namespace grove {
/**
* @brief Interface for the root data tree container
*
* Manages the entire tree structure and provides hot-reload capabilities
* Unified system for configuration, persistent data, and runtime state.
* Supports hot-reload for config and persistence for data.
*
* Tree Structure:
* - config/ : Read-only game configuration (hot-reload enabled, moddable)
* - data/ : Persistent player data (read-write, saved to disk)
* - runtime/ : Temporary runtime state (read-write, never saved)
*/
class IDataTree {
public:
@ -21,37 +27,91 @@ public:
// ========================================
/**
* @brief Get root node of the tree
* @return Root node
* @brief Get root node of the entire tree
* @return Root node containing config/, data/, runtime/
*
* WARNING: This gives access to everything. Use getConfigRoot(),
* getDataRoot(), or getRuntimeRoot() for isolated access.
*/
virtual std::unique_ptr<IDataNode> getRoot() = 0;
/**
* @brief Get node by path from root
* @param path Path from root (e.g., "vehicles/tanks/heavy")
* @param path Path from root (e.g., "config/vehicles/tanks/heavy")
* @return Node at path or nullptr if not found
*/
virtual std::unique_ptr<IDataNode> getNode(const std::string& path) = 0;
// ========================================
// MANUAL HOT-RELOAD (SIMPLE & EFFECTIVE)
// SEPARATE ROOTS (Recommended Access Pattern)
// ========================================
/**
* @brief Check if source files have changed
* @return true if changes detected
* @brief Get config tree root (read-only, hot-reload enabled)
* @return Config root node (config/)
*
* Use for: Game configuration, unit stats, modding
*/
virtual std::unique_ptr<IDataNode> getConfigRoot() = 0;
/**
* @brief Get persistent data root (read-write, saved to disk)
* @return Data root node (data/)
*
* Use for: Campaign progress, unlocks, player statistics
*/
virtual std::unique_ptr<IDataNode> getDataRoot() = 0;
/**
* @brief Get runtime data root (read-write, never saved)
* @return Runtime root node (runtime/)
*
* Use for: Current game state, temporary calculations, caches
*/
virtual std::unique_ptr<IDataNode> getRuntimeRoot() = 0;
// ========================================
// SAVE OPERATIONS
// ========================================
/**
* @brief Save all persistent data to disk
* @return true if save succeeded
*
* Saves the entire data/ subtree to disk. Does not affect config/ or runtime/.
*/
virtual bool saveData() = 0;
/**
* @brief Save specific node and its subtree
* @param path Path to node to save (e.g., "data/campaign/progress")
* @return true if save succeeded
*
* Allows granular saves for performance. Only works for data/ paths.
*/
virtual bool saveNode(const std::string& path) = 0;
// ========================================
// HOT-RELOAD (Config Only)
// ========================================
/**
* @brief Check if config files have changed
* @return true if changes detected in config/
*/
virtual bool checkForChanges() = 0;
/**
* @brief Reload entire tree if changes detected
* @brief Reload config tree if files changed
* @return true if reload was performed
*
* Only reloads config/. Does not affect data/ or runtime/.
*/
virtual bool reloadIfChanged() = 0;
/**
* @brief Register callback for when tree is reloaded
* @param callback Function called after successful reload
* @brief Register callback for when config is reloaded
* @param callback Function called after successful config reload
*/
virtual void onTreeReloaded(std::function<void()> callback) = 0;
@ -66,4 +126,4 @@ public:
virtual std::string getType() = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <vector>
#include <memory>
namespace warfactory {
namespace grove {
/**
* @brief Interface for data values - abstracts underlying data format
@ -40,4 +40,4 @@ public:
virtual std::string toString() const = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -4,12 +4,12 @@
#include <memory>
// Forward declarations to avoid circular dependencies
namespace warfactory {
namespace grove {
class IModuleSystem;
class IIO;
}
namespace warfactory {
namespace grove {
enum class EngineType {
DEBUG = 0,
@ -120,4 +120,4 @@ public:
virtual EngineType getType() const = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -6,7 +6,7 @@
#include <memory>
#include "IDataNode.h"
namespace warfactory {
namespace grove {
enum class IOType {
INTRA = 0, // Same process
@ -98,4 +98,4 @@ public:
virtual IOType getType() const = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -6,11 +6,11 @@
#include "ITaskScheduler.h"
// Forward declarations
namespace warfactory {
namespace grove {
class IIO;
}
namespace warfactory {
namespace grove {
@ -108,4 +108,4 @@ public:
virtual std::string getType() const = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -5,12 +5,12 @@
#include "ITaskScheduler.h"
// Forward declarations to avoid circular dependencies
namespace warfactory {
namespace grove {
class IModule;
class IIO;
}
namespace warfactory {
namespace grove {
enum class ModuleSystemType {
SEQUENTIAL = 0,
@ -89,4 +89,4 @@ public:
virtual ModuleSystemType getType() const = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -11,7 +11,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
/**
* @brief Factory for creating IO transport implementations
@ -131,4 +131,4 @@ private:
static std::string generateEndpoint(IOType ioType);
};
} // namespace warfactory
} // namespace grove

View File

@ -2,7 +2,7 @@
#include <string>
namespace warfactory {
namespace grove {
/**
* @brief Interface for geological regions during world generation
@ -47,4 +47,4 @@ public:
virtual bool canFuseWith(const IRegion* other) const = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -3,7 +3,7 @@
#include "IDataNode.h"
#include <memory>
namespace warfactory {
namespace grove {
class ISerializable {
public:
@ -13,4 +13,4 @@ public:
virtual void deserialize(const IDataNode& data) = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <memory>
#include "IDataNode.h"
namespace warfactory {
namespace grove {
/**
* @brief Task scheduling interface for module delegation to execution system
@ -100,4 +100,4 @@ public:
virtual std::unique_ptr<IDataNode> getCompletedTask() = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <string>
#include <functional>
namespace warfactory {
namespace grove {
using json = nlohmann::json;
@ -126,4 +126,4 @@ public:
virtual void setState(const json& state) = 0;
};
} // namespace warfactory
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <string>
#include <functional>
namespace warfactory {
namespace grove {
using json = nlohmann::json;
@ -337,4 +337,4 @@ constexpr const char* toString(Orientation orient) {
}
}
} // namespace warfactory
} // namespace grove

View File

@ -13,7 +13,7 @@
#include <functional>
#include <chrono>
namespace warfactory {
namespace grove {
/**
* @brief ImGui implementation of IUI interface
@ -704,4 +704,4 @@ private:
void renderLogConsole();
};
} // namespace warfactory
} // namespace grove

View File

@ -17,7 +17,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
// Interface for message delivery to avoid circular include
class IIntraIODelivery {
@ -132,4 +132,4 @@ public:
const std::string& getInstanceId() const;
};
} // namespace warfactory
} // namespace grove

View File

@ -13,7 +13,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
class IntraIO; // Forward declaration
class IIntraIODelivery; // Forward declaration
@ -88,4 +88,4 @@ public:
static IntraIOManager& getInstance();
};
} // namespace warfactory
} // namespace grove

View File

@ -0,0 +1,108 @@
#pragma once
#include "IDataNode.h"
#include "JsonDataValue.h"
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <memory>
#include <map>
#include <functional>
namespace grove {
using json = nlohmann::json;
/**
* @brief Concrete implementation of IDataNode backed by JSON
*
* Represents a node in the hierarchical data tree. Can have:
* - Children nodes (map of name -> node)
* - Own data (JSON value)
* - Path in the tree for identification
*/
class JsonDataNode : public IDataNode {
public:
/**
* @brief Create a node with name and optional data
* @param name Node name
* @param data Optional JSON data for this node
* @param parent Optional parent node (for path tracking)
* @param readOnly Whether this node is read-only (for config/)
*/
JsonDataNode(const std::string& name,
const json& data = json::object(),
JsonDataNode* parent = nullptr,
bool readOnly = false);
virtual ~JsonDataNode() = default;
// Tree navigation
std::unique_ptr<IDataNode> getChild(const std::string& name) override;
std::vector<std::string> getChildNames() override;
bool hasChildren() override;
// Exact search in children
std::vector<IDataNode*> getChildrenByName(const std::string& name) override;
bool hasChildrenByName(const std::string& name) const override;
IDataNode* getFirstChildByName(const std::string& name) override;
// Pattern matching search
std::vector<IDataNode*> getChildrenByNameMatch(const std::string& pattern) override;
bool hasChildrenByNameMatch(const std::string& pattern) const override;
IDataNode* getFirstChildByNameMatch(const std::string& pattern) override;
// Query by properties
std::vector<IDataNode*> queryByProperty(const std::string& propName,
const std::function<bool(const IDataValue&)>& predicate) override;
// Node's own data
std::unique_ptr<IDataValue> getData() const override;
bool hasData() const override;
void setData(std::unique_ptr<IDataValue> data) override;
// Typed data access
std::string getString(const std::string& name, const std::string& defaultValue = "") const override;
int getInt(const std::string& name, int defaultValue = 0) const override;
double getDouble(const std::string& name, double defaultValue = 0.0) const override;
bool getBool(const std::string& name, bool defaultValue = false) const override;
bool hasProperty(const std::string& name) const override;
// Hash system
std::string getDataHash() override;
std::string getTreeHash() override;
std::string getSubtreeHash(const std::string& childPath) override;
// Metadata
std::string getPath() const override;
std::string getName() const override;
std::string getNodeType() const override;
// Tree modification
void setChild(const std::string& name, std::unique_ptr<IDataNode> node) override;
bool removeChild(const std::string& name) override;
void clearChildren() override;
// Direct JSON access (for internal use by JsonDataTree)
const json& getJsonData() const { return m_data; }
json& getJsonData() { return m_data; }
const std::map<std::string, std::unique_ptr<JsonDataNode>>& getChildren() const { return m_children; }
private:
std::string m_name;
json m_data;
JsonDataNode* m_parent;
bool m_readOnly;
std::map<std::string, std::unique_ptr<JsonDataNode>> m_children;
// Helper methods
bool matchesPattern(const std::string& text, const std::string& pattern) const;
void collectMatchingNodes(const std::string& pattern, std::vector<IDataNode*>& results);
void collectNodesByProperty(const std::string& propName,
const std::function<bool(const IDataValue&)>& predicate,
std::vector<IDataNode*>& results);
std::string computeHash(const std::string& input) const;
void checkReadOnly() const;
};
} // namespace grove

View File

@ -0,0 +1,86 @@
#pragma once
#include "IDataTree.h"
#include "JsonDataNode.h"
#include <string>
#include <memory>
#include <functional>
#include <map>
#include <chrono>
#include <filesystem>
namespace grove {
/**
* @brief Concrete implementation of IDataTree backed by JSON files
*
* Manages three separate trees:
* - config/ : Read-only configuration loaded from files (hot-reload enabled)
* - data/ : Persistent player data (read-write, saved to disk)
* - runtime/ : Temporary runtime state (read-write, never saved)
*
* File structure:
* basePath/
* config/
* tanks.json
* weapons.json
* ...
* data/
* campaign.json
* unlocks.json
* ...
* runtime/ (in-memory only, not on disk)
*/
class JsonDataTree : public IDataTree {
public:
/**
* @brief Create a data tree from a base directory
* @param basePath Base directory containing config/, data/ subdirs
*/
explicit JsonDataTree(const std::string& basePath);
virtual ~JsonDataTree() = default;
// Tree access
std::unique_ptr<IDataNode> getRoot() override;
std::unique_ptr<IDataNode> getNode(const std::string& path) override;
// Separate roots
std::unique_ptr<IDataNode> getConfigRoot() override;
std::unique_ptr<IDataNode> getDataRoot() override;
std::unique_ptr<IDataNode> getRuntimeRoot() override;
// Save operations
bool saveData() override;
bool saveNode(const std::string& path) override;
// Hot-reload
bool checkForChanges() override;
bool reloadIfChanged() override;
void onTreeReloaded(std::function<void()> callback) override;
// Metadata
std::string getType() override;
private:
std::string m_basePath;
std::unique_ptr<JsonDataNode> m_root;
std::unique_ptr<JsonDataNode> m_configRoot;
std::unique_ptr<JsonDataNode> m_dataRoot;
std::unique_ptr<JsonDataNode> m_runtimeRoot;
std::map<std::string, std::filesystem::file_time_type> m_configFileTimes;
std::vector<std::function<void()>> m_reloadCallbacks;
// Helper methods
void loadConfigTree();
void loadDataTree();
void initializeRuntimeTree();
void scanDirectory(const std::string& dirPath, JsonDataNode* parentNode, bool readOnly);
json loadJsonFile(const std::string& filePath);
bool saveJsonFile(const std::string& filePath, const json& data);
void buildNodeFromJson(const std::string& name, const json& data, JsonDataNode* parentNode, bool readOnly);
json nodeToJson(const JsonDataNode* node);
void updateFileTimestamps(const std::string& dirPath);
};
} // namespace grove

View File

@ -0,0 +1,53 @@
#pragma once
#include "IDataValue.h"
#include <nlohmann/json.hpp>
#include <memory>
#include <string>
#include <vector>
namespace grove {
using json = nlohmann::json;
/**
* @brief Concrete implementation of IDataValue backed by nlohmann::json
*/
class JsonDataValue : public IDataValue {
public:
explicit JsonDataValue(const json& value);
explicit JsonDataValue(json&& value);
virtual ~JsonDataValue() = default;
// Type checking
bool isNull() const override;
bool isBool() const override;
bool isNumber() const override;
bool isString() const override;
bool isArray() const override;
bool isObject() const override;
// Value access with defaults
bool asBool(bool defaultValue = false) const override;
int asInt(int defaultValue = 0) const override;
double asDouble(double defaultValue = 0.0) const override;
std::string asString(const std::string& defaultValue = "") const override;
// Array/Object access
size_t size() const override;
std::unique_ptr<IDataValue> get(size_t index) const override;
std::unique_ptr<IDataValue> get(const std::string& key) const override;
bool has(const std::string& key) const override;
// Serialization
std::string toString() const override;
// Direct JSON access (for internal use)
const json& getJson() const { return m_value; }
json& getJson() { return m_value; }
private:
json m_value;
};
} // namespace grove

View File

@ -11,7 +11,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
/**
* @brief Factory for loading and creating modules from shared libraries (.so files)
@ -99,4 +99,4 @@ private:
void logModuleError(const std::string& operation, const std::string& details) const;
};
} // namespace warfactory
} // namespace grove

View File

@ -11,7 +11,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
/**
* @brief Factory for creating ModuleSystem implementations
@ -113,4 +113,4 @@ private:
static int detectCpuCores();
};
} // namespace warfactory
} // namespace grove

View File

@ -3,7 +3,7 @@
#include <random>
#include <cstdint>
namespace warfactory {
namespace grove {
/**
* @brief Centralized random number generator singleton
@ -86,4 +86,4 @@ public:
}
};
} // namespace warfactory
} // namespace grove

View File

@ -5,7 +5,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
class Resource {
private:
@ -34,4 +34,4 @@ public:
static Resource loadFromJson(const std::string& resource_id, const json& resource_data);
};
} // namespace warfactory
} // namespace grove

View File

@ -6,7 +6,7 @@
#include <string>
#include <memory>
namespace warfactory {
namespace grove {
/**
* @brief Singleton registry for all game resources with fast uint32_t ID lookup
@ -110,4 +110,4 @@ public:
#define GET_RESOURCE(id) warfactory::ResourceRegistry::getInstance().getResource(id)
#define VALID_RESOURCE(id) warfactory::ResourceRegistry::getInstance().isValidResourceId(id)
} // namespace warfactory
} // namespace grove

View File

@ -12,7 +12,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
/**
* @brief Sequential module system implementation for debug and testing
@ -84,4 +84,4 @@ public:
void setLogLevel(spdlog::level::level_enum level);
};
} // namespace warfactory
} // namespace grove

View File

@ -7,7 +7,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
class ASerializable;
@ -35,4 +35,4 @@ public:
void clear();
};
} // namespace warfactory
} // namespace grove

15
src/DataTreeFactory.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "grove/DataTreeFactory.h"
#include "grove/JsonDataTree.h"
#include <stdexcept>
namespace grove {
std::unique_ptr<IDataTree> DataTreeFactory::create(const std::string& type, const std::string& sourcePath) {
if (type == "json") {
return std::make_unique<JsonDataTree>(sourcePath);
}
throw std::runtime_error("Unknown data tree type: " + type);
}
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
namespace warfactory {
namespace grove {
DebugEngine::DebugEngine() {
// Create comprehensive logger with multiple sinks
@ -484,4 +484,4 @@ void DebugEngine::validateConfiguration() {
logger->trace("🚧 TODO: Implement comprehensive config validation");
}
} // namespace warfactory
} // namespace grove

View File

@ -6,7 +6,7 @@
using json = nlohmann::json;
namespace warfactory {
namespace grove {
std::unique_ptr<IEngine> EngineFactory::createEngine(const std::string& engineType) {
auto logger = getFactoryLogger();
@ -204,4 +204,4 @@ std::string EngineFactory::toLowercase(const std::string& str) {
return result;
}
} // namespace warfactory
} // namespace grove

View File

@ -11,7 +11,7 @@
// #include "LocalIO.h"
// #include "NetworkIO.h"
namespace warfactory {
namespace grove {
std::unique_ptr<IIO> IOFactory::create(const std::string& transportType, const std::string& instanceId) {
auto logger = getFactoryLogger();
@ -308,4 +308,4 @@ std::string IOFactory::generateEndpoint(IOType ioType) {
}
}
} // namespace warfactory
} // namespace grove

View File

@ -3,7 +3,7 @@
#include <iomanip>
#include <iostream>
namespace warfactory {
namespace grove {
// ========================================
// IUI INTERFACE IMPLEMENTATION - REQUESTS & EVENTS
@ -543,4 +543,4 @@ void ImGuiUI::renderLogConsole() {
ImGui::End();
}
} // namespace warfactory
} // namespace grove

View File

@ -6,7 +6,7 @@
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
namespace warfactory {
namespace grove {
// Factory function for IntraIOManager to avoid circular include
std::shared_ptr<IntraIO> createIntraIOInstance(const std::string& instanceId) {
@ -481,4 +481,4 @@ const std::string& IntraIO::getInstanceId() const {
return instanceId;
}
} // namespace warfactory
} // namespace grove

View File

@ -4,7 +4,7 @@
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
namespace warfactory {
namespace grove {
IntraIOManager::IntraIOManager() {
// Create logger
@ -266,4 +266,4 @@ IntraIOManager& IntraIOManager::getInstance() {
return instance;
}
} // namespace warfactory
} // namespace grove

343
src/JsonDataNode.cpp Normal file
View File

@ -0,0 +1,343 @@
#include "grove/JsonDataNode.h"
#include <regex>
#include <sstream>
#include <iomanip>
#include <openssl/sha.h>
#include <stdexcept>
namespace grove {
JsonDataNode::JsonDataNode(const std::string& name, const json& data, JsonDataNode* parent, bool readOnly)
: m_name(name), m_data(data), m_parent(parent), m_readOnly(readOnly) {
}
// ========================================
// TREE NAVIGATION
// ========================================
std::unique_ptr<IDataNode> JsonDataNode::getChild(const std::string& name) {
auto it = m_children.find(name);
if (it == m_children.end()) {
return nullptr;
}
// Return a copy wrapped in unique_ptr
return std::make_unique<JsonDataNode>(it->second->getName(),
it->second->getJsonData(),
this,
it->second->m_readOnly);
}
std::vector<std::string> JsonDataNode::getChildNames() {
std::vector<std::string> names;
names.reserve(m_children.size());
for (const auto& [name, _] : m_children) {
names.push_back(name);
}
return names;
}
bool JsonDataNode::hasChildren() {
return !m_children.empty();
}
// ========================================
// EXACT SEARCH IN CHILDREN
// ========================================
std::vector<IDataNode*> JsonDataNode::getChildrenByName(const std::string& name) {
std::vector<IDataNode*> results;
auto it = m_children.find(name);
if (it != m_children.end()) {
results.push_back(it->second.get());
}
return results;
}
bool JsonDataNode::hasChildrenByName(const std::string& name) const {
return m_children.find(name) != m_children.end();
}
IDataNode* JsonDataNode::getFirstChildByName(const std::string& name) {
auto it = m_children.find(name);
if (it != m_children.end()) {
return it->second.get();
}
return nullptr;
}
// ========================================
// PATTERN MATCHING SEARCH
// ========================================
bool JsonDataNode::matchesPattern(const std::string& text, const std::string& pattern) const {
// Convert wildcard pattern to regex
std::string regexPattern = pattern;
// Escape special regex characters except *
regexPattern = std::regex_replace(regexPattern, std::regex("\\."), "\\.");
regexPattern = std::regex_replace(regexPattern, std::regex("\\+"), "\\+");
regexPattern = std::regex_replace(regexPattern, std::regex("\\?"), "\\?");
regexPattern = std::regex_replace(regexPattern, std::regex("\\["), "\\[");
regexPattern = std::regex_replace(regexPattern, std::regex("\\]"), "\\]");
regexPattern = std::regex_replace(regexPattern, std::regex("\\^"), "\\^");
regexPattern = std::regex_replace(regexPattern, std::regex("\\$"), "\\$");
regexPattern = std::regex_replace(regexPattern, std::regex("\\("), "\\(");
regexPattern = std::regex_replace(regexPattern, std::regex("\\)"), "\\)");
regexPattern = std::regex_replace(regexPattern, std::regex("\\{"), "\\{");
regexPattern = std::regex_replace(regexPattern, std::regex("\\}"), "\\}");
regexPattern = std::regex_replace(regexPattern, std::regex("\\|"), "\\|");
// Convert * to .*
regexPattern = std::regex_replace(regexPattern, std::regex("\\*"), ".*");
// Match entire string
std::regex re("^" + regexPattern + "$");
return std::regex_match(text, re);
}
void JsonDataNode::collectMatchingNodes(const std::string& pattern, std::vector<IDataNode*>& results) {
// Check this node
if (matchesPattern(m_name, pattern)) {
results.push_back(this);
}
// Recursively check children
for (auto& [name, child] : m_children) {
child->collectMatchingNodes(pattern, results);
}
}
std::vector<IDataNode*> JsonDataNode::getChildrenByNameMatch(const std::string& pattern) {
std::vector<IDataNode*> results;
collectMatchingNodes(pattern, results);
return results;
}
bool JsonDataNode::hasChildrenByNameMatch(const std::string& pattern) const {
// Cast away const for search (doesn't modify state)
JsonDataNode* mutableThis = const_cast<JsonDataNode*>(this);
std::vector<IDataNode*> results;
mutableThis->collectMatchingNodes(pattern, results);
return !results.empty();
}
IDataNode* JsonDataNode::getFirstChildByNameMatch(const std::string& pattern) {
std::vector<IDataNode*> results;
collectMatchingNodes(pattern, results);
return results.empty() ? nullptr : results[0];
}
// ========================================
// QUERY BY PROPERTIES
// ========================================
void JsonDataNode::collectNodesByProperty(const std::string& propName,
const std::function<bool(const IDataValue&)>& predicate,
std::vector<IDataNode*>& results) {
// Check this node
if (hasProperty(propName)) {
auto value = getData();
if (value->has(propName)) {
auto propValue = value->get(propName);
if (predicate(*propValue)) {
results.push_back(this);
}
}
}
// Recursively check children
for (auto& [name, child] : m_children) {
child->collectNodesByProperty(propName, predicate, results);
}
}
std::vector<IDataNode*> JsonDataNode::queryByProperty(const std::string& propName,
const std::function<bool(const IDataValue&)>& predicate) {
std::vector<IDataNode*> results;
collectNodesByProperty(propName, predicate, results);
return results;
}
// ========================================
// NODE'S OWN DATA
// ========================================
std::unique_ptr<IDataValue> JsonDataNode::getData() const {
return std::make_unique<JsonDataValue>(m_data);
}
bool JsonDataNode::hasData() const {
return !m_data.is_null() && !m_data.empty();
}
void JsonDataNode::setData(std::unique_ptr<IDataValue> data) {
checkReadOnly();
// Extract JSON from JsonDataValue
if (auto* jsonValue = dynamic_cast<JsonDataValue*>(data.get())) {
m_data = jsonValue->getJson();
} else {
throw std::runtime_error("JsonDataNode requires JsonDataValue");
}
}
// ========================================
// TYPED DATA ACCESS
// ========================================
std::string JsonDataNode::getString(const std::string& name, const std::string& defaultValue) const {
if (!m_data.is_object() || !m_data.contains(name)) {
return defaultValue;
}
const auto& val = m_data[name];
return val.is_string() ? val.get<std::string>() : defaultValue;
}
int JsonDataNode::getInt(const std::string& name, int defaultValue) const {
if (!m_data.is_object() || !m_data.contains(name)) {
return defaultValue;
}
const auto& val = m_data[name];
return val.is_number() ? val.get<int>() : defaultValue;
}
double JsonDataNode::getDouble(const std::string& name, double defaultValue) const {
if (!m_data.is_object() || !m_data.contains(name)) {
return defaultValue;
}
const auto& val = m_data[name];
return val.is_number() ? val.get<double>() : defaultValue;
}
bool JsonDataNode::getBool(const std::string& name, bool defaultValue) const {
if (!m_data.is_object() || !m_data.contains(name)) {
return defaultValue;
}
const auto& val = m_data[name];
return val.is_boolean() ? val.get<bool>() : defaultValue;
}
bool JsonDataNode::hasProperty(const std::string& name) const {
return m_data.is_object() && m_data.contains(name);
}
// ========================================
// HASH SYSTEM
// ========================================
std::string JsonDataNode::computeHash(const std::string& input) const {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(input.c_str()), input.length(), hash);
std::stringstream ss;
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
}
return ss.str();
}
std::string JsonDataNode::getDataHash() {
std::string dataStr = m_data.dump();
return computeHash(dataStr);
}
std::string JsonDataNode::getTreeHash() {
// Combine data hash with all children hashes
std::string combined = getDataHash();
for (const auto& [name, child] : m_children) {
combined += name + ":" + child->getTreeHash();
}
return computeHash(combined);
}
std::string JsonDataNode::getSubtreeHash(const std::string& childPath) {
// Parse path and navigate
size_t pos = childPath.find('/');
if (pos == std::string::npos) {
// Direct child
auto it = m_children.find(childPath);
if (it != m_children.end()) {
return it->second->getTreeHash();
}
return "";
} else {
// Nested path
std::string firstPart = childPath.substr(0, pos);
std::string rest = childPath.substr(pos + 1);
auto it = m_children.find(firstPart);
if (it != m_children.end()) {
return it->second->getSubtreeHash(rest);
}
return "";
}
}
// ========================================
// METADATA
// ========================================
std::string JsonDataNode::getPath() const {
if (m_parent == nullptr) {
return m_name;
}
std::string parentPath = m_parent->getPath();
return parentPath.empty() ? m_name : parentPath + "/" + m_name;
}
std::string JsonDataNode::getName() const {
return m_name;
}
std::string JsonDataNode::getNodeType() const {
return "JsonDataNode";
}
// ========================================
// TREE MODIFICATION
// ========================================
void JsonDataNode::checkReadOnly() const {
if (m_readOnly) {
throw std::runtime_error("Cannot modify read-only node: " + getPath());
}
}
void JsonDataNode::setChild(const std::string& name, std::unique_ptr<IDataNode> node) {
checkReadOnly();
// Extract JsonDataNode
if (auto* jsonNode = dynamic_cast<JsonDataNode*>(node.get())) {
auto newNode = std::make_unique<JsonDataNode>(
jsonNode->getName(),
jsonNode->getJsonData(),
this,
m_readOnly // Inherit read-only status
);
// Copy children recursively
for (const auto& [childName, child] : jsonNode->getChildren()) {
newNode->setChild(childName, std::make_unique<JsonDataNode>(
child->getName(),
child->getJsonData(),
newNode.get(),
m_readOnly
));
}
m_children[name] = std::move(newNode);
} else {
throw std::runtime_error("JsonDataNode requires JsonDataNode child");
}
}
bool JsonDataNode::removeChild(const std::string& name) {
checkReadOnly();
return m_children.erase(name) > 0;
}
void JsonDataNode::clearChildren() {
checkReadOnly();
m_children.clear();
}
} // namespace grove

352
src/JsonDataTree.cpp Normal file
View File

@ -0,0 +1,352 @@
#include "grove/JsonDataTree.h"
#include <fstream>
#include <stdexcept>
#include <iostream>
namespace grove {
namespace fs = std::filesystem;
JsonDataTree::JsonDataTree(const std::string& basePath)
: m_basePath(basePath) {
// Create root node
m_root = std::make_unique<JsonDataNode>("", json::object(), nullptr, false);
// Load three sub-trees
loadConfigTree();
loadDataTree();
initializeRuntimeTree();
// Attach to root
m_root->setChild("config", std::move(m_configRoot));
m_root->setChild("data", std::move(m_dataRoot));
m_root->setChild("runtime", std::move(m_runtimeRoot));
}
// ========================================
// TREE ACCESS
// ========================================
std::unique_ptr<IDataNode> JsonDataTree::getRoot() {
// Return copy of root
return std::make_unique<JsonDataNode>("", m_root->getJsonData(), nullptr, false);
}
std::unique_ptr<IDataNode> JsonDataTree::getNode(const std::string& path) {
if (path.empty()) {
return getRoot();
}
// Split path and navigate
JsonDataNode* current = m_root.get();
std::string remaining = path;
while (!remaining.empty()) {
size_t pos = remaining.find('/');
std::string part = (pos == std::string::npos) ? remaining : remaining.substr(0, pos);
remaining = (pos == std::string::npos) ? "" : remaining.substr(pos + 1);
auto child = current->getFirstChildByName(part);
if (!child) {
return nullptr;
}
current = static_cast<JsonDataNode*>(child);
}
return std::make_unique<JsonDataNode>(current->getName(),
current->getJsonData(),
nullptr,
false);
}
// ========================================
// SEPARATE ROOTS
// ========================================
std::unique_ptr<IDataNode> JsonDataTree::getConfigRoot() {
auto configNode = m_root->getFirstChildByName("config");
if (!configNode) {
return nullptr;
}
auto* jsonNode = static_cast<JsonDataNode*>(configNode);
return std::make_unique<JsonDataNode>(jsonNode->getName(),
jsonNode->getJsonData(),
nullptr,
true); // Read-only
}
std::unique_ptr<IDataNode> JsonDataTree::getDataRoot() {
auto dataNode = m_root->getFirstChildByName("data");
if (!dataNode) {
return nullptr;
}
auto* jsonNode = static_cast<JsonDataNode*>(dataNode);
return std::make_unique<JsonDataNode>(jsonNode->getName(),
jsonNode->getJsonData(),
nullptr,
false);
}
std::unique_ptr<IDataNode> JsonDataTree::getRuntimeRoot() {
auto runtimeNode = m_root->getFirstChildByName("runtime");
if (!runtimeNode) {
return nullptr;
}
auto* jsonNode = static_cast<JsonDataNode*>(runtimeNode);
return std::make_unique<JsonDataNode>(jsonNode->getName(),
jsonNode->getJsonData(),
nullptr,
false);
}
// ========================================
// SAVE OPERATIONS
// ========================================
bool JsonDataTree::saveData() {
try {
std::string dataPath = m_basePath + "/data";
auto dataNode = m_root->getFirstChildByName("data");
if (!dataNode) {
return false;
}
auto* jsonNode = static_cast<JsonDataNode*>(dataNode);
json dataJson = nodeToJson(jsonNode);
// Save each top-level child as separate file
for (const auto& [name, child] : jsonNode->getChildren()) {
std::string filePath = dataPath + "/" + name + ".json";
json childJson = nodeToJson(child.get());
if (!saveJsonFile(filePath, childJson)) {
return false;
}
}
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to save data: " << e.what() << std::endl;
return false;
}
}
bool JsonDataTree::saveNode(const std::string& path) {
// Only allow saving data/ paths
if (path.find("data/") != 0) {
std::cerr << "Can only save nodes under data/: " << path << std::endl;
return false;
}
try {
auto node = getNode(path);
if (!node) {
return false;
}
// Extract filename from path
size_t lastSlash = path.find_last_of('/');
std::string filename = (lastSlash == std::string::npos) ? path : path.substr(lastSlash + 1);
std::string filePath = m_basePath + "/" + path + ".json";
auto* jsonNode = dynamic_cast<JsonDataNode*>(node.get());
if (!jsonNode) {
return false;
}
json nodeJson = nodeToJson(jsonNode);
return saveJsonFile(filePath, nodeJson);
} catch (const std::exception& e) {
std::cerr << "Failed to save node " << path << ": " << e.what() << std::endl;
return false;
}
}
// ========================================
// HOT-RELOAD
// ========================================
bool JsonDataTree::checkForChanges() {
std::string configPath = m_basePath + "/config";
try {
for (const auto& [filePath, lastTime] : m_configFileTimes) {
if (!fs::exists(filePath)) {
return true; // File deleted
}
auto currentTime = fs::last_write_time(filePath);
if (currentTime != lastTime) {
return true; // File modified
}
}
// Check for new files
if (fs::exists(configPath)) {
for (const auto& entry : fs::directory_iterator(configPath)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
if (m_configFileTimes.find(entry.path().string()) == m_configFileTimes.end()) {
return true; // New file
}
}
}
}
return false;
} catch (const std::exception& e) {
std::cerr << "Error checking for changes: " << e.what() << std::endl;
return false;
}
}
bool JsonDataTree::reloadIfChanged() {
if (!checkForChanges()) {
return false;
}
try {
loadConfigTree();
// Trigger callbacks
for (auto& callback : m_reloadCallbacks) {
callback();
}
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to reload config: " << e.what() << std::endl;
return false;
}
}
void JsonDataTree::onTreeReloaded(std::function<void()> callback) {
m_reloadCallbacks.push_back(callback);
}
// ========================================
// METADATA
// ========================================
std::string JsonDataTree::getType() {
return "JsonDataTree";
}
// ========================================
// HELPER METHODS
// ========================================
void JsonDataTree::loadConfigTree() {
std::string configPath = m_basePath + "/config";
m_configRoot = std::make_unique<JsonDataNode>("config", json::object(), nullptr, true);
if (fs::exists(configPath) && fs::is_directory(configPath)) {
scanDirectory(configPath, m_configRoot.get(), true);
updateFileTimestamps(configPath);
}
}
void JsonDataTree::loadDataTree() {
std::string dataPath = m_basePath + "/data";
m_dataRoot = std::make_unique<JsonDataNode>("data", json::object(), nullptr, false);
if (fs::exists(dataPath) && fs::is_directory(dataPath)) {
scanDirectory(dataPath, m_dataRoot.get(), false);
} else {
// Create data directory if it doesn't exist
fs::create_directories(dataPath);
}
}
void JsonDataTree::initializeRuntimeTree() {
m_runtimeRoot = std::make_unique<JsonDataNode>("runtime", json::object(), nullptr, false);
}
void JsonDataTree::scanDirectory(const std::string& dirPath, JsonDataNode* parentNode, bool readOnly) {
for (const auto& entry : fs::directory_iterator(dirPath)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
std::string filename = entry.path().stem().string();
json data = loadJsonFile(entry.path().string());
buildNodeFromJson(filename, data, parentNode, readOnly);
} else if (entry.is_directory()) {
// Create child node for subdirectory
auto childNode = std::make_unique<JsonDataNode>(
entry.path().filename().string(),
json::object(),
parentNode,
readOnly
);
scanDirectory(entry.path().string(), childNode.get(), readOnly);
parentNode->setChild(entry.path().filename().string(), std::move(childNode));
}
}
}
json JsonDataTree::loadJsonFile(const std::string& filePath) {
std::ifstream file(filePath);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file: " + filePath);
}
json data;
file >> data;
return data;
}
bool JsonDataTree::saveJsonFile(const std::string& filePath, const json& data) {
try {
// Ensure directory exists
fs::path path(filePath);
if (path.has_parent_path()) {
fs::create_directories(path.parent_path());
}
std::ofstream file(filePath);
if (!file.is_open()) {
return false;
}
file << data.dump(2); // Pretty print with 2-space indent
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to save JSON file: " << e.what() << std::endl;
return false;
}
}
void JsonDataTree::buildNodeFromJson(const std::string& name, const json& data, JsonDataNode* parentNode, bool readOnly) {
auto node = std::make_unique<JsonDataNode>(name, data, parentNode, readOnly);
// If data is an object with children, create child nodes
if (data.is_object()) {
for (auto& [key, value] : data.items()) {
if (value.is_object() || value.is_array()) {
buildNodeFromJson(key, value, node.get(), readOnly);
}
}
}
parentNode->setChild(name, std::move(node));
}
json JsonDataTree::nodeToJson(const JsonDataNode* node) {
json result = node->getJsonData();
// Add children
for (const auto& [name, child] : node->getChildren()) {
result[name] = nodeToJson(child.get());
}
return result;
}
void JsonDataTree::updateFileTimestamps(const std::string& dirPath) {
m_configFileTimes.clear();
for (const auto& entry : fs::directory_iterator(dirPath)) {
if (entry.is_regular_file() && entry.path().extension() == ".json") {
m_configFileTimes[entry.path().string()] = fs::last_write_time(entry.path());
}
}
}
} // namespace grove

97
src/JsonDataValue.cpp Normal file
View File

@ -0,0 +1,97 @@
#include "grove/JsonDataValue.h"
namespace grove {
JsonDataValue::JsonDataValue(const json& value) : m_value(value) {}
JsonDataValue::JsonDataValue(json&& value) : m_value(std::move(value)) {}
// Type checking
bool JsonDataValue::isNull() const {
return m_value.is_null();
}
bool JsonDataValue::isBool() const {
return m_value.is_boolean();
}
bool JsonDataValue::isNumber() const {
return m_value.is_number();
}
bool JsonDataValue::isString() const {
return m_value.is_string();
}
bool JsonDataValue::isArray() const {
return m_value.is_array();
}
bool JsonDataValue::isObject() const {
return m_value.is_object();
}
// Value access with defaults
bool JsonDataValue::asBool(bool defaultValue) const {
if (!m_value.is_boolean()) {
return defaultValue;
}
return m_value.get<bool>();
}
int JsonDataValue::asInt(int defaultValue) const {
if (!m_value.is_number()) {
return defaultValue;
}
return m_value.get<int>();
}
double JsonDataValue::asDouble(double defaultValue) const {
if (!m_value.is_number()) {
return defaultValue;
}
return m_value.get<double>();
}
std::string JsonDataValue::asString(const std::string& defaultValue) const {
if (!m_value.is_string()) {
return defaultValue;
}
return m_value.get<std::string>();
}
// Array/Object access
size_t JsonDataValue::size() const {
if (m_value.is_array() || m_value.is_object()) {
return m_value.size();
}
return 0;
}
std::unique_ptr<IDataValue> JsonDataValue::get(size_t index) const {
if (!m_value.is_array() || index >= m_value.size()) {
return std::make_unique<JsonDataValue>(json(nullptr));
}
return std::make_unique<JsonDataValue>(m_value[index]);
}
std::unique_ptr<IDataValue> JsonDataValue::get(const std::string& key) const {
if (!m_value.is_object() || !m_value.contains(key)) {
return std::make_unique<JsonDataValue>(json(nullptr));
}
return std::make_unique<JsonDataValue>(m_value[key]);
}
bool JsonDataValue::has(const std::string& key) const {
if (!m_value.is_object()) {
return false;
}
return m_value.contains(key);
}
// Serialization
std::string JsonDataValue::toString() const {
return m_value.dump();
}
} // namespace grove

View File

@ -7,7 +7,7 @@
namespace fs = std::filesystem;
namespace warfactory {
namespace grove {
ModuleFactory::ModuleFactory() {
// Create logger with file and console output
@ -506,4 +506,4 @@ void ModuleFactory::logModuleError(const std::string& operation, const std::stri
logger->error("❌ Module {} error: {}", operation, details);
}
} // namespace warfactory
} // namespace grove

View File

@ -10,7 +10,7 @@
// #include "ThreadPoolModuleSystem.h"
// #include "ClusterModuleSystem.h"
namespace warfactory {
namespace grove {
std::unique_ptr<IModuleSystem> ModuleSystemFactory::create(const std::string& strategy) {
auto logger = getFactoryLogger();
@ -236,4 +236,4 @@ int ModuleSystemFactory::detectCpuCores() {
return cores;
}
} // namespace warfactory
} // namespace grove

View File

@ -1,7 +1,7 @@
#include <grove/ResourceRegistry.h>
#include <algorithm>
namespace warfactory {
namespace grove {
// Static member initialization
std::unique_ptr<ResourceRegistry> ResourceRegistry::instance = nullptr;
@ -117,4 +117,4 @@ void ResourceRegistry::clear() {
next_id = 1;
}
} // namespace warfactory
} // namespace grove

View File

@ -3,7 +3,7 @@
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
namespace warfactory {
namespace grove {
SequentialModuleSystem::SequentialModuleSystem() {
// Create logger with file and console output
@ -273,4 +273,4 @@ void SequentialModuleSystem::validateModule() const {
}
}
} // namespace warfactory
} // namespace grove