Tiny robust json wrapper to get values
Some checks are pending
Build-StoryEditor / build_linux (push) Waiting to run
Build-StoryEditor / build_win32 (push) Waiting to run

This commit is contained in:
anthony@rabine.fr 2025-10-26 15:41:23 +01:00
parent c594e01912
commit 397da70d83
11 changed files with 140 additions and 384 deletions

View file

@ -20,14 +20,6 @@ BaseNode::~BaseNode()
}
std::string BaseNode::GetEntryLabel(const std::string &id)
{
std::stringstream ss;
ss << ".nodeEntry" << std::setw(4) << std::setfill('0') << id;
return ss.str();
}
void BaseNode::FromJson(const nlohmann::json &j)
{
try
@ -35,14 +27,14 @@ void BaseNode::FromJson(const nlohmann::json &j)
m_uuid = j["uuid"].get<std::string>();
m_internal_data = j["internal-data"];
m_type = j["type"].get<std::string>();
m_title = j.value("title", "Default node");
m_title = JsonReader::get<std::string>(j, "title", "Default node");
nlohmann::json posJson = j["position"];
SetPosition(posJson["x"].get<double>(), posJson["y"].get<double>());
}
catch (std::exception& e)
{
std::cout << "ERROR: " << e.what() << std::endl;
std::cout << "[BASE NODE] ERROR: " << e.what() << std::endl;
}
}

View file

@ -6,6 +6,7 @@
#include <string>
#include "json.hpp"
#include "json_reader.h"
#include "i_story_page.h"
#include "i_story_project.h"
#include "story_options.h"
@ -67,8 +68,6 @@ public:
BaseNode(const std::string &type, const std::string &typeName, Behavior behavior = BEHAVIOR_EXECUTION);
virtual ~BaseNode();
static std::string GetEntryLabel(const std::string &id);
virtual void Initialize() = 0;
void SetPosition(float x, float y);
@ -77,10 +76,6 @@ public:
virtual float GetX() const;
virtual float GetY() const;
std::string GetMyEntryLabel() const {
return GetEntryLabel(m_uuid);
}
// Coded type, internal use
std::string GetType() const
{

View file

@ -27,10 +27,10 @@ public:
static InputBinding FromJson(const nlohmann::json& j) {
InputBinding ib;
ib.paramName = j.value("paramName", "");
std::string modeStr = j.value("mode", "connected");
ib.paramName = JsonReader::get<std::string>(j,"paramName", "");
std::string modeStr = JsonReader::get<std::string>(j, "mode", "connected");
ib.mode = (modeStr == "constant") ? MODE_CONSTANT : MODE_CONNECTED;
ib.constantValue = j.value("constantValue", "");
ib.constantValue = JsonReader::get<std::string>(j, "constantValue", "");
return ib;
}
};
@ -48,8 +48,8 @@ public:
static OutputMapping FromJson(const nlohmann::json& j) {
OutputMapping om;
om.returnValueName = j.value("returnValueName", "");
om.targetVariable = j.value("targetVariable", "");
om.returnValueName = JsonReader::get<std::string>(j, "returnValueName", "");
om.targetVariable = JsonReader::get<std::string>(j, "targetVariable", "");
return om;
}
};

View file

@ -21,9 +21,9 @@ public:
static Parameter FromJson(const nlohmann::json& j) {
Parameter p;
p.name = j.value("name", "");
p.type = j.value("type", "int");
p.defaultValue = j.value("defaultValue", "");
p.name = JsonReader::get<std::string>(j, "name", "");
p.type = JsonReader::get<std::string>(j, "type", "int");
p.defaultValue = JsonReader::get<std::string>(j, "defaultValue", "");
return p;
}
};

View file

