diff --git a/CHANGELOG.md b/CHANGELOG.md index 9803506..4c09ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ All notable changes to this project will be documented in this file. follow-up layers. - Added side-effect-free built-in configuration validation and default materialization for file, memory, MDBX, OTLP, and Prometheus plans. +- Added a binding/resolution and atomic caller-visible publication contract for + materialized runtime logger plans; factory side effects are not transactional, + and parser frontends and hot reload remain outside this layer. ### Fixed diff --git a/include/logit_cpp/logit.hpp b/include/logit_cpp/logit.hpp index 643e8a4..313953e 100644 --- a/include/logit_cpp/logit.hpp +++ b/include/logit_cpp/logit.hpp @@ -16,6 +16,7 @@ #include "logit/utils.hpp" #include "logit/formatter.hpp" #include "logit/loggers.hpp" +#include "logit/configuration_runtime.hpp" #include "logit/Logger.hpp" #include "logit/log_macros.hpp" diff --git a/include/logit_cpp/logit/configuration.hpp b/include/logit_cpp/logit/configuration.hpp index 0c47005..f0660c8 100644 --- a/include/logit_cpp/logit/configuration.hpp +++ b/include/logit_cpp/logit/configuration.hpp @@ -30,7 +30,9 @@ namespace logit { MissingProgrammaticBinding, UnknownField, BackendValidationFailed, - MaterializationFailed + MaterializationFailed, + BindingResolutionFailed, + InstantiationFailed }; inline const char* configuration_error_code_name(ConfigurationErrorCode code) { @@ -47,6 +49,8 @@ namespace logit { case ConfigurationErrorCode::UnknownField: return "unknown_field"; case ConfigurationErrorCode::BackendValidationFailed: return "backend_validation_failed"; case ConfigurationErrorCode::MaterializationFailed: return "materialization_failed"; + case ConfigurationErrorCode::BindingResolutionFailed: return "binding_resolution_failed"; + case ConfigurationErrorCode::InstantiationFailed: return "instantiation_failed"; } return "unknown_configuration_error"; } diff --git a/include/logit_cpp/logit/configuration_runtime.hpp b/include/logit_cpp/logit/configuration_runtime.hpp new file mode 100644 index 0000000..ea05cbc --- /dev/null +++ b/include/logit_cpp/logit/configuration_runtime.hpp @@ -0,0 +1,179 @@ +#pragma once +#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_RUNTIME_HPP_INCLUDED +#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_RUNTIME_HPP_INCLUDED + +/// \file configuration_runtime.hpp +/// \brief Binding and runtime-instantiation contract for materialized plans. + +#include "configuration.hpp" +#include "loggers.hpp" + +#include +#include +#include +#include +#include + +namespace logit { + + /// \class ConfigurationBindingSet + /// \brief Type-erased programmatic resources resolved for one logger. + /// + /// Bindings deliberately remain outside the parser-neutral document. A + /// resolver and its factory agree on the concrete type associated with each + /// name; the configuration layer only owns the lifetime of the shared value. + class ConfigurationBindingSet { + public: + bool empty() const { return m_values.empty(); } + + bool contains(const std::string& name) const { + std::map::const_iterator it = m_values.find(name); + return it != m_values.end() && static_cast(it->second.value); + } + + template + void set(const std::string& name, const std::shared_ptr& value) { + m_values[name] = Entry(value, std::type_index(typeid(T))); + } + + template + std::shared_ptr get(const std::string& name) const { + std::map::const_iterator it = m_values.find(name); + if (it == m_values.end() || !it->second.value || + it->second.type != std::type_index(typeid(T))) { + return std::shared_ptr(); + } + return std::static_pointer_cast(it->second.value); + } + + private: + struct Entry { + Entry() : value(), type(typeid(void)) {} + Entry(const std::shared_ptr& value, const std::type_index& type) + : value(value), type(type) {} + + std::shared_ptr value; + std::type_index type; + }; + + std::map m_values; + }; + + /// \class ConfigurationBindingResolver + /// \brief Resolves required programmatic bindings without constructing a logger. + class ConfigurationBindingResolver { + public: + virtual ~ConfigurationBindingResolver() {} + + /// \brief Resolve bindings for one materialized logger. + /// + /// Diagnostic paths are relative to the logger root. The document layer + /// prefixes them with `loggers[N]`. + virtual bool resolve(const ConfigurationMaterializedLogger& logger, + ConfigurationBindingSet& bindings, + ConfigurationDiagnostics& diagnostics) const = 0; + }; + + /// \class ConfigurationLoggerFactory + /// \brief Creates one runtime logger after bindings have been resolved. + /// Factory side effects are owned by the caller and are not transactional; + /// only publication of the caller-visible logger vector is atomic. + /// A factory owns the concrete binding types it expects and must retrieve + /// required values through `ConfigurationBindingSet::get()`. If a + /// required value has the wrong type, the factory must fail closed by + /// adding a diagnostic or returning an empty logger pointer. + class ConfigurationLoggerFactory { + public: + virtual ~ConfigurationLoggerFactory() {} + + /// \brief Instantiate one logger from a complete plan entry. + /// + /// Returning an empty pointer is a failed instantiation. Diagnostic + /// paths are relative to the logger root and are rebased by the document + /// layer. + virtual std::shared_ptr instantiate( + const ConfigurationMaterializedLogger& logger, + const ConfigurationBindingSet& bindings, + ConfigurationDiagnostics& diagnostics) const = 0; + }; + + /// \brief Resolve bindings and publish a complete logger set atomically. + /// + /// The caller-visible logger vector is updated only after every binding has + /// resolved and every factory call has returned a logger. Factories may + /// still perform external side effects while constructing instances; those + /// effects are not transactional and are not rolled back if a later factory + /// fails. + inline bool instantiate_configuration( + const ConfigurationPlan& plan, + const ConfigurationBindingResolver& resolver, + const ConfigurationLoggerFactory& factory, + std::vector>& loggers, + ConfigurationDiagnostics& diagnostics) { + diagnostics = ConfigurationDiagnostics(); + std::vector resolved_bindings; + resolved_bindings.reserve(plan.loggers.size()); + bool bindings_ok = true; + for (std::size_t index = 0; index < plan.loggers.size(); ++index) { + const ConfigurationMaterializedLogger& entry = plan.loggers[index]; + ConfigurationBindingSet bindings; + ConfigurationDiagnostics local_diagnostics; + const bool resolved = resolver.resolve(entry, bindings, local_diagnostics); + + bool missing_binding = false; + for (std::vector::const_iterator required = + entry.required_bindings.begin(); + required != entry.required_bindings.end(); ++required) { + if (!bindings.contains(*required)) { + local_diagnostics.add(ConfigurationErrorCode::MissingProgrammaticBinding, + "binding." + *required, + "Required programmatic binding was not resolved"); + missing_binding = true; + } + } + + const std::string prefix = "loggers[" + std::to_string(index) + "]"; + if (!resolved || !local_diagnostics.empty() || missing_binding) { + bindings_ok = false; + if (local_diagnostics.empty()) { + diagnostics.add(ConfigurationErrorCode::BindingResolutionFailed, + prefix + ".binding", + "Programmatic bindings could not be resolved"); + } else { + diagnostics.append_rebased(local_diagnostics, prefix); + } + } + resolved_bindings.push_back(bindings); + } + + if (!bindings_ok) return false; + + std::vector> candidate; + candidate.reserve(plan.loggers.size()); + for (std::size_t index = 0; index < plan.loggers.size(); ++index) { + const ConfigurationMaterializedLogger& entry = plan.loggers[index]; + const std::string prefix = "loggers[" + std::to_string(index) + "]"; + + ConfigurationDiagnostics factory_diagnostics; + std::shared_ptr runtime_logger = + factory.instantiate(entry, resolved_bindings[index], factory_diagnostics); + if (!runtime_logger || !factory_diagnostics.empty()) { + if (factory_diagnostics.empty()) { + diagnostics.add(ConfigurationErrorCode::InstantiationFailed, + prefix, + "Logger factory could not instantiate the plan entry"); + } else { + diagnostics.append_rebased(factory_diagnostics, prefix); + } + return false; + } + candidate.push_back(runtime_logger); + } + + loggers = candidate; + return true; + } + +} // namespace logit + +#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_CONFIGURATION_RUNTIME_HPP_INCLUDED diff --git a/tests/configuration_contract_test.cpp b/tests/configuration_contract_test.cpp index aa0a708..413199d 100644 --- a/tests/configuration_contract_test.cpp +++ b/tests/configuration_contract_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -53,6 +54,51 @@ struct RecordingMaterializer : logit::ConfigurationMaterializer { } }; +struct RecordingBindingResolver : logit::ConfigurationBindingResolver { + bool provide_binding = true; + bool provide_null_binding = false; + bool provide_wrong_type = false; + int missing_at = -1; + mutable int calls = 0; + + bool resolve(const logit::ConfigurationMaterializedLogger& logger, + logit::ConfigurationBindingSet& bindings, + logit::ConfigurationDiagnostics&) const override { + ++calls; + if (provide_binding && !logger.required_bindings.empty() && + (missing_at < 0 || calls != missing_at)) { + if (provide_null_binding) { + bindings.set("on_payload", std::shared_ptr()); + } else if (provide_wrong_type) { + bindings.set("on_payload", std::shared_ptr(new std::string("wrong"))); + } else { + bindings.set("on_payload", std::shared_ptr(new int(1))); + } + } + return true; + } +}; + +struct RecordingLoggerFactory : logit::ConfigurationLoggerFactory { + mutable int calls = 0; + int fail_at = -1; + + std::shared_ptr instantiate( + const logit::ConfigurationMaterializedLogger&, + const logit::ConfigurationBindingSet& bindings, + logit::ConfigurationDiagnostics& diagnostics) const override { + ++calls; + if (!bindings.get("on_payload")) { + diagnostics.add(logit::ConfigurationErrorCode::InstantiationFailed, + "binding.on_payload", + "Test factory rejected a binding with the wrong type"); + return std::shared_ptr(); + } + if (fail_at == calls) return std::shared_ptr(); + return std::shared_ptr(new logit::MemoryLogger()); + } +}; + logit::ConfigurationDocument valid_document() { logit::ConfigurationDocument document; document.schema_version = 1; @@ -356,6 +402,80 @@ void test_optional_format_defaults_merge_recursively() { #endif } +void test_runtime_binding_and_instantiation_are_atomic() { + logit::ConfigurationPlan plan; + plan.schema_version = 1; + logit::ConfigurationMaterializedLogger first; + first.id = "callback-entry"; + first.required_bindings.push_back("on_payload"); + plan.loggers.push_back(first); + + RecordingBindingResolver resolver; + RecordingLoggerFactory factory; + std::vector> loggers; + std::shared_ptr sentinel(new logit::MemoryLogger()); + loggers.push_back(sentinel); + logit::ConfigurationDiagnostics diagnostics; + assert(logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics)); + assert(diagnostics.empty()); + assert(loggers.size() == 1); + assert(loggers[0] != sentinel); + + resolver.provide_binding = false; + loggers.clear(); + loggers.push_back(sentinel); + assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics)); + assert(diagnostics.items()[0].path == "loggers[0].binding.on_payload"); + assert(loggers.size() == 1); + assert(loggers[0] == sentinel); + + resolver.provide_binding = true; + resolver.provide_null_binding = true; + factory.calls = 0; + assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics)); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::MissingProgrammaticBinding)); + assert(factory.calls == 0); + + resolver.provide_null_binding = false; + resolver.provide_wrong_type = true; + logit::ConfigurationBindingSet bindings; + bindings.set("value", std::shared_ptr(new int(1))); + assert(bindings.get("value")); + assert(!bindings.get("value")); + resolver.provide_wrong_type = false; + + resolver.provide_binding = true; + resolver.provide_wrong_type = true; + factory.calls = 0; + loggers.clear(); + loggers.push_back(sentinel); + assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics)); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::InstantiationFailed)); + assert(factory.calls == 1); + assert(loggers.size() == 1); + assert(loggers[0] == sentinel); + resolver.provide_wrong_type = false; + + resolver.provide_binding = true; + factory.calls = 0; + logit::ConfigurationMaterializedLogger second = first; + second.id = "second"; + plan.loggers.push_back(second); + resolver.calls = 0; + resolver.missing_at = 2; + assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics)); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::MissingProgrammaticBinding)); + assert(factory.calls == 0); + assert(loggers.size() == 1); + assert(loggers[0] == sentinel); + + resolver.provide_binding = true; + resolver.missing_at = -1; + factory.fail_at = 2; + assert(!logit::instantiate_configuration(plan, resolver, factory, loggers, diagnostics)); + assert(diagnostics.has_code(logit::ConfigurationErrorCode::InstantiationFailed)); +} + } // namespace int main() { @@ -371,5 +491,6 @@ int main() { test_materializer_diagnostics_are_rebased_to_logger_index(); test_callback_backends_record_required_bindings_when_available(); test_optional_format_defaults_merge_recursively(); + test_runtime_binding_and_instantiation_are_atomic(); return 0; }