From 4e93886c3f6110d744c61e06caa9bdee823b5183 Mon Sep 17 00:00:00 2001 From: Greg Huels Date: Tue, 2 Dec 2025 07:53:24 -0600 Subject: [PATCH 1/3] feat: parseConfiugration helper --- CHANGELOG.md | 17 ++++++++ Makefile | 6 +-- README.md | 70 ++++++++++++++++++------------ examples/assignment_details.cpp | 29 ++++++------- examples/bandits.cpp | 75 ++++++++++----------------------- examples/flag_assignments.cpp | 29 ++++++------- src/bandit_model.cpp | 7 ++- src/configuration.cpp | 75 +++++++++++++++++++++++++++++++++ src/configuration.hpp | 34 +++++++++++++++ 9 files changed, 222 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7cdd8..4e7a79b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConfigurationStore(Configuration config)` - constructor accepting Configuration by value - `setConfiguration(Configuration config)` - setter accepting Configuration by value - Move constructor and move assignment operator for `Configuration` class +- `parseConfiguration()` convenience function for simplified configuration parsing: + - `parseConfiguration(flagConfigJson, error)` - parse flags only with error feedback + - `parseConfiguration(flagConfigJson, banditModelsJson, error)` - parse flags and bandits together + - Provides simple string-based error handling for common use cases + - Built on top of `ParseResult` for structured error collection +- `parseConfigResponse()` and `parseBanditResponse()` functions returning `ParseResult`: + - Support parsing from both JSON strings and input streams + - Collect all parsing errors instead of failing on first error + - Enable partial success handling (return value with warnings) +- New `ParseResult` template for structured error reporting during parsing ### Changed @@ -22,11 +32,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Eliminates expensive configuration copies on every flag evaluation - **BREAKING**: `Configuration` constructors now take parameters by value for better performance - `ConfigurationStore` now uses atomic operations instead of mutex internally for better performance +- **BREAKING**: Parsing functions now report errors instead of silently skipping invalid entries: + - `parseConfigResponse()` and `parseBanditResponse()` return `ParseResult` with error collection + - Use the new `parseConfiguration()` convenience function for simplified error handling + - Errors are aggregated and returned rather than causing silent data loss ### Removed - **BREAKING**: `Configuration::precompute()` removed from public API - It is now called automatically in constructors, so manual invocation is no longer needed +- **BREAKING**: `from_json()` for `ConfigResponse` and `BanditResponse` removed + - Replaced with `parseConfigResponse()` and `parseBanditResponse()` for better error feedback + - Use `parseConfiguration()` convenience function for most use cases ## [1.0.0] - 2025-11-14 diff --git a/Makefile b/Makefile index 7e7c621..76867ec 100644 --- a/Makefile +++ b/Makefile @@ -91,17 +91,17 @@ examples: .PHONY: run-bandits run-bandits: examples @echo "Running bandits example..." - @$(BUILD_DIR)/bandits + @cd examples && ../$(BUILD_DIR)/bandits .PHONY: run-flags run-flags: examples @echo "Running flag_assignments example..." - @$(BUILD_DIR)/flag_assignments + @cd examples && ../$(BUILD_DIR)/flag_assignments .PHONY: run-assignment-details run-assignment-details: examples @echo "Running assignment_details example..." - @$(BUILD_DIR)/assignment_details + @cd examples && ../$(BUILD_DIR)/assignment_details # Clean build artifacts .PHONY: clean diff --git a/README.md b/README.md index 8ab5c95..3068f35 100644 --- a/README.md +++ b/README.md @@ -124,15 +124,8 @@ Other dependencies (nlohmann/json, semver, etc.) are vendored and require no ins The Eppo SDK requires configuration data containing your feature flags. This SDK is designed for offline use, so you'll load configuration directly rather than using SDK keys or polling. ```cpp -#include #include "client.hpp" -// Parse configuration from a JSON string -eppoclient::ConfigResponse parseConfiguration(const std::string& configJson) { - nlohmann::json j = nlohmann::json::parse(configJson); - return j; -} - // Your configuration as a JSON string std::string configJson = R"({ "flags": { @@ -143,10 +136,17 @@ std::string configJson = R"({ } })"; +// Parse configuration from JSON string +std::string error; +eppoclient::Configuration config = eppoclient::parseConfiguration(configJson, error); +if (!error.empty()) { + std::cerr << "Configuration parsing error: " << error << std::endl; + return 1; +} + // Create and initialize the configuration store eppoclient::ConfigurationStore configStore; -eppoclient::ConfigResponse config = parseConfiguration(configJson); -configStore.setConfiguration(eppoclient::Configuration(config)); +configStore.setConfiguration(config); // Create the client (configStore must outlive client) eppoclient::EppoClient client(configStore); @@ -267,8 +267,13 @@ int main() { // Initialize configuration eppoclient::ConfigurationStore configStore; std::string configJson = "..."; // Your JSON config string - eppoclient::ConfigResponse config = parseConfiguration(configJson); - configStore.setConfiguration(eppoclient::Configuration(config)); + std::string error; + eppoclient::Configuration config = eppoclient::parseConfiguration(configJson, error); + if (!error.empty()) { + std::cerr << "Configuration parsing error: " << error << std::endl; + return 1; + } + configStore.setConfiguration(config); // Create loggers auto assignmentLogger = std::make_shared(); @@ -317,25 +322,25 @@ Eppo's contextual bandits allow you to dynamically select the best variant based To use bandits, you need to load both flag configuration and bandit models: ```cpp -#include #include "client.hpp" -// Parse bandit models from a JSON string -eppoclient::BanditResponse parseBanditModels(const std::string& modelsJson) { - nlohmann::json j = nlohmann::json::parse(modelsJson); - return j; -} - // Your configuration and bandit models as JSON strings std::string flagConfigJson = "..."; // Your flag config JSON std::string banditModelsJson = "..."; // Your bandit models JSON -// Initialize with both flags and bandit models -eppoclient::ConfigResponse flagConfig = parseConfiguration(flagConfigJson); -eppoclient::BanditResponse banditModels = parseBanditModels(banditModelsJson); +std::string error; +eppoclient::Configuration config = eppoclient::parseConfiguration( + flagConfigJson, + banditModelsJson, + error +); +if (!error.empty()) { + std::cerr << "Configuration parsing error: " << error << std::endl; + return 1; +} eppoclient::ConfigurationStore configStore; -configStore.setConfiguration(eppoclient::Configuration(flagConfig, banditModels)); +configStore.setConfiguration(config); // Create bandit logger to track bandit actions class MyBanditLogger : public eppoclient::BanditLogger { @@ -435,9 +440,14 @@ int main() { eppoclient::ConfigurationStore configStore; std::string flagConfigJson = "..."; // Your flag config JSON std::string banditModelsJson = "..."; // Your bandit models JSON - eppoclient::ConfigResponse flagConfig = parseConfiguration(flagConfigJson); - eppoclient::BanditResponse banditModels = parseBanditModels(banditModelsJson); - configStore.setConfiguration(eppoclient::Configuration(flagConfig, banditModels)); + std::string error; + eppoclient::Configuration config = eppoclient::parseConfiguration( + flagConfigJson, banditModelsJson, error); + if (!error.empty()) { + std::cerr << "Configuration parsing error: " << error << std::endl; + return 1; + } + configStore.setConfiguration(config); // Create loggers auto assignmentLogger = std::make_shared(); @@ -637,8 +647,14 @@ Always ensure these preconditions are met to avoid assertion failures. int main() { // Initialize client with application logger eppoclient::ConfigurationStore configStore; - eppoclient::ConfigResponse config = parseConfiguration(configJson); - configStore.setConfiguration(eppoclient::Configuration(config)); + std::string configJson = "..."; // Your JSON config string + std::string error; + eppoclient::Configuration config = eppoclient::parseConfiguration(configJson, error); + if (!error.empty()) { + std::cerr << "Configuration parsing error: " << error << std::endl; + return 1; + } + configStore.setConfiguration(config); auto applicationLogger = std::make_shared(); eppoclient::EppoClient client( diff --git a/examples/assignment_details.cpp b/examples/assignment_details.cpp index c332e8c..3c1212e 100644 --- a/examples/assignment_details.cpp +++ b/examples/assignment_details.cpp @@ -69,45 +69,40 @@ class ConsoleApplicationLogger : public eppoclient::ApplicationLogger { }; // Helper function to load flags configuration from JSON file -bool loadFlagsConfiguration(const std::string& filepath, eppoclient::ConfigResponse& response) { +bool loadFlagsConfiguration(const std::string& filepath, eppoclient::Configuration& config) { std::ifstream file(filepath); if (!file.is_open()) { std::cerr << "Failed to open flags configuration file: " << filepath << std::endl; return false; } - auto result = eppoclient::parseConfigResponse(file); + // Read entire file content into a string + std::string configJson((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); - // Report all parsing errors/warnings - if (result.hasErrors()) { - std::cerr << "Warning: encountered " << result.errors.size() - << " error(s) while parsing config:" << std::endl; - for (const auto& error : result.errors) { - std::cerr << " - " << error << std::endl; - } - } + // Parse configuration using parseConfiguration + std::string error; + config = eppoclient::parseConfiguration(configJson, error); - // Use the config if we got a value (even with warnings) - if (!result.hasValue()) { - std::cerr << "Error: Failed to parse config response" << std::endl; + if (!error.empty()) { + std::cerr << "Failed to parse configuration: " << error << std::endl; return false; } - response = *result; return true; } int main() { // Load the flags configuration std::cout << "Loading flags configuration..." << std::endl; - eppoclient::ConfigResponse ufc; - if (!loadFlagsConfiguration("config/flags-v1.json", ufc)) { + eppoclient::Configuration config; + if (!loadFlagsConfiguration("config/flags-v1.json", config)) { return 1; } // Create configuration store and set the configuration auto configStore = std::make_shared(); - configStore->setConfiguration(eppoclient::Configuration(ufc)); + configStore->setConfiguration(config); // Create assignment logger and application logger auto assignmentLogger = std::make_shared(); diff --git a/examples/bandits.cpp b/examples/bandits.cpp index 2fed20f..ebd4c2e 100644 --- a/examples/bandits.cpp +++ b/examples/bandits.cpp @@ -118,79 +118,50 @@ class ConsoleApplicationLogger : public eppoclient::ApplicationLogger { } }; -// Helper function to load flags configuration from JSON file -bool loadFlagsConfiguration(const std::string& filepath, eppoclient::ConfigResponse& response) { - std::ifstream file(filepath); - if (!file.is_open()) { - std::cerr << "Failed to open flags configuration file: " << filepath << std::endl; +// Helper function to load complete configuration (flags + bandits) from JSON files +bool loadConfiguration(const std::string& flagsFilepath, const std::string& banditsFilepath, + eppoclient::Configuration& config) { + // Read flags configuration file + std::ifstream flagsFile(flagsFilepath); + if (!flagsFile.is_open()) { + std::cerr << "Failed to open flags configuration file: " << flagsFilepath << std::endl; return false; } + std::string flagsJson((std::istreambuf_iterator(flagsFile)), + std::istreambuf_iterator()); - auto result = eppoclient::parseConfigResponse(file); - - // Report all parsing errors/warnings - if (result.hasErrors()) { - std::cerr << "Warning: encountered " << result.errors.size() - << " error(s) while parsing config:" << std::endl; - for (const auto& error : result.errors) { - std::cerr << " - " << error << std::endl; - } - } - - // Use the config if we got a value (even with warnings) - if (!result.hasValue()) { - std::cerr << "Error: Failed to parse config response" << std::endl; + // Read bandit models file + std::ifstream banditsFile(banditsFilepath); + if (!banditsFile.is_open()) { + std::cerr << "Failed to open bandit models file: " << banditsFilepath << std::endl; return false; } + std::string banditsJson((std::istreambuf_iterator(banditsFile)), + std::istreambuf_iterator()); - response = *result; - return true; -} + // Parse both configurations at once + std::string error; + config = eppoclient::parseConfiguration(flagsJson, banditsJson, error); -// Helper function to load bandit models from JSON file -bool loadBanditModels(const std::string& filepath, eppoclient::BanditResponse& response) { - std::ifstream file(filepath); - if (!file.is_open()) { - std::cerr << "Failed to open bandit models file: " << filepath << std::endl; + if (!error.empty()) { + std::cerr << "Failed to parse configuration: " << error << std::endl; return false; } - auto result = eppoclient::parseBanditResponse(file); - - // Report all parsing errors/warnings - if (result.hasErrors()) { - std::cerr << "Warning: encountered " << result.errors.size() - << " error(s) while parsing bandit models:" << std::endl; - for (const auto& error : result.errors) { - std::cerr << " - " << error << std::endl; - } - } - - // Use the bandit models if we got a value (even with warnings) - if (!result.hasValue()) { - std::cerr << "Error: Failed to parse bandit response" << std::endl; - return false; - } - - response = *result; return true; } int main() { // Load the bandit flags and models configuration std::cout << "Loading bandit flags and models configuration..." << std::endl; - eppoclient::ConfigResponse banditFlags; - eppoclient::BanditResponse banditModels; - if (!loadFlagsConfiguration("config/bandit-flags-v1.json", banditFlags)) { - return 1; - } - if (!loadBanditModels("config/bandit-models-v1.json", banditModels)) { + eppoclient::Configuration config; + if (!loadConfiguration("config/bandit-flags-v1.json", "config/bandit-models-v1.json", config)) { return 1; } // Create configuration store and set the configuration with both flags and bandits auto configStore = std::make_shared(); - configStore->setConfiguration(eppoclient::Configuration(banditFlags, banditModels)); + configStore->setConfiguration(config); // Create assignment logger, bandit logger, and application logger auto assignmentLogger = std::make_shared(); diff --git a/examples/flag_assignments.cpp b/examples/flag_assignments.cpp index d166c2a..be3150a 100644 --- a/examples/flag_assignments.cpp +++ b/examples/flag_assignments.cpp @@ -67,45 +67,40 @@ class ConsoleApplicationLogger : public eppoclient::ApplicationLogger { }; // Helper function to load flags configuration from JSON file -bool loadFlagsConfiguration(const std::string& filepath, eppoclient::ConfigResponse& response) { +bool loadFlagsConfiguration(const std::string& filepath, eppoclient::Configuration& config) { std::ifstream file(filepath); if (!file.is_open()) { std::cerr << "Failed to open flags configuration file: " << filepath << std::endl; return false; } - auto result = eppoclient::parseConfigResponse(file); + // Read entire file content into a string + std::string configJson((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); - // Report all parsing errors/warnings - if (result.hasErrors()) { - std::cerr << "Warning: encountered " << result.errors.size() - << " error(s) while parsing config:" << std::endl; - for (const auto& error : result.errors) { - std::cerr << " - " << error << std::endl; - } - } + // Parse configuration using parseConfiguration + std::string error; + config = eppoclient::parseConfiguration(configJson, error); - // Use the config if we got a value (even with warnings) - if (!result.hasValue()) { - std::cerr << "Error: Failed to parse config response" << std::endl; + if (!error.empty()) { + std::cerr << "Failed to parse configuration: " << error << std::endl; return false; } - response = *result; return true; } int main() { // Load the flags configuration std::cout << "Loading flags configuration..." << std::endl; - eppoclient::ConfigResponse ufc; - if (!loadFlagsConfiguration("config/flags-v1.json", ufc)) { + eppoclient::Configuration config; + if (!loadFlagsConfiguration("config/flags-v1.json", config)) { return 1; } // Create configuration store and set the configuration auto configStore = std::make_shared(); - configStore->setConfiguration(eppoclient::Configuration(ufc)); + configStore->setConfiguration(config); // Create assignment logger and application logger auto assignmentLogger = std::make_shared(); diff --git a/src/bandit_model.cpp b/src/bandit_model.cpp index 7604aac..663fa90 100644 --- a/src/bandit_model.cpp +++ b/src/bandit_model.cpp @@ -284,10 +284,9 @@ ParseResult parseBanditResponse(const nlohmann::json& j) { } else { std::string error; br.updatedAt = parseISOTimestamp(j["updatedAt"].get_ref(), error); - // TODO: log error - // if (!error.empty()) { - // logger.error("BanditResponse: Invalid updatedAt: " + error); - // } + if (!error.empty()) { + result.errors.push_back("BanditResponse: Invalid updatedAt: " + error); + } } } diff --git a/src/configuration.cpp b/src/configuration.cpp index 967142f..be81ae2 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -1,4 +1,5 @@ #include "configuration.hpp" +#include namespace eppoclient { @@ -60,4 +61,78 @@ const BanditConfiguration* Configuration::getBanditConfiguration(const std::stri return &(it->second); } +Configuration parseConfiguration(const std::string& flagConfigJson, + const std::string& banditModelsJson, std::string& error) { + error.clear(); + + // Parse flag configuration JSON + nlohmann::json flagsJson = nlohmann::json::parse(flagConfigJson, nullptr, false); + if (flagsJson.is_discarded()) { + error = "Failed to parse flag configuration JSON: invalid JSON"; + return Configuration(); + } + + // Parse the flag configuration + auto flagResult = parseConfigResponse(flagsJson); + if (!flagResult.hasValue()) { + error = "Failed to parse flag configuration"; + if (flagResult.hasErrors()) { + error += ":\n"; + for (const auto& err : flagResult.errors) { + error += " - " + err + "\n"; + } + } + return Configuration(); + } + + // Optionally parse bandit models if provided + BanditResponse banditModels; + if (!banditModelsJson.empty()) { + nlohmann::json banditsJson = nlohmann::json::parse(banditModelsJson, nullptr, false); + if (banditsJson.is_discarded()) { + error = "Failed to parse bandit models JSON: invalid JSON"; + return Configuration(); + } + + auto banditResult = parseBanditResponse(banditsJson); + if (!banditResult.hasValue()) { + error = "Failed to parse bandit models"; + if (banditResult.hasErrors()) { + error += ":\n"; + for (const auto& err : banditResult.errors) { + error += " - " + err + "\n"; + } + } + return Configuration(); + } + + // Collect any warnings from bandit parsing + if (banditResult.hasErrors()) { + error = "Bandit models parsed with warnings:\n"; + for (const auto& err : banditResult.errors) { + error += " - " + err + "\n"; + } + } + + banditModels = *banditResult.value; + } + + // Collect any warnings from flag parsing + if (flagResult.hasErrors()) { + if (!error.empty()) { + error += "\n"; + } + error += "Flag configuration parsed with warnings:\n"; + for (const auto& err : flagResult.errors) { + error += " - " + err + "\n"; + } + } + + return Configuration(std::move(*flagResult.value), std::move(banditModels)); +} + +Configuration parseConfiguration(const std::string& flagConfigJson, std::string& error) { + return parseConfiguration(flagConfigJson, "", error); +} + } // namespace eppoclient diff --git a/src/configuration.hpp b/src/configuration.hpp index bbcf4b5..d497af8 100644 --- a/src/configuration.hpp +++ b/src/configuration.hpp @@ -54,6 +54,40 @@ class Configuration { std::map> banditFlagAssociations_; }; +/** + * Parse complete configuration from JSON strings with simplified error handling. + * + * This is a convenience wrapper around parseConfigResponse() and parseBanditResponse() + * that provides a simpler API for common use cases. + * + * @param flagConfigJson JSON string containing flag configuration + * @param banditModelsJson JSON string containing bandit models (optional - pass empty string to + * skip) + * @param error Output parameter that will contain error message if parsing fails + * @return Configuration object. Check error parameter to see if parsing was successful. + * + * Example usage: + * @code + * std::string error; + * Configuration config = parseConfiguration(flagsJson, banditsJson, error); + * if (!error.empty()) { + * std::cerr << "Configuration parsing error: " << error << std::endl; + * return 1; + * } + * @endcode + */ +Configuration parseConfiguration(const std::string& flagConfigJson, + const std::string& banditModelsJson, std::string& error); + +/** + * Parse flag configuration only (without bandit models) with simplified error handling. + * + * @param flagConfigJson JSON string containing flag configuration + * @param error Output parameter that will contain error message if parsing fails + * @return Configuration object. Check error parameter to see if parsing was successful. + */ +Configuration parseConfiguration(const std::string& flagConfigJson, std::string& error); + } // namespace eppoclient #endif // CONFIGURATION_H From a08c2ecc72be2bb0ba1104b57eba2748e9cf6e0e Mon Sep 17 00:00:00 2001 From: Greg Huels Date: Tue, 2 Dec 2025 15:11:33 -0600 Subject: [PATCH 2/3] feat: string overloads for parseConfigResponse and parseBanditResult --- README.md | 65 ++++++++++++++++++--------------- examples/assignment_details.cpp | 19 ++++++++-- examples/bandits.cpp | 19 ++++++++-- examples/flag_assignments.cpp | 19 ++++++++-- src/bandit_model.cpp | 14 +++++++ src/bandit_model.hpp | 7 ++++ src/config_response.cpp | 14 +++++++ src/config_response.hpp | 7 ++++ src/configuration.cpp | 45 +++++++++-------------- src/configuration.hpp | 35 +++++++++--------- 10 files changed, 157 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 3068f35..a06ba03 100644 --- a/README.md +++ b/README.md @@ -137,16 +137,18 @@ std::string configJson = R"({ })"; // Parse configuration from JSON string -std::string error; -eppoclient::Configuration config = eppoclient::parseConfiguration(configJson, error); -if (!error.empty()) { - std::cerr << "Configuration parsing error: " << error << std::endl; +auto result = eppoclient::parseConfiguration(configJson); +if (!result.hasValue()) { + std::cerr << "Configuration parsing failed:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return 1; } // Create and initialize the configuration store eppoclient::ConfigurationStore configStore; -configStore.setConfiguration(config); +configStore.setConfiguration(std::move(*result.value)); // Create the client (configStore must outlive client) eppoclient::EppoClient client(configStore); @@ -267,13 +269,15 @@ int main() { // Initialize configuration eppoclient::ConfigurationStore configStore; std::string configJson = "..."; // Your JSON config string - std::string error; - eppoclient::Configuration config = eppoclient::parseConfiguration(configJson, error); - if (!error.empty()) { - std::cerr << "Configuration parsing error: " << error << std::endl; + auto result = eppoclient::parseConfiguration(configJson); + if (!result.hasValue()) { + std::cerr << "Configuration parsing failed:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return 1; } - configStore.setConfiguration(config); + configStore.setConfiguration(std::move(*result.value)); // Create loggers auto assignmentLogger = std::make_shared(); @@ -328,19 +332,17 @@ To use bandits, you need to load both flag configuration and bandit models: std::string flagConfigJson = "..."; // Your flag config JSON std::string banditModelsJson = "..."; // Your bandit models JSON -std::string error; -eppoclient::Configuration config = eppoclient::parseConfiguration( - flagConfigJson, - banditModelsJson, - error -); -if (!error.empty()) { - std::cerr << "Configuration parsing error: " << error << std::endl; +auto result = eppoclient::parseConfiguration(flagConfigJson, banditModelsJson); +if (!result.hasValue()) { + std::cerr << "Configuration parsing failed:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return 1; } eppoclient::ConfigurationStore configStore; -configStore.setConfiguration(config); +configStore.setConfiguration(std::move(*result.value)); // Create bandit logger to track bandit actions class MyBanditLogger : public eppoclient::BanditLogger { @@ -440,14 +442,15 @@ int main() { eppoclient::ConfigurationStore configStore; std::string flagConfigJson = "..."; // Your flag config JSON std::string banditModelsJson = "..."; // Your bandit models JSON - std::string error; - eppoclient::Configuration config = eppoclient::parseConfiguration( - flagConfigJson, banditModelsJson, error); - if (!error.empty()) { - std::cerr << "Configuration parsing error: " << error << std::endl; + auto result = eppoclient::parseConfiguration(flagConfigJson, banditModelsJson); + if (!result.hasValue()) { + std::cerr << "Configuration parsing failed:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return 1; } - configStore.setConfiguration(config); + configStore.setConfiguration(std::move(*result.value)); // Create loggers auto assignmentLogger = std::make_shared(); @@ -648,13 +651,15 @@ int main() { // Initialize client with application logger eppoclient::ConfigurationStore configStore; std::string configJson = "..."; // Your JSON config string - std::string error; - eppoclient::Configuration config = eppoclient::parseConfiguration(configJson, error); - if (!error.empty()) { - std::cerr << "Configuration parsing error: " << error << std::endl; + auto result = eppoclient::parseConfiguration(configJson); + if (!result.hasValue()) { + std::cerr << "Configuration parsing failed:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return 1; } - configStore.setConfiguration(config); + configStore.setConfiguration(std::move(*result.value)); auto applicationLogger = std::make_shared(); eppoclient::EppoClient client( diff --git a/examples/assignment_details.cpp b/examples/assignment_details.cpp index 3c1212e..1ba316b 100644 --- a/examples/assignment_details.cpp +++ b/examples/assignment_details.cpp @@ -81,14 +81,25 @@ bool loadFlagsConfiguration(const std::string& filepath, eppoclient::Configurati std::istreambuf_iterator()); // Parse configuration using parseConfiguration - std::string error; - config = eppoclient::parseConfiguration(configJson, error); + auto result = eppoclient::parseConfiguration(configJson); - if (!error.empty()) { - std::cerr << "Failed to parse configuration: " << error << std::endl; + if (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return false; } + // Report any warnings + if (result.hasErrors()) { + std::cerr << "Configuration parsing had errors:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } + } + + config = std::move(*result.value); return true; } diff --git a/examples/bandits.cpp b/examples/bandits.cpp index ebd4c2e..2b8e3b4 100644 --- a/examples/bandits.cpp +++ b/examples/bandits.cpp @@ -140,14 +140,25 @@ bool loadConfiguration(const std::string& flagsFilepath, const std::string& band std::istreambuf_iterator()); // Parse both configurations at once - std::string error; - config = eppoclient::parseConfiguration(flagsJson, banditsJson, error); + auto result = eppoclient::parseConfiguration(flagsJson, banditsJson); - if (!error.empty()) { - std::cerr << "Failed to parse configuration: " << error << std::endl; + if (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return false; } + // Report any warnings + if (result.hasErrors()) { + std::cerr << "Configuration parsing had errors:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } + } + + config = std::move(*result.value); return true; } diff --git a/examples/flag_assignments.cpp b/examples/flag_assignments.cpp index be3150a..133a6a6 100644 --- a/examples/flag_assignments.cpp +++ b/examples/flag_assignments.cpp @@ -79,14 +79,25 @@ bool loadFlagsConfiguration(const std::string& filepath, eppoclient::Configurati std::istreambuf_iterator()); // Parse configuration using parseConfiguration - std::string error; - config = eppoclient::parseConfiguration(configJson, error); + auto result = eppoclient::parseConfiguration(configJson); - if (!error.empty()) { - std::cerr << "Failed to parse configuration: " << error << std::endl; + if (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return false; } + // Report any warnings + if (result.hasErrors()) { + std::cerr << "Configuration parsing had errors:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } + } + + config = std::move(*result.value); return true; } diff --git a/src/bandit_model.cpp b/src/bandit_model.cpp index 663fa90..54e1afd 100644 --- a/src/bandit_model.cpp +++ b/src/bandit_model.cpp @@ -308,6 +308,20 @@ ParseResult parseBanditResponse(std::istream& is) { return parseBanditResponse(j); } +ParseResult parseBanditResponse(const std::string& jsonString) { + ParseResult result; + + // Parse JSON from string (with exceptions disabled) + auto j = nlohmann::json::parse(jsonString, nullptr, false); + if (j.is_discarded()) { + result.errors.push_back("JSON parse error: invalid JSON"); + return result; + } + + // Delegate to the JSON-based parser + return parseBanditResponse(j); +} + // BanditVariation serialization void to_json(nlohmann::json& j, const BanditVariation& bv) { j = nlohmann::json{{"key", bv.key}, diff --git a/src/bandit_model.hpp b/src/bandit_model.hpp index 3fdc63a..838b885 100644 --- a/src/bandit_model.hpp +++ b/src/bandit_model.hpp @@ -116,6 +116,13 @@ ParseResult parseBanditResponse(const nlohmann::json& j); */ ParseResult parseBanditResponse(std::istream& is); +/** + * Parse BanditResponse from a JSON string with error collection. + * @param jsonString The JSON string to parse + * @return ParseResult containing the parsed BanditResponse and any errors encountered + */ +ParseResult parseBanditResponse(const std::string& jsonString); + /** * Associates a bandit with a specific flag variation. * Used to link feature flag variations to bandit experiments. diff --git a/src/config_response.cpp b/src/config_response.cpp index 0fd4fd9..c9410f9 100644 --- a/src/config_response.cpp +++ b/src/config_response.cpp @@ -702,4 +702,18 @@ ParseResult parseConfigResponse(std::istream& is) { return parseConfigResponse(j); } +ParseResult parseConfigResponse(const std::string& jsonString) { + ParseResult result; + + // Parse JSON from string (with exceptions disabled) + auto j = nlohmann::json::parse(jsonString, nullptr, false); + if (j.is_discarded()) { + result.errors.push_back("JSON parse error: invalid JSON"); + return result; + } + + // Delegate to the JSON-based parser + return parseConfigResponse(j); +} + } // namespace eppoclient diff --git a/src/config_response.hpp b/src/config_response.hpp index 731b0ef..2dd24b3 100644 --- a/src/config_response.hpp +++ b/src/config_response.hpp @@ -172,6 +172,13 @@ ParseResult parseConfigResponse(const nlohmann::json& j); */ ParseResult parseConfigResponse(std::istream& is); +/** + * Parse ConfigResponse from a JSON string with error collection. + * @param jsonString The JSON string to parse + * @return ParseResult containing the parsed ConfigResponse and any errors encountered + */ +ParseResult parseConfigResponse(const std::string& jsonString); + // Internal namespace for implementation details not covered by semver namespace internal { diff --git a/src/configuration.cpp b/src/configuration.cpp index be81ae2..3048a12 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -61,28 +61,27 @@ const BanditConfiguration* Configuration::getBanditConfiguration(const std::stri return &(it->second); } -Configuration parseConfiguration(const std::string& flagConfigJson, - const std::string& banditModelsJson, std::string& error) { - error.clear(); +ParseResult parseConfiguration(const std::string& flagConfigJson, + const std::string& banditModelsJson) { + ParseResult result; // Parse flag configuration JSON nlohmann::json flagsJson = nlohmann::json::parse(flagConfigJson, nullptr, false); if (flagsJson.is_discarded()) { - error = "Failed to parse flag configuration JSON: invalid JSON"; - return Configuration(); + result.errors.push_back("Failed to parse flag configuration JSON: invalid JSON"); + return result; } // Parse the flag configuration auto flagResult = parseConfigResponse(flagsJson); if (!flagResult.hasValue()) { - error = "Failed to parse flag configuration"; + result.errors.push_back("Failed to parse flag configuration"); if (flagResult.hasErrors()) { - error += ":\n"; for (const auto& err : flagResult.errors) { - error += " - " + err + "\n"; + result.errors.push_back(" " + err); } } - return Configuration(); + return result; } // Optionally parse bandit models if provided @@ -90,27 +89,25 @@ Configuration parseConfiguration(const std::string& flagConfigJson, if (!banditModelsJson.empty()) { nlohmann::json banditsJson = nlohmann::json::parse(banditModelsJson, nullptr, false); if (banditsJson.is_discarded()) { - error = "Failed to parse bandit models JSON: invalid JSON"; - return Configuration(); + result.errors.push_back("Failed to parse bandit models JSON: invalid JSON"); + return result; } auto banditResult = parseBanditResponse(banditsJson); if (!banditResult.hasValue()) { - error = "Failed to parse bandit models"; + result.errors.push_back("Failed to parse bandit models"); if (banditResult.hasErrors()) { - error += ":\n"; for (const auto& err : banditResult.errors) { - error += " - " + err + "\n"; + result.errors.push_back(" " + err); } } - return Configuration(); + return result; } // Collect any warnings from bandit parsing if (banditResult.hasErrors()) { - error = "Bandit models parsed with warnings:\n"; for (const auto& err : banditResult.errors) { - error += " - " + err + "\n"; + result.errors.push_back("Bandit warning: " + err); } } @@ -119,20 +116,14 @@ Configuration parseConfiguration(const std::string& flagConfigJson, // Collect any warnings from flag parsing if (flagResult.hasErrors()) { - if (!error.empty()) { - error += "\n"; - } - error += "Flag configuration parsed with warnings:\n"; for (const auto& err : flagResult.errors) { - error += " - " + err + "\n"; + result.errors.push_back("Flag warning: " + err); } } - return Configuration(std::move(*flagResult.value), std::move(banditModels)); -} - -Configuration parseConfiguration(const std::string& flagConfigJson, std::string& error) { - return parseConfiguration(flagConfigJson, "", error); + // Set the configuration value + result.value = Configuration(std::move(*flagResult.value), std::move(banditModels)); + return result; } } // namespace eppoclient diff --git a/src/configuration.hpp b/src/configuration.hpp index d497af8..b9c4a71 100644 --- a/src/configuration.hpp +++ b/src/configuration.hpp @@ -4,6 +4,7 @@ #include #include "bandit_model.hpp" #include "config_response.hpp" +#include "parse_result.hpp" namespace eppoclient { @@ -55,7 +56,7 @@ class Configuration { }; /** - * Parse complete configuration from JSON strings with simplified error handling. + * Parse complete configuration from JSON strings. * * This is a convenience wrapper around parseConfigResponse() and parseBanditResponse() * that provides a simpler API for common use cases. @@ -63,30 +64,28 @@ class Configuration { * @param flagConfigJson JSON string containing flag configuration * @param banditModelsJson JSON string containing bandit models (optional - pass empty string to * skip) - * @param error Output parameter that will contain error message if parsing fails - * @return Configuration object. Check error parameter to see if parsing was successful. + * @return ParseResult containing Configuration object and any errors encountered during parsing * * Example usage: * @code - * std::string error; - * Configuration config = parseConfiguration(flagsJson, banditsJson, error); - * if (!error.empty()) { - * std::cerr << "Configuration parsing error: " << error << std::endl; + * // Parse flags only + * auto result = parseConfiguration(flagsJson); + * + * // Parse flags and bandits + * auto result = parseConfiguration(flagsJson, banditsJson); + * + * if (!result.hasValue()) { + * std::cerr << "Configuration parsing failed:" << std::endl; + * for (const auto& error : result.errors) { + * std::cerr << " - " << error << std::endl; + * } * return 1; * } + * Configuration config = std::move(*result.value); * @endcode */ -Configuration parseConfiguration(const std::string& flagConfigJson, - const std::string& banditModelsJson, std::string& error); - -/** - * Parse flag configuration only (without bandit models) with simplified error handling. - * - * @param flagConfigJson JSON string containing flag configuration - * @param error Output parameter that will contain error message if parsing fails - * @return Configuration object. Check error parameter to see if parsing was successful. - */ -Configuration parseConfiguration(const std::string& flagConfigJson, std::string& error); +ParseResult parseConfiguration(const std::string& flagConfigJson, + const std::string& banditModelsJson = ""); } // namespace eppoclient From deef37a72c492440d44a617d18364c9ad8634c64 Mon Sep 17 00:00:00 2001 From: Greg Huels Date: Wed, 3 Dec 2025 03:45:42 -0600 Subject: [PATCH 3/3] add single-argument overload for parseConfiguration --- src/configuration.cpp | 4 ++++ src/configuration.hpp | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/configuration.cpp b/src/configuration.cpp index 3048a12..ad5f574 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -126,4 +126,8 @@ ParseResult parseConfiguration(const std::string& flagConfigJson, return result; } +ParseResult parseConfiguration(const std::string& flagConfigJson) { + return parseConfiguration(flagConfigJson, ""); +} + } // namespace eppoclient diff --git a/src/configuration.hpp b/src/configuration.hpp index b9c4a71..26a9bca 100644 --- a/src/configuration.hpp +++ b/src/configuration.hpp @@ -62,8 +62,7 @@ class Configuration { * that provides a simpler API for common use cases. * * @param flagConfigJson JSON string containing flag configuration - * @param banditModelsJson JSON string containing bandit models (optional - pass empty string to - * skip) + * @param banditModelsJson JSON string containing bandit models * @return ParseResult containing Configuration object and any errors encountered during parsing * * Example usage: @@ -85,7 +84,18 @@ class Configuration { * @endcode */ ParseResult parseConfiguration(const std::string& flagConfigJson, - const std::string& banditModelsJson = ""); + const std::string& banditModelsJson); + +/** + * Parse configuration from flag configuration JSON only. + * + * This is a convenience overload for when you only have flag configuration + * and no bandit models. + * + * @param flagConfigJson JSON string containing flag configuration + * @return ParseResult containing Configuration object and any errors encountered during parsing + */ +ParseResult parseConfiguration(const std::string& flagConfigJson); } // namespace eppoclient