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..a06ba03 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,19 @@ std::string configJson = R"({ } })"; +// Parse configuration from JSON string +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; -eppoclient::ConfigResponse config = parseConfiguration(configJson); -configStore.setConfiguration(eppoclient::Configuration(config)); +configStore.setConfiguration(std::move(*result.value)); // Create the client (configStore must outlive client) eppoclient::EppoClient client(configStore); @@ -267,8 +269,15 @@ int main() { // Initialize configuration eppoclient::ConfigurationStore configStore; std::string configJson = "..."; // Your JSON config string - eppoclient::ConfigResponse config = parseConfiguration(configJson); - configStore.setConfiguration(eppoclient::Configuration(config)); + 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(std::move(*result.value)); // Create loggers auto assignmentLogger = std::make_shared(); @@ -317,25 +326,23 @@ 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); +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(eppoclient::Configuration(flagConfig, banditModels)); +configStore.setConfiguration(std::move(*result.value)); // Create bandit logger to track bandit actions class MyBanditLogger : public eppoclient::BanditLogger { @@ -435,9 +442,15 @@ 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)); + 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(std::move(*result.value)); // Create loggers auto assignmentLogger = std::make_shared(); @@ -637,8 +650,16 @@ 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 + 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(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 c332e8c..1ba316b 100644 --- a/examples/assignment_details.cpp +++ b/examples/assignment_details.cpp @@ -69,45 +69,51 @@ 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; + // Parse configuration using parseConfiguration + auto result = eppoclient::parseConfiguration(configJson); + + if (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; for (const auto& error : result.errors) { std::cerr << " - " << error << std::endl; } + return false; } - // Use the config if we got a value (even with warnings) - if (!result.hasValue()) { - std::cerr << "Error: Failed to parse config response" << 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; + } } - response = *result; + config = std::move(*result.value); 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..2b8e3b4 100644 --- a/examples/bandits.cpp +++ b/examples/bandits.cpp @@ -118,79 +118,61 @@ 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 + auto result = eppoclient::parseConfiguration(flagsJson, banditsJson); -// 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 (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; + for (const auto& error : result.errors) { + std::cerr << " - " << error << std::endl; + } return false; } - auto result = eppoclient::parseBanditResponse(file); - - // Report all parsing errors/warnings + // Report any warnings if (result.hasErrors()) { - std::cerr << "Warning: encountered " << result.errors.size() - << " error(s) while parsing bandit models:" << std::endl; + std::cerr << "Configuration parsing had errors:" << 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; + config = std::move(*result.value); 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..133a6a6 100644 --- a/examples/flag_assignments.cpp +++ b/examples/flag_assignments.cpp @@ -67,45 +67,51 @@ 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; + // Parse configuration using parseConfiguration + auto result = eppoclient::parseConfiguration(configJson); + + if (!result.hasValue()) { + std::cerr << "Failed to parse configuration:" << std::endl; for (const auto& error : result.errors) { std::cerr << " - " << error << std::endl; } + return false; } - // Use the config if we got a value (even with warnings) - if (!result.hasValue()) { - std::cerr << "Error: Failed to parse config response" << 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; + } } - response = *result; + config = std::move(*result.value); 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..54e1afd 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); + } } } @@ -309,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 967142f..ad5f574 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -1,4 +1,5 @@ #include "configuration.hpp" +#include namespace eppoclient { @@ -60,4 +61,73 @@ const BanditConfiguration* Configuration::getBanditConfiguration(const std::stri return &(it->second); } +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()) { + 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()) { + result.errors.push_back("Failed to parse flag configuration"); + if (flagResult.hasErrors()) { + for (const auto& err : flagResult.errors) { + result.errors.push_back(" " + err); + } + } + return result; + } + + // Optionally parse bandit models if provided + BanditResponse banditModels; + if (!banditModelsJson.empty()) { + nlohmann::json banditsJson = nlohmann::json::parse(banditModelsJson, nullptr, false); + if (banditsJson.is_discarded()) { + result.errors.push_back("Failed to parse bandit models JSON: invalid JSON"); + return result; + } + + auto banditResult = parseBanditResponse(banditsJson); + if (!banditResult.hasValue()) { + result.errors.push_back("Failed to parse bandit models"); + if (banditResult.hasErrors()) { + for (const auto& err : banditResult.errors) { + result.errors.push_back(" " + err); + } + } + return result; + } + + // Collect any warnings from bandit parsing + if (banditResult.hasErrors()) { + for (const auto& err : banditResult.errors) { + result.errors.push_back("Bandit warning: " + err); + } + } + + banditModels = *banditResult.value; + } + + // Collect any warnings from flag parsing + if (flagResult.hasErrors()) { + for (const auto& err : flagResult.errors) { + result.errors.push_back("Flag warning: " + err); + } + } + + // Set the configuration value + result.value = Configuration(std::move(*flagResult.value), std::move(banditModels)); + return result; +} + +ParseResult parseConfiguration(const std::string& flagConfigJson) { + return parseConfiguration(flagConfigJson, ""); +} + } // namespace eppoclient diff --git a/src/configuration.hpp b/src/configuration.hpp index bbcf4b5..26a9bca 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 { @@ -54,6 +55,48 @@ class Configuration { std::map> banditFlagAssociations_; }; +/** + * Parse complete configuration from JSON strings. + * + * 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 + * @return ParseResult containing Configuration object and any errors encountered during parsing + * + * Example usage: + * @code + * // 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 + */ +ParseResult parseConfiguration(const std::string& flagConfigJson, + 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 #endif // CONFIGURATION_H