Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,35 @@ 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<T>` for structured error collection
- `parseConfigResponse()` and `parseBanditResponse()` functions returning `ParseResult<T>`:
- 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<T>` template for structured error reporting during parsing

### Changed

- **BREAKING**: `ConfigurationStore::getConfiguration()` now returns `std::shared_ptr<const Configuration>` instead of `Configuration` by value
- 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<T>` 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

Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,17 @@ examples:
.PHONY: run-bandits
run-bandits: examples
@echo "Running bandits example..."
@$(BUILD_DIR)/bandits
@cd examples && ../$(BUILD_DIR)/bandits
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we expect examples to run from ./examples directory? Not opposed to that but that seems inconsistent with tests, that run from repository root

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes more sense than updating the relative paths in the examples, yeah. These are meant to be self-contained examples that the user could copy if they wanted.


.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
Expand Down
75 changes: 48 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nlohmann/json.hpp>
#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": {
Expand All @@ -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);
Expand Down Expand Up @@ -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<MyAssignmentLogger>();
Expand Down Expand Up @@ -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 <nlohmann/json.hpp>
#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 {
Expand Down Expand Up @@ -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<MyAssignmentLogger>();
Expand Down Expand Up @@ -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<MyApplicationLogger>();
eppoclient::EppoClient client(
Expand Down
34 changes: 20 additions & 14 deletions examples/assignment_details.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>(file)),
std::istreambuf_iterator<char>());

// 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<eppoclient::ConfigurationStore>();
configStore->setConfiguration(eppoclient::Configuration(ufc));
configStore->setConfiguration(config);

// Create assignment logger and application logger
auto assignmentLogger = std::make_shared<ConsoleAssignmentLogger>();
Expand Down
74 changes: 28 additions & 46 deletions examples/bandits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>(flagsFile)),
std::istreambuf_iterator<char>());

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<char>(banditsFile)),
std::istreambuf_iterator<char>());

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<eppoclient::ConfigurationStore>();
configStore->setConfiguration(eppoclient::Configuration(banditFlags, banditModels));
configStore->setConfiguration(config);

// Create assignment logger, bandit logger, and application logger
auto assignmentLogger = std::make_shared<ConsoleAssignmentLogger>();
Expand Down
Loading
Loading