///|
/// Synchronous callback adapter, not a flag decision engine. Lifecycle events
/// are explicit: an initializer must emit_ready itself, even when it returns Ok.
/// Fields are package-visible but hidden from consumers of the package.
pub struct Provider {
priv name : String
priv domain_scoped : Bool
priv mut first_domain : String?
priv resolver : (String, FlagType, FlagValue, EvaluationContext) -> Resolution
priv mut status : ProviderStatus
priv mut initialized : Bool
priv mut stopped : Bool
priv hooks : Array[Hook]
priv bus : EventBus
priv initialize_callback : ((Provider, EvaluationContext) -> Result[
Unit,
SdkError,
])?
priv shutdown_callback : ((Provider) -> Result[Unit, SdkError])?
priv track_callback : ((TrackingEvent, EvaluationContext) -> Result[
Unit,
SdkError,
])?
priv context_changed_callback : ((
Provider,
EvaluationContext,
EvaluationContext,
) -> Result[Unit, SdkError])?
}
///|
pub fn Provider::new(
name : String,
resolver : (String, FlagType, FlagValue, EvaluationContext) -> Resolution,
domain_scoped~ : Bool = false,
initialize~ : ((Provider, EvaluationContext) -> Result[Unit, SdkError])? = None,
shutdown~ : ((Provider) -> Result[Unit, SdkError])? = None,
track~ : ((TrackingEvent, EvaluationContext) -> Result[Unit, SdkError])? = None,
context_changed~ : ((Provider, EvaluationContext, EvaluationContext) -> Result[
Unit,
SdkError,
])? = None,
) -> Provider {
{
name,
domain_scoped,
first_domain: None,
resolver,
status: NotReady,
initialized: false,
stopped: false,
hooks: [],
bus: EventBus::new(),
initialize_callback: initialize,
shutdown_callback: shutdown,
track_callback: track,
context_changed_callback: context_changed,
}
}
///|
pub fn Provider::name(self : Provider) -> String {
self.name
}
///|
pub fn Provider::status(self : Provider) -> ProviderStatus {
self.status
}
///|
/// Records an initialization attempt, not readiness or successful completion.
pub fn Provider::is_initialized(self : Provider) -> Bool {
self.initialized
}
///|
/// At most one attempt, including failed and reentrant attempts. Set the guard
/// before entering user code. Only an absent callback synthesizes Ready.
/// Returned errors update state without inventing an event. Terminal state set
/// inside a callback cannot be undone by its eventual return value.
pub fn Provider::initialize(
self : Provider,
context : EvaluationContext,
) -> Result[Unit, SdkError] {
if self.stopped {
return Err(SdkError::new(ProviderNotReady, "provider is shut down"))
}
if self.initialized {
return Ok(())
}
if self.status == Fatal {
return Err(SdkError::new(ProviderFatal, "provider is fatal"))
}
self.initialized = true
match self.initialize_callback {
None => {
self.emit_ready()
Ok(())
}
Some(callback) => {
let result = callback(self, context.copy())
if result is Err(error) {
self.record_error(error)
}
result
}
}
}
///|
/// Terminal state is committed before the callback. Reentrant or repeated
/// shutdown is a successful no-op, including after a callback returns Err.
/// There is no invented shutdown event in the OpenFeature event vocabulary.
pub fn Provider::shutdown(self : Provider) -> Result[Unit, SdkError] {
if self.stopped {
return Ok(())
}
self.stopped = true
self.status = Shutdown
match self.shutdown_callback {
None => Ok(())
Some(callback) => callback(self)
}
}
///|
/// Default validation belongs to the caller. The resolver sees snapshots;
/// checked validates its output and detaches mutable values and metadata.
pub fn Provider::resolve(
self : Provider,
key : String,
kind : FlagType,
default : FlagValue,
context : EvaluationContext,
) -> Resolution {
match self.status.evaluation_error() {
Some(error) => Resolution::failure(default.copy(), error)
None =>
(self.resolver)(key, kind, default.copy(), context.copy()).checked(
kind, default,
)
}
}
///|
/// Tracking is an independent optional callback, not flag evaluation: it is
/// not gated on readiness and does not synthesize lifecycle state or events.
pub fn Provider::track(
self : Provider,
event : TrackingEvent,
context : EvaluationContext,
) -> Result[Unit, SdkError] {
match event.validate() {
Err(error) => return Err(error)
Ok(_) => ()
}
match self.track_callback {
None => Ok(())
Some(callback) => callback(event.copy(), context.copy())
}
}
///|
/// Context notifications likewise leave state/events to the callback. Both
/// contexts are independently snapshotted, even when old and new are identical.
pub fn Provider::on_context_changed(
self : Provider,
old : EvaluationContext,
new : EvaluationContext,
) -> Result[Unit, SdkError] {
match self.context_changed_callback {
None => Ok(())
Some(callback) => callback(self, old.copy(), new.copy())
}
}
///|
pub fn Provider::add_hook(self : Provider, hook : Hook) -> Unit {
self.hooks.push(hook)
}
///|
/// The registration list is detached; immutable Hook callbacks are shared.
pub fn Provider::hooks(self : Provider) -> Array[Hook] {
self.hooks.copy()
}
///|
/// Return the same bus so consumers may register and remove observers.
pub fn Provider::events(self : Provider) -> EventBus {
self.bus
}
///|
/// Fatal and Shutdown are terminal. Recoverable Error and Stale may become
/// Ready again. Repeated Ready is ignored rather than emitting a duplicate.
pub fn Provider::emit_ready(self : Provider) -> Unit {
if self.stopped || self.status == Fatal || self.status == Ready {
return
}
self.status = Ready
self.bus.emit(ProviderEvent::new(ProviderReady, self.name))
}
///|
/// All errors remain observable until shutdown. A later recoverable error
/// cannot downgrade Fatal; returned initializer errors reuse this state logic.
fn Provider::record_error(self : Provider, error : SdkError) -> Unit {
if self.stopped || self.status == Fatal {
return
}
self.status = if error.code == ProviderFatal { Fatal } else { Error }
}
///|
pub fn Provider::emit_error(self : Provider, error : SdkError) -> Unit {
if self.stopped {
return
}
self.record_error(error)
self.bus.emit(
ProviderEvent::new(
ProviderError,
self.name,
message=error.message,
error=Some(error),
),
)
}
///|
/// Staleness cannot recover Fatal or Shutdown. Repeat notifications in Stale
/// are allowed so a changed diagnostic message can be delivered.
pub fn Provider::emit_stale(self : Provider, message : String) -> Unit {
if self.stopped || self.status == Fatal {
return
}
self.status = Stale
self.bus.emit(ProviderEvent::new(ProviderStale, self.name, message~))
}
///|
/// Configuration notifications do not change readiness. Flags are copied by
/// ProviderEvent and EventBus; shutdown suppresses subsequent notifications.
pub fn Provider::emit_configuration_changed(
self : Provider,
flags : Array[String],
) -> Unit {
if self.stopped {
return
}
self.bus.emit(
ProviderEvent::new(ProviderConfigurationChanged, self.name, flags~),
)
}
///|
/// The first SDK binding domain, available to initialize callbacks. Standalone
/// initialization has no SDK domain; binding records it before callbacks run.
pub fn Provider::binding_domain(self : Provider) -> String? {
self.first_domain
}
///|
pub fn Provider::is_domain_scoped(self : Provider) -> Bool {
self.domain_scoped
}
///|
fn Provider::bind_domain(
self : Provider,
domain : String,
) -> Result[Unit, SdkError] {
match self.first_domain {
Some(first) =>
if self.domain_scoped && first != domain {
Err(
SdkError::new(
General,
"domain-scoped provider cannot serve multiple domains",
),
)
} else {
Ok(())
}
None => {
self.first_domain = Some(domain)
Ok(())
}
}
}