@ -19,8 +19,8 @@ public:
static ReturnValue FromJson(const nlohmann::json& j) {
ReturnValue rv;
rv.name = j.value("name", "");
rv.type = j.value("type", "int");
rv.name = JsonReader::get<std::string>(j, "name", "");
rv.type = JsonReader::get<std::string>(j, "type", "int");
return rv;
}
};
@ -32,6 +32,8 @@ public:
SetWeight(900); // High weight, near the end
SetBehavior(BaseNode::BEHAVIOR_EXECUTION);
SetupExecutionPorts(true, 0, true); // Has input, no output (it's the end)
SaveData();
}
void Initialize() override {
@ -39,7 +41,7 @@ public:
nlohmann::json j = GetInternalData();
m_returnValues.clear();
m_exitLabel = j.value("exitLabel", "Return");
m_exitLabel = JsonReader::get<std::string>(j, "exitLabel", "Return");
if (j.contains("returnValues") && j["returnValues"].is_array()) {
for (const auto& rvJson : j["returnValues"]) {

View file

@ -13,6 +13,7 @@
#include "sys_lib.h"
#include "assembly_generator_chip32_tac.h"
#include "nodes_factory.h"
#include "json_reader.h"
StoryProject::StoryProject(ILogger &log)
: m_log(log)
@ -282,8 +283,6 @@ bool StoryProject::ModelFromJson(const nlohmann::json &model, NodesFactory &fact
std::string type = element["type"].get<std::string>();
std::cout << "!!!!!!!!!!!!!!!!!" << type << std::endl;
auto n = factory.CreateNode(type);
if (n)
{

View file

@ -0,0 +1,64 @@
#pragma once
#include <json.hpp>
#include <string>
#include <iostream>
using json = nlohmann::json;
class JsonReader {
public:
/**
* @brief Récupère une valeur d'un objet JSON.
* * Cette fonction est similaire à value(), mais ajoute un logging et
* lance optionnellement une exception si la clé est manquante ou si la conversion échoue.
* * @tparam ValueType Le type C++ de la valeur attendue.
* @param j L'objet JSON dans lequel chercher.
* @param key La clé (champ) à chercher.
* @param defaultValue La valeur à retourner en cas d'échec ou de clé manquante.
* @param logOnError Si true, affiche un message d'erreur sur cerr.
* @param throwOnError Si true, lance une std::runtime_error en cas d'échec.
* @return ValueType La valeur trouvée ou la defaultValue.
*/
template<typename ValueType>
static ValueType get(
const json& j,
const std::string& key,
const ValueType& defaultValue,
bool logOnError = true,
bool throwOnError = false
) {
// 1. Vérification de la présence de la clé
if (!j.contains(key)) {
const std::string error_msg = "Clé '" + key + "' manquante dans l'objet JSON. Utilisation de la valeur par défaut.";
if (logOnError) {
std::cout << "⚠️ ERREUR (JsonGetter) : " << error_msg << std::endl;
}
if (throwOnError) {
throw std::runtime_error(error_msg);
}
return defaultValue;
}
// 2. Récupération de la valeur avec gestion de l'erreur de type
try {
// Utiliser 'at()' est plus sûr car il lève une exception en cas de type incorrect
// si la conversion n'est pas possible, ce qui est mieux que value() pour le diagnostic.
return j.at(key).get<ValueType>();
} catch (const nlohmann::json::type_error& e) {
// Le type existe, mais il est incompatible (ex: string au lieu de int)
const std::string error_msg = "Erreur de type pour la clé '" + key + "'. Attendu: " + typeid(ValueType).name() + ". Détails: " + e.what() + ". Utilisation de la valeur par défaut.";
if (logOnError) {
std::cout << "❌ ERREUR (JsonGetter) : " << error_msg << std::endl;
}
if (throwOnError) {
throw std::runtime_error(error_msg);
}
return defaultValue;
}
// Note: l'exception out_of_range est gérée par le 'contains' ci-dessus.
}
};

View file

@ -1,186 +0,0 @@
#include "json_wrapper.h"
#include "json.hpp"
#include <stdexcept>
#include <sstream>
#include <exception>
#include <iostream>
using json = nlohmann::json;
class JsonWrapper::Impl {
public:
json j;
};
JsonWrapper::JsonWrapper() : impl(std::make_unique<Impl>()) {
impl->j = json::object();
}
JsonWrapper::JsonWrapper(JsonWrapper&& other) noexcept : impl(std::move(other.impl)) {}
JsonWrapper& JsonWrapper::operator=(JsonWrapper&& other) noexcept {
if (this != &other) {
impl = std::move(other.impl);
}
return *this;
}
JsonWrapper::JsonWrapper(const std::string& jsonString) : impl(std::make_unique<Impl>()) {
try {
impl->j = json::parse(jsonString);
} catch (...) {
impl->j = json::object();
}
}
JsonWrapper::~JsonWrapper() = default;
bool JsonWrapper::hasKey(const std::string& key) const {
return impl->j.contains(key);
}
std::optional<std::string> JsonWrapper::getString(const std::string& key, std::string& errorKey) const {
try {
return impl->j.at(key).get<std::string>();
} catch (...) {
errorKey = key;
return std::nullopt;
}
}
std::optional<int> JsonWrapper::getInt(const std::string& key, std::string& errorKey) const {
try {
return impl->j.at(key).get<int>();
} catch (...) {
errorKey = key;
return std::nullopt;
}
}
std::optional<double> JsonWrapper::getDouble(const std::string& key, std::string& errorKey) const {
try {
return impl->j.at(key).get<double>();
} catch (...) {
errorKey = key;
return std::nullopt;
}
}
std::optional<bool> JsonWrapper::getBool(const std::string& key, std::string& errorKey) const {
try {
return impl->j.at(key).get<bool>();
} catch (...) {
errorKey = key;
return std::nullopt;
}
}
void JsonWrapper::setString(const std::string& key, const std::string& value) {
impl->j[key] = value;
}
void JsonWrapper::setInt(const std::string& key, int value) {
impl->j[key] = value;
}
void JsonWrapper::setDouble(const std::string& key, double value) {
impl->j[key] = value;
}
void JsonWrapper::setBool(const std::string& key, bool value) {
impl->j[key] = value;
}
void JsonWrapper::setArrayImpl(const std::string& key, const void* values, size_t typeHash) {
// Ici on doit caster et stocker dans json, mais sans RTTI avancé on limite
// Par exemple on peut specialiser cette méthode pour les types courants
if (typeHash == typeid(int).hash_code()) {
const auto& v = *reinterpret_cast<const std::vector<int>*>(values);
impl->j[key] = v;
} else if (typeHash == typeid(double).hash_code()) {
const auto& v = *reinterpret_cast<const std::vector<double>*>(values);
impl->j[key] = v;
} else if (typeHash == typeid(std::string).hash_code()) {
const auto& v = *reinterpret_cast<const std::vector<std::string>*>(values);
impl->j[key] = v;
} else if (typeHash == typeid(bool).hash_code()) {
const auto& v = *reinterpret_cast<const std::vector<bool>*>(values);
impl->j[key] = v;
} else {
// Pour les types non pris en charge, on peut lever une exception ou ignorer
throw std::runtime_error("Type non supporté pour setArray");
}
}
template <typename T>
std::optional<std::vector<T>> JsonWrapper::getArrayImpl(const std::string& key, std::string& errorKey) const {
try {
return impl->j.at(key).get<std::vector<T>>();
} catch (...) {
errorKey = key;
return std::nullopt;
}
}
template <typename T>
T JsonWrapper::getImpl(const std::string& key, std::string& errorKey) const {
try {
return impl->j.at(key).get<T>();
} catch (...) {
errorKey = key;
throw;
}
}
template <typename T>
T JsonWrapper::asImpl(std::string& errorKey) const {
try {
return impl->j.get<T>();
} catch (...) {
errorKey = "[root]";
throw;
}
}
JsonWrapper JsonWrapper::fromImpl(const void* value, size_t typeHash) {
JsonWrapper wrapper;
if (typeHash == typeid(int).hash_code()) {
wrapper.impl->j = *reinterpret_cast<const int*>(value);
} else if (typeHash == typeid(double).hash_code()) {
wrapper.impl->j = *reinterpret_cast<const double*>(value);
} else if (typeHash == typeid(std::string).hash_code()) {
wrapper.impl->j = *reinterpret_cast<const std::string*>(value);
} else if (typeHash == typeid(bool).hash_code()) {
wrapper.impl->j = *reinterpret_cast<const bool*>(value);
} else {
// Si cest un type complexe, il faut une surcharge ou un specialization dans le .cpp
// Exemple: si T a to_json/from_json, on peut faire une conversion nlohmann::json value = T;
// Ici on fait un cast "générique" (moins safe)
// On peut faire un throw ici pour forcer lutilisateur à spécialiser.
throw std::runtime_error("Type non supporté pour from()");
}
return std::move(wrapper);
}
// Explicit instantiations for getArrayImpl, getImpl, asImpl for common types
template std::optional<std::vector<int>> JsonWrapper::getArrayImpl<int>(const std::string&, std::string&) const;
template std::optional<std::vector<double>> JsonWrapper::getArrayImpl<double>(const std::string&, std::string&) const;
template std::optional<std::vector<std::string>> JsonWrapper::getArrayImpl<std::string>(const std::string&, std::string&) const;
template std::optional<std::vector<bool>> JsonWrapper::getArrayImpl<bool>(const std::string&, std::string&) const;
template int JsonWrapper::getImpl<int>(const std::string&, std::string&) const;
template double JsonWrapper::getImpl<double>(const std::string&, std::string&) const;
template std::string JsonWrapper::getImpl<std::string>(const std::string&, std::string&) const;
template bool JsonWrapper::getImpl<bool>(const std::string&, std::string&) const;
template int JsonWrapper::asImpl<int>(std::string&) const;
template double JsonWrapper::asImpl<double>(std::string&) const;
template std::string JsonWrapper::asImpl<std::string>(std::string&) const;
template bool JsonWrapper::asImpl<bool>(std::string&) const;
std::string JsonWrapper::dump(int indent) const {
if (indent < 0) return impl->j.dump();
return impl->j.dump(indent);
}

View file

@ -1,82 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <memory>
class JsonWrapper {
public:
JsonWrapper();
explicit JsonWrapper(const std::string& jsonString);
~JsonWrapper();
// Ajout move constructor et move assign
JsonWrapper(JsonWrapper&&) noexcept;
JsonWrapper& operator=(JsonWrapper&&) noexcept;
// Supprimer copie pour éviter erreur
JsonWrapper(const JsonWrapper&) = delete;
JsonWrapper& operator=(const JsonWrapper&) = delete;
bool hasKey(const std::string& key) const;
// Valeurs simples
std::optional<std::string> getString(const std::string& key, std::string& errorKey) const;
std::optional<int> getInt(const std::string& key, std::string& errorKey) const;
std::optional<double> getDouble(const std::string& key, std::string& errorKey) const;
std::optional<bool> getBool(const std::string& key, std::string& errorKey) const;
void setString(const std::string& key, const std::string& value);
void setInt(const std::string& key, int value);
void setDouble(const std::string& key, double value);
void setBool(const std::string& key, bool value);
// Array génériques
template <typename T>
void setArray(const std::string& key, const std::vector<T>& values) {
setArrayImpl(key, reinterpret_cast<const void*>(&values), typeid(T).hash_code());
}
template <typename T>
std::optional<std::vector<T>> getArray(const std::string& key, std::string& errorKey) const {
return getArrayImpl<T>(key, errorKey);
}
// Sérialisation/Désérialisation générique
template <typename T>
T get(const std::string& key, std::string& errorKey) const {
return getImpl<T>(key, errorKey);
}
template <typename T>
T as(std::string& errorKey) const {
return asImpl<T>(errorKey);
}
template <typename T>
static JsonWrapper from(const T& value) {
return fromImpl(reinterpret_cast<const void*>(&value), typeid(T).hash_code());
}
// Dump JSON
std::string dump(int indent = -1) const;
private:
class Impl;
std::unique_ptr<Impl> impl;
// Implémentations privées non template (définies dans .cpp)
void setArrayImpl(const std::string& key, const void* values, size_t typeHash);
template <typename T>
std::optional<std::vector<T>> getArrayImpl(const std::string& key, std::string& errorKey) const;
template <typename T>
T getImpl(const std::string& key, std::string& errorKey) const;
template <typename T>
T asImpl(std::string& errorKey) const;
static JsonWrapper fromImpl(const void* value, size_t typeHash);
};

View file

@ -48,8 +48,6 @@ add_executable(${PROJECT_NAME}
../story-manager/src/nodes/break_node.cpp
../story-manager/src/nodes/continue_node.cpp
../story-manager/src/utils/json_wrapper.cpp
../chip32/chip32_assembler.cpp
../chip32/chip32_vm.c
../chip32/chip32_binary_format.c

View file

@ -1,101 +1,75 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "json_wrapper.h"
#include <catch2/matchers/catch_matchers_vector.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "json_reader.h"
#include "json.hpp"
using Catch::Matchers::WithinRel;
using namespace nlohmann;
TEST_CASE("JsonWrapper basic types", "[json]") {
JsonWrapper json;
json.setString("name", "Alice");
json.setInt("age", 30);
json.setDouble("pi", 3.14159);
json.setBool("is_valid", true);
using namespace Catch::Matchers;
std::string err;
TEST_CASE("JsonReader::get - Scénarios de base", "[JsonReader]") {
json data = R"({
"nom": "Bob",
"age": 42,
"is_admin": true
})"_json;
SECTION("Get string") {
auto name = json.getString("name", err);
REQUIRE(name.has_value());
REQUIRE(name.value() == "Alice");
// --- Scénario 1 : Succès (Clé présente, Type correct) ---
SECTION("Succès - Récupération d'un entier") {
int age = JsonReader::get<int>(data, "age", 0, false, false);
REQUIRE(age == 42);
}
SECTION("Get int") {
auto age = json.getInt("age", err);
REQUIRE(age.has_value());
REQUIRE(age.value() == 30);
SECTION("Succès - Récupération d'une chaîne") {
std::string nom = JsonReader::get<std::string>(data, "nom", "N/A", false, false);
REQUIRE(nom == "Bob");
}
SECTION("Get double") {
auto pi = json.getDouble("pi", err);
REQUIRE(pi.has_value());
REQUIRE_THAT(pi.value(), WithinRel(3.14159, 1e-6));
}
SECTION("Get bool") {
auto valid = json.getBool("is_valid", err);
REQUIRE(valid.has_value());
REQUIRE(valid.value() == true);
// --- Scénario 2 : Clé Manquante (Retourne la valeur par défaut) ---
SECTION("Clé Manquante - Retourne la valeur par défaut") {
// Log désactivé pour ne pas polluer la sortie de test
double taux = JsonReader::get<double>(data, "taux_inexistant", 1.5, false, false);
REQUIRE(taux == 1.5);
}
}
TEST_CASE("JsonWrapper arrays", "[json]") {
JsonWrapper json;
TEST_CASE("JsonReader::get - Gestion des Erreurs", "[JsonReader][Erreur]") {
json data_err = R"({
"taux_texte": "0.99",
"liste": [1, 2]
})"_json;
json.setArray<int>("scores", {10, 20, 30});
json.setArray<std::string>("tags", {"news", "tech"});
json.setArray<double>("measurements", {1.1, 2.2, 3.3});
json.setArray<bool>("flags", {true, false, true});
// --- Scénario 3 : Erreur de Type (Retourne la valeur par défaut) ---
SECTION("Erreur de Type - Retourne la valeur par défaut (Int vs String)") {
// Tente de lire "0.99" (string) comme un int.
int taux = JsonReader::get<int>(data_err, "taux_texte", -1, false, false);
REQUIRE(taux == -1);
}
std::string err;
// --- Scénario 4 : Clé Manquante (Lance une exception) ---
SECTION("Clé Manquante - Lance une runtime_error") {
REQUIRE_THROWS_AS(
JsonReader::get<std::string>(data_err, "cle_manquante_fatale", "N/A", false, true),
std::runtime_error
);
// Vérifie le message pour s'assurer qu'il mentionne la clé
REQUIRE_THROWS_WITH(
JsonReader::get<std::string>(data_err, "cle_manquante_fatale", "N/A", false, true),
ContainsSubstring("cle_manquante_fatale")
);
}
auto scores = json.getArray<int>("scores", err);
REQUIRE(scores.has_value());
REQUIRE(scores->size() == 3);
REQUIRE(scores->at(1) == 20);
auto tags = json.getArray<std::string>("tags", err);
REQUIRE(tags.has_value());
REQUIRE(tags->at(0) == "news");
auto measurements = json.getArray<double>("measurements", err);
REQUIRE(measurements.has_value());
REQUIRE_THAT(measurements->at(2), WithinRel(3.3, 1e-6));
auto flags = json.getArray<bool>("flags", err);
REQUIRE(flags.has_value());
REQUIRE(flags->at(0) == true);
}
struct Connection {
std::string host;
int port;ccM9XAGZ$mz^b*52T5p&sMA@ujPbCUNW
};
// from_json / to_json must be defined for Connection
inline void to_json(nlohmann::json& j, const Connection& c) {
j = nlohmann::json{{"host", c.host}, {"port", c.port}};
}
inline void from_json(const nlohmann::json& j, Connection& c) {
j.at("host").get_to(c.host);
j.at("port").get_to(c.port);
}
template Connection JsonWrapper::asImpl<Connection>(std::string&) const;
template Connection JsonWrapper::getImpl<Connection>(const std::string&, std::string&) const;
template std::optional<std::vector<Connection>> JsonWrapper::getArrayImpl<Connection>(const std::string&, std::string&) const;
TEST_CASE("JsonWrapper generic serialization/deserialization", "[json][struct]") {
Connection original{"127.0.0.1", 5000};
auto wrapper = JsonWrapper::from(original);
std::string err;
auto restored = wrapper.as<Connection>(err);
REQUIRE(restored.host == "127.0.0.1");
REQUIRE(restored.port == 5000);
}
// --- Scénario 5 : Erreur de Type (Lance une exception) ---
SECTION("Erreur de Type - Lance une runtime_error") {
// Tente de lire une [array] comme un int
REQUIRE_THROWS_AS(
JsonReader::get<int>(data_err, "liste", 0, false, true),
std::runtime_error
);
// Vérifie le message pour s'assurer qu'il mentionne la clé
REQUIRE_THROWS_WITH(
JsonReader::get<int>(data_err, "liste", 0, false, true),
ContainsSubstring("liste") && ContainsSubstring("Erreur de type")
);
}
}