Skip to main content

aster_forge_config/
lib.rs

1//! Shared runtime configuration primitives for Aster services.
2//!
3//! This crate owns product-neutral configuration mechanics: typed configuration
4//! definitions, registry construction, storage value conversion, in-process
5//! runtime snapshots, reload diffing, and cross-process reload notifications.
6//! Product crates still own their concrete database entities, repositories,
7//! localized labels, config keys, domain-specific normalizers, and any derived
8//! runtime state that is built from configuration values.
9#![cfg_attr(
10    not(test),
11    deny(
12        clippy::unwrap_used,
13        clippy::unreachable,
14        clippy::expect_used,
15        clippy::panic,
16        clippy::unimplemented,
17        clippy::todo
18    )
19)]
20
21pub mod avatar;
22mod error;
23mod notification;
24mod registry;
25mod runtime;
26mod value;
27
28pub use error::{ConfigCoreError, Result};
29#[cfg(feature = "redis-pubsub")]
30pub use notification::RedisConfigChangeNotifier;
31pub use notification::{
32    CONFIG_SYNC_BACKEND_DISABLED, CONFIG_SYNC_BACKEND_REDIS, ConfigChangeEvent,
33    ConfigChangeNotifier, ConfigNotification, ConfigNotificationSource, ConfigReloadDecision,
34    ConfigReloadMessage, ConfigReloadObservation, ConfigReloadObserver, ConfigReloadWorkerConfig,
35    ConfigSyncConfig, ConfigSyncConnectionObservation, ConfigSyncConnectionObserver,
36    ConfigSyncConnectionState, ConfigSyncEndpoint, ConfigSyncRuntime, InMemoryConfigNotifier,
37    SharedConfigChangeNotifier, build_config_sync_runtime,
38    build_config_sync_runtime_with_runtime_id, decode_config_reload_transport_payload,
39    default_config_sync_topic, handle_config_reload_notification, run_config_reload_supervisor,
40    run_config_reload_supervisor_with_observers, run_config_reload_worker,
41    run_config_reload_worker_with_observer,
42};
43pub use registry::{
44    ConfigDefinition, ConfigDependencyValidator, ConfigNormalizer, ConfigRegistry,
45    ConfigSeedRecord, ConfigValueLookup,
46};
47pub use runtime::{
48    AsyncConfigSnapshot, AsyncConfigStore, AsyncRuntimeConfig, RuntimeConfigChange,
49    RuntimeConfigRecord, StoredConfig, SyncConfigSnapshot, SyncRuntimeConfig,
50    normalize_bool_config_value, normalize_bounded_u8_config_value,
51    normalize_bounded_u64_config_value, normalize_finite_f32_config_value,
52    normalize_non_negative_u64_config_value, normalize_positive_u32_config_value,
53    normalize_positive_u64_config_value, normalize_strict_bool_config_value, parse_bool_like_value,
54    parse_bounded_u8, parse_bounded_u64, parse_finite_f32, parse_non_negative_u64,
55    parse_positive_i32, parse_positive_u32, parse_positive_u64, parse_strict_bool_value, read_bool,
56    read_bounded_u8, read_bounded_u64, read_finite_f32, read_non_negative_u64, read_positive_i32,
57    read_positive_u32, read_positive_u64, read_positive_usize,
58};
59pub use value::{
60    ConfigSource, ConfigValue, ConfigValueType, ConfigVisibility, config_value_audit_string,
61    normalize_string_enum_set_selection, parse_single_string_enum_selection,
62    parse_string_array_config_value, parse_string_enum_set_selection, present_config_value,
63    validate_storage_value,
64};
65
66/// Builds a static [`ConfigRegistry`] from a list of [`ConfigDefinition`] items.
67///
68/// Product crates normally wrap this macro in their own module that names
69/// product-specific keys and default functions. Keeping registration declarative
70/// makes it easier for services to hand the same registry to default
71/// initialization, validation, `OpenAPI` presentation, and admin UI metadata.
72/// Duplicate and empty keys in the static list are rejected at compile time.
73///
74/// ```
75/// use aster_forge_config::ConfigDefinition;
76///
77/// const FIRST: ConfigDefinition = ConfigDefinition {
78///     key: "first",
79///     ..ConfigDefinition::private_system()
80/// };
81/// const SECOND: ConfigDefinition = ConfigDefinition { key: "second", ..FIRST };
82/// aster_forge_config::define_config_registry! {
83///     static REGISTRY = [FIRST, SECOND];
84/// }
85/// aster_forge_config::define_config_registry! {
86///     static EMPTY_REGISTRY = [];
87/// }
88/// assert!(REGISTRY.contains_key("second"));
89/// assert!(EMPTY_REGISTRY.validate_unique_keys().is_ok());
90/// ```
91///
92/// ```compile_fail,E0080
93/// use aster_forge_config::ConfigDefinition;
94///
95/// const DEFINITION: ConfigDefinition = ConfigDefinition {
96///     key: "duplicate",
97///     ..ConfigDefinition::private_system()
98/// };
99/// aster_forge_config::define_config_registry! {
100///     static REGISTRY = [DEFINITION, DEFINITION];
101/// }
102/// ```
103///
104/// ```compile_fail,E0080
105/// use aster_forge_config::ConfigDefinition;
106///
107/// const DEFINITION: ConfigDefinition = ConfigDefinition {
108///     key: "",
109///     ..ConfigDefinition::private_system()
110/// };
111/// aster_forge_config::define_config_registry! {
112///     static REGISTRY = [DEFINITION];
113/// }
114/// ```
115#[macro_export]
116macro_rules! define_config_registry {
117    ($vis:vis static $name:ident = [$($definition:expr),* $(,)?];) => {
118        const _: () = $crate::__assert_unique_config_definition_keys(&[
119            $($definition),*
120        ]);
121        $vis static $name: $crate::ConfigRegistry = $crate::ConfigRegistry::new(&[
122            $($definition),*
123        ]);
124    };
125}
126
127/// Compile-time validation used by [`define_config_registry!`].
128///
129/// This is public only because exported macros expand in the caller's crate.
130/// Applications should use [`ConfigRegistry::validate_unique_keys`] when they
131/// construct a registry dynamically.
132#[doc(hidden)]
133pub const fn __assert_unique_config_definition_keys(definitions: &[ConfigDefinition]) {
134    let mut index = 0;
135    while index < definitions.len() {
136        let key = definitions[index].key;
137        assert!(!key.is_empty(), "config definition key cannot be empty");
138
139        let mut other = index + 1;
140        while other < definitions.len() {
141            assert!(
142                !const_str_eq(key, definitions[other].key),
143                "duplicate config definition key"
144            );
145            other += 1;
146        }
147        index += 1;
148    }
149}
150
151const fn const_str_eq(left: &str, right: &str) -> bool {
152    let left_bytes = left.as_bytes();
153    let right_bytes = right.as_bytes();
154    if left_bytes.len() != right_bytes.len() {
155        return false;
156    }
157
158    let mut index = 0;
159    while index < left_bytes.len() {
160        if left_bytes[index] != right_bytes[index] {
161            return false;
162        }
163        index += 1;
164    }
165    true
166}