sourceafIocConfig::ConfigSource.fan

using afIoc::Inject
using afBeanUtils::TypeCoercer
using afBeanUtils::NotFoundErr

** (Service) - Provides the config values. 
@Js
const mixin ConfigSource {
    
    ** Creates a 'ConfigSource' instance with the given 'ConfigProviders'.
    static new make(ConfigProvider[] configProviders) {
        ConfigSourceImpl(configProviders)
    }
    
    ** Return the config value with the given id, optionally coercing it to the given type.
    ** 
    ** Throws 'ArgErr' if the ID could not be found.
    @Operator
    abstract Obj? get(Str id, Type? coerceTo := null, Bool checked := true)
    
    ** Returns a case-insensitive map of all the config values
    abstract Str:Obj? config()

    ** Returns a case-insensitive map suitable for logging. 
    ** Same as 'config()' but without environment variables and duplicated env config.
    abstract Str:Obj? configMuted()

    ** Properties defined with this prefix override those without. 
    ** Example, if 'env' equals 'dev' and the given the config contains:
    ** 
    **   acme.myProperty     = wot 
    **   dev.acme.myProperty = ever
    ** 
    ** Then the config 'acme.myProperty' would have the value 'ever'.
    ** 
    ** 'env' is set via the special config property 'afIocConfig.env'.
    abstract Str? env()
}

@Js
internal const class ConfigSourceImpl : ConfigSource {
    private  const TypeCoercer  typeCoercer := TypeCoercer()
    override const Str:Obj?     config
    override const Str:Obj?     configMuted
    override const Str?         env

    new make(ConfigProvider[] configProviders) {
        config := Str:Obj?[:] { caseInsensitive = true }
        configProviders.each {
            config.setAll(it.config)
        }
        
        this.env    = config["afIocConfig.env"]?.toStr?.trimToNull
        
        // tidy up the config by removing env vars and overrides
        configMuted := config.dup
        envPrefix   := (env ?: "") + "."
        envPrefixes := typeCoercer.coerce(config["afIocConfig.envs"], Str?#)?.toStr?.split(',')?.map { "${it}." } ?: Str#.emptyList
        config.each |val, key| {
            if (env != null && key.startsWith(envPrefix))
                configMuted[key[envPrefix.size..-1]] = val
            if (envPrefixes.any { key.startsWith(it) })
                configMuted.remove(key)
        }
        this.config = configMuted.toImmutable

        // remove environment variables from muted map
        Env.cur.vars.keys.each {
            // we could be removing useful stuff here - maybe have a re-think!? 
            configMuted.remove(it)
        }
        configMuted.remove("afIocConfig.env")
        configMuted.remove("afIocConfig.envs")
        this.configMuted = configMuted
    }
    
    override Obj? get(Str id, Type? coerceTo := null, Bool checked := true) {
        if (!config.containsKey(id))
            return checked ? throw ConfigNotFoundErr(ErrMsgs.configNotFound(id), config.keys) : null 
        value := config[id]
        if (value == null) 
            return null 
        if (coerceTo == null || value.typeof.fits(coerceTo))
            return value
        return typeCoercer.coerce(value, coerceTo)
    }   
}

** Thrown when a config ID has not been mapped.
@Js @NoDoc // we can't subclass ArgErr in JS - see http://fantom.org/forum/topic/2468
const class ConfigNotFoundErr : Err, NotFoundErr {
    override const Str?[]   availableValues
    override const Str      valueMsg    := "Available Config IDs:"

    new make(Str msg, Obj?[] availableValues, Err? cause := null) : super(msg, cause) {
        this.availableValues = availableValues.map { it?.toStr }.sort
    }
    
    override Str toStr() {
        NotFoundErr.super.toStr     
    }
}