Current section
Files
Jump to
Current section
Files
src/openfeature.gleam
import gleam/dict.{type Dict}
import gleam/erlang/process.{type Subject}
import gleam/list
import gleam/otp/actor
import gleam/result
import openfeature/client.{type Client, Client, ClientMetadata}
import openfeature/domain.{type Domain}
import openfeature/evaluation_context.{type EvaluationContext, EvaluationContext}
import openfeature/events.{type EventCallback}
import openfeature/provider.{type FeatureProvider, type Metadata}
import openfeature/providers/no_op
import worm
const timeout = 10_000
type APIMessage {
Shutdown
SetProvider(
reply_with: Subject(Result(Nil, Nil)),
domain: Domain,
provider: FeatureProvider,
)
GetProvider(reply_with: Subject(FeatureProvider), domain: Domain)
SetContext(evaluation_context: EvaluationContext)
AddHandler(domain: Domain, callback: EventCallback)
RemoveHandler(domain: Domain, callback: EventCallback)
}
type API {
API(
provider_registry: Dict(Domain, FeatureProvider),
global_context: EvaluationContext,
event_callbacks: Dict(Domain, List(EventCallback)),
)
}
fn get_api_subject() -> Subject(APIMessage) {
worm.persist(init_api)
}
fn init_api() {
let initial_state = API(dict.new(), evaluation_context.empty(), dict.new())
let assert Ok(subject) =
actor.start(initial_state, fn(message: APIMessage, state: API) {
case message {
Shutdown -> {
shutdown_internal(state)
actor.Stop(process.Normal)
}
SetProvider(reply_with, domain, provider) ->
set_provider_internal(state, reply_with, domain, provider)
|> actor.continue
GetProvider(reply_with, domain) ->
get_provider_internal(state, reply_with, domain)
|> actor.continue
SetContext(evaluation_context) ->
set_context_internal(state, evaluation_context)
|> actor.continue
AddHandler(domain, callback) ->
add_handler_internal(state, domain, callback)
|> actor.continue
RemoveHandler(domain, callback) ->
remove_handler_internal(state, domain, callback)
|> actor.continue
}
})
subject
}
pub fn set_provider(provider: FeatureProvider) -> Result(Nil, Nil) {
process.try_call(
get_api_subject(),
SetProvider(_, domain.Global, provider),
timeout,
)
|> result.nil_error
|> result.flatten
}
fn set_provider_internal(
state: API,
reply_with: Subject(Result(Nil, Nil)),
domain: Domain,
provider: FeatureProvider,
) -> API {
let init_result = provider.initialize(state.global_context)
actor.send(reply_with, init_result)
let provider_registry =
state.provider_registry
|> dict.insert(domain, provider)
API(provider_registry, state.global_context, state.event_callbacks)
}
fn get_provider() -> FeatureProvider {
actor.call(get_api_subject(), GetProvider(_, domain.Global), timeout)
}
fn get_provider_internal(
state: API,
reply_with: Subject(FeatureProvider),
domain: Domain,
) -> API {
state.provider_registry
|> dict.get(domain)
|> result.unwrap(no_op.provider())
|> actor.send(reply_with, _)
state
}
pub fn get_provider_metadata() -> Metadata {
get_provider().get_metadata()
}
pub fn set_domain_provider(domain: String, provider: FeatureProvider) -> Nil {
actor.call(
get_api_subject(),
SetProvider(_, domain.Scoped(domain), provider),
timeout,
)
|> result.unwrap(Nil)
}
fn get_domain_provider(domain: String) -> FeatureProvider {
actor.call(get_api_subject(), GetProvider(_, domain.Scoped(domain)), timeout)
}
pub fn get_domain_provider_metadata(domain: String) -> Metadata {
get_domain_provider(domain).get_metadata()
}
pub fn get_client() {
Client(
provider: get_provider(),
metadata: ClientMetadata(domain.Global),
evaluation_context: evaluation_context.empty(),
)
}
/// Initialise and retrieve a new client for the provided domain.
/// If the domain has a corresponding provider registered, the client will use that provider.
/// Otherwise, the global default provider is used.
pub fn get_domain_client(domain: String) {
Client(
provider: get_domain_provider(domain),
metadata: ClientMetadata(domain.Scoped(domain)),
evaluation_context: evaluation_context.empty(),
)
}
fn set_context_internal(
state: API,
evaluation_context: EvaluationContext,
) -> API {
API(state.provider_registry, evaluation_context, state.event_callbacks)
}
pub fn shutdown() {
actor.send(get_api_subject(), Shutdown)
}
fn shutdown_internal(state: API) -> Nil {
state.provider_registry
|> dict.values
|> list.each(fn(provider) { provider.shutdown() })
}
fn add_handler_internal(
state: API,
domain: Domain,
callback: EventCallback,
) -> API {
let domain_callbacks =
state.event_callbacks
|> dict.get(domain)
|> result.unwrap([])
|> list.prepend(callback)
state.event_callbacks
|> dict.insert(domain, domain_callbacks)
|> API(state.provider_registry, state.global_context, _)
}
fn remove_handler_internal(state: API, domain: Domain, callback: EventCallback) {
let domain_callbacks =
state.event_callbacks
|> dict.get(domain)
|> result.unwrap([])
|> list.drop_while(fn(x) { x == callback })
state.event_callbacks
|> dict.insert(domain, domain_callbacks)
|> API(state.provider_registry, state.global_context, _)
}