cppfig 0.1.0
Modern C++20 compile-time type-safe configuration library
Loading...
Searching...
No Matches
configuration.h
Go to the documentation of this file.
1#pragma once
2
3#include <cstdlib>
4#include <filesystem>
5#include <string>
6#include <string_view>
7
8#include "cppfig/conf.h"
9#include "cppfig/diff.h"
10#include "cppfig/interface.h"
11#include "cppfig/logging.h"
12#include "cppfig/serializer.h"
13#include "cppfig/setting.h"
15#include "cppfig/traits.h"
16#include "cppfig/value.h"
17
18namespace cppfig {
19
87template <typename Schema, Serializer SerializerT = ConfSerializer, typename ThreadPolicy = SingleThreadedPolicy>
88class Configuration : public IConfigurationProvider<Configuration<Schema, SerializerT, ThreadPolicy>, Schema> {
89public:
90 using serializer_type = SerializerT;
92
96 explicit Configuration(std::string file_path)
97 : file_path_(std::move(file_path))
98 , file_values_(Value::Object())
99 {
100 BuildDefaults();
101 }
102
111 template <IsSetting S>
112 requires(Schema::template has_setting<S>)
113 [[nodiscard]] auto GetImpl() const -> typename S::value_type
114 {
115 using value_type = typename S::value_type;
116
117 // 1. Check environment variable (no lock needed — no mutable state accessed)
118 constexpr auto env_override = GetEnvOverride<S>();
119 if constexpr (!env_override.empty()) {
120 if (const char* env_value = std::getenv(std::string(env_override).c_str())) {
121 auto parsed = ConfigTraits<value_type>::FromString(env_value);
122 if (!parsed.has_value()) {
123 Logger::WarnF("Failed to parse environment variable %.*s='%s', using fallback",
124 static_cast<int>(env_override.size()), env_override.data(), env_value);
125 }
126 else {
127 // An override that fails the setting's own validator must
128 // not slip through unchecked; fall back and say so.
129 auto validation = GetSettingValidator<S>()(*parsed);
130 if (validation) {
131 return *parsed;
132 }
133 Logger::WarnF("Environment variable %.*s='%s' is invalid (%s), using fallback",
134 static_cast<int>(env_override.size()), env_override.data(), env_value,
135 validation.error_message.c_str());
136 }
137 }
138 }
139
140 // 2. Check file value (shared lock — concurrent readers allowed)
141 {
142 typename ThreadPolicy::shared_lock lock(mutex_);
143 auto file_result = file_values_.GetAtPath(S::path);
144 if (file_result.ok()) {
145 auto parsed = ConfigTraits<value_type>::Deserialize(*file_result);
146 if (parsed.has_value()) {
147 return *parsed;
148 }
149 Logger::WarnF("Failed to parse file value for '%.*s', using default",
150 static_cast<int>(S::path.size()), S::path.data());
151 }
152 }
153
154 // 3. Return default value (immutable after construction — no lock needed)
155 return S::default_value();
156 }
157
163 template <IsSetting S>
164 requires(Schema::template has_setting<S>)
165 [[nodiscard]] auto IsOverriddenImpl() const -> bool
166 {
167 constexpr auto env_override = GetEnvOverride<S>();
168 if constexpr (env_override.empty()) {
169 return false;
170 }
171 else {
172 return std::getenv(std::string(env_override).c_str()) != nullptr;
173 }
174 }
175
184 template <IsSetting S>
185 requires(Schema::template has_setting<S>)
186 auto SetImpl(typename S::value_type value) -> Status
187 {
188 using value_type = typename S::value_type;
189
190 // Validate the value *before* acquiring the exclusive lock
191 auto validator = GetSettingValidator<S>();
192 auto validation = validator(value);
193 if (!validation) {
194 return InvalidArgumentError(validation.error_message);
195 }
196
197 // Returning OK from a write the next read will ignore is a trap.
198 constexpr auto env_override = GetEnvOverride<S>();
199 if constexpr (!env_override.empty()) {
200 if (IsOverriddenImpl<S>()) {
201 Logger::WarnF("Set('%.*s') stored, but %.*s is overriding it and Get will return the override",
202 static_cast<int>(S::path.size()), S::path.data(),
203 static_cast<int>(env_override.size()), env_override.data());
204 }
205 }
206
207 // Set the value under exclusive lock
208 typename ThreadPolicy::unique_lock lock(mutex_);
209 auto serialized = ConfigTraits<value_type>::Serialize(value);
210 file_values_.SetAtPath(S::path, serialized);
211
212 return OkStatus();
213 }
214
219 [[nodiscard]] auto LoadImpl() -> Status
220 {
221 typename ThreadPolicy::unique_lock lock(mutex_);
222 return LoadUnlocked();
223 }
224
231 [[nodiscard]] auto SaveImpl() const -> Status
232 {
233 typename ThreadPolicy::unique_lock lock(mutex_);
234 return SaveUnlocked();
235 }
236
240 [[nodiscard]] auto DiffImpl() const -> ConfigDiff
241 {
242 typename ThreadPolicy::shared_lock lock(mutex_);
243 return DiffFileFromDefaults(defaults_, file_values_);
244 }
245
249 [[nodiscard]] auto ValidateAllImpl() const -> Status
250 {
251 typename ThreadPolicy::shared_lock lock(mutex_);
252 return ValidateAllUnlocked();
253 }
254
258 [[nodiscard]] auto GetFilePathImpl() const -> std::string_view { return file_path_; }
259
267 [[nodiscard]] auto GetFileValues() const -> Value
268 {
269 typename ThreadPolicy::shared_lock lock(mutex_);
270 return file_values_;
271 }
272
277 [[nodiscard]] auto GetDefaults() const -> Value { return defaults_; }
278
279private:
281 [[nodiscard]] auto LoadUnlocked() -> Status
282 {
283 namespace fs = std::filesystem;
284
285 if (!fs::exists(file_path_)) {
286 // File doesn't exist - create with defaults
287 Logger::InfoF("Configuration file '%s' not found, creating with defaults", file_path_.c_str());
288 file_values_ = defaults_;
289 auto save_status = SaveUnlocked();
290 if (!save_status.ok()) {
291 return save_status;
292 }
293 return ValidateFileValuesUnlocked();
294 }
295
296 // Load existing file
297 auto result = ReadFile<SerializerT>(file_path_);
298 if (!result.ok()) {
299 return result.status();
300 }
301
302 file_values_ = *result;
303 CoerceToSchemaTypes();
304
305 // Check for schema migration (new settings in defaults not in file)
306 auto diff = DiffDefaultsFromFile(defaults_, file_values_);
307 auto added = diff.Added();
308
309 if (!added.empty()) {
310 Logger::Warn("New settings detected in schema, adding to configuration file:");
311 for (const auto& entry : added) {
312 Logger::WarnF(" - %s = %s", entry.path.c_str(), entry.new_value.c_str());
313 // Copy the default value directly from the defaults tree
314 auto default_val = defaults_.GetAtPath(entry.path);
315 if (default_val.ok()) {
316 file_values_.SetAtPath(entry.path, *default_val);
317 }
318 }
319
320 // Save the updated configuration
321 auto save_status = SaveUnlocked();
322 if (!save_status.ok()) {
323 Logger::ErrorF("Failed to save migrated configuration: %s",
324 std::string(save_status.message()).c_str());
325 return save_status;
326 }
327 }
328
329 // A successful Load must leave the configuration holding only values
330 // the schema accepts; otherwise every caller of Get inherits an
331 // invalid value that nothing ever objected to.
332 return ValidateFileValuesUnlocked();
333 }
334
336 [[nodiscard]] auto SaveUnlocked() const -> Status
337 {
338 namespace fs = std::filesystem;
339
340 // Create parent directories if needed
341 fs::path path(file_path_);
342 if (path.has_parent_path()) {
343 std::error_code error_code;
344 fs::create_directories(path.parent_path(), error_code);
345 if (error_code) {
346 return InternalError("Failed to create directory: " + error_code.message());
347 }
348 }
349
350 return WriteFile<SerializerT>(file_path_, file_values_);
351 }
352
357 [[nodiscard]] auto ValidateFileValuesUnlocked() const -> Status
358 {
359 Status status = OkStatus();
360
361 Schema::ForEachSetting([this, &status]<typename S>() {
362 if (!status.ok()) {
363 return; // Stop on first error
364 }
365
366 using value_type = typename S::value_type;
367 auto file_result = file_values_.GetAtPath(S::path);
368 if (!file_result.ok()) {
369 return;
370 }
371
372 auto parsed = ConfigTraits<value_type>::Deserialize(*file_result);
373 if (!parsed.has_value()) {
374 return;
375 }
376
377 auto validation = GetSettingValidator<S>()(*parsed);
378 if (!validation) {
379 status = InvalidArgumentError(std::string(S::path) + ": " + validation.error_message);
380 }
381 });
382
383 return status;
384 }
385
392 [[nodiscard]] auto ValidateAllUnlocked() const -> Status
393 {
394 Status status = ValidateEnvOverridesUnlocked();
395 if (!status.ok()) {
396 return status;
397 }
398 return ValidateFileValuesUnlocked();
399 }
400
402 [[nodiscard]] auto ValidateEnvOverridesUnlocked() const -> Status
403 {
404 Status status = OkStatus();
405
406 Schema::ForEachSetting([&status]<typename S>() {
407 if (!status.ok()) {
408 return; // Stop on first error
409 }
410
411 constexpr auto env_override = GetEnvOverride<S>();
412 if constexpr (!env_override.empty()) {
413 const char* env_value = std::getenv(std::string(env_override).c_str());
414 if (env_value == nullptr) {
415 return;
416 }
417
418 using value_type = typename S::value_type;
419 auto parsed = ConfigTraits<value_type>::FromString(env_value);
420 if (!parsed.has_value()) {
421 status = InvalidArgumentError(std::string(env_override) + ": cannot parse '" + env_value + "' as a value for '" + std::string(S::path) + "'");
422 return;
423 }
424
425 auto validation = GetSettingValidator<S>()(*parsed);
426 if (!validation) {
427 status = InvalidArgumentError(std::string(env_override) + ": " + validation.error_message);
428 }
429 }
430 });
431
432 return status;
433 }
434
447 void CoerceToSchemaTypes()
448 {
449 Schema::ForEachSetting([this]<typename S>() {
450 using value_type = typename S::value_type;
451
452 auto file_result = file_values_.GetAtPath(S::path);
453 if (!file_result.ok()) {
454 return;
455 }
456 if (ConfigTraits<value_type>::Deserialize(*file_result).has_value()) {
457 return;
458 }
459
460 auto text = file_result->TryToText();
461 if (!text.has_value()) {
462 return;
463 }
464
465 auto parsed = ConfigTraits<value_type>::FromString(*text);
466 if (parsed.has_value()) {
467 file_values_.SetAtPath(S::path, ConfigTraits<value_type>::Serialize(*parsed));
468 }
469 });
470 }
471
472 void BuildDefaults()
473 {
474 defaults_ = Value::Object();
475
476 Schema::ForEachSetting([this]<typename S>() {
477 using value_type = typename S::value_type;
478 auto serialized = ConfigTraits<value_type>::Serialize(S::default_value());
479 defaults_.SetAtPath(S::path, serialized);
480 });
481 }
482
483 std::string file_path_;
484 Value file_values_;
485 Value defaults_;
486 mutable typename ThreadPolicy::mutex_type mutex_;
487};
488
489} // namespace cppfig
Result of comparing two configurations.
Definition diff.h:40
Main configuration manager.
Definition configuration.h:88
auto IsOverriddenImpl() const -> bool
Reports whether an environment variable is overriding a setting.
Definition configuration.h:165
auto GetImpl() const -> typename S::value_type
Gets the value for a setting type.
Definition configuration.h:113
auto SetImpl(typename S::value_type value) -> Status
Sets the value for a setting type.
Definition configuration.h:186
Configuration(std::string file_path)
Creates a configuration manager with a file path.
Definition configuration.h:96
auto LoadImpl() -> Status
Loads configuration from the file.
Definition configuration.h:219
SerializerT serializer_type
Definition configuration.h:90
auto GetFilePathImpl() const -> std::string_view
Returns the file path.
Definition configuration.h:258
auto DiffImpl() const -> ConfigDiff
Returns the diff between file values and defaults.
Definition configuration.h:240
auto GetFileValues() const -> Value
Returns a snapshot of the current file values.
Definition configuration.h:267
auto GetDefaults() const -> Value
Returns a snapshot of the default values.
Definition configuration.h:277
auto SaveImpl() const -> Status
Saves the current configuration to the file.
Definition configuration.h:231
auto ValidateAllImpl() const -> Status
Validates all current values against their validators.
Definition configuration.h:249
CRTP base class for configuration providers.
Definition interface.h:20
static void WarnF(const char *format, Args... args)
Logs a formatted warning message to stderr.
Definition logging.h:62
static void ErrorF(const char *format, Args... args)
Logs a formatted error message to stderr.
Definition logging.h:74
static void InfoF(const char *format, Args... args)
Logs a formatted info message to stdout.
Definition logging.h:50
static void Warn(std::string_view message)
Logs a warning message to stderr.
Definition logging.h:21
A lightweight status object carrying an error code and message.
Definition status.h:25
A self-contained, recursive value type for configuration data.
Definition value.h:32
auto GetAtPath(std::string_view path) const -> StatusOr< Value >
Gets a value at a dot-separated path.
Definition value.h:442
void SetAtPath(std::string_view path, const Value &value)
Sets a value at a dot-separated path, creating intermediate objects.
Definition value.h:466
static auto Object() -> Value
Creates an empty object value.
Definition value.h:139
C++20 compile-time type-safe configuration library.
Definition conf.h:17
auto DiffDefaultsFromFile(const Value &defaults, const Value &file_values) -> ConfigDiff
Compares defaults against file configuration.
Definition diff.h:169
auto InternalError(std::string message) -> Status
Returns an Internal error status.
Definition status.h:69
auto DiffFileFromDefaults(const Value &defaults, const Value &file_values) -> ConfigDiff
Compares file configuration against defaults.
Definition diff.h:158
auto OkStatus() -> Status
Returns an OK status.
Definition status.h:54
auto InvalidArgumentError(std::string message) -> Status
Returns an InvalidArgument error status.
Definition status.h:63
static auto Deserialize(const Value &value) -> std::optional< T >=delete
Deserializes a value from a Value node.
static auto FromString(std::string_view str) -> std::optional< T >=delete
Parses a value from a string (e.g., from environment variables).
static auto Serialize(const T &value) -> Value=delete
Serializes a value to a Value node.