** Passed into module contribution methods to allow the method to contribute configuration.
**
** The service defines the *type* of contribution by declaring a parameterised list or map in its
** ctor or builder method. Contributions must be compatible with the type.
**
** @since 1.7.0
class Configuration {
private ConfigurationImpl config
** By using this wrapped, all the internals are hidden from IDE auto-complete proposals.
internal new make(ConfigurationImpl config) {
this.config = config
}
** A convenience method that instantiates an object, injecting any dependencies. See `Registry.autobuild`.
Obj autobuild(Type type, Obj?[]? ctorArgs := null, [Field:Obj?]? fieldVals := null) {
config.autobuild(type, ctorArgs, fieldVals)
}
** A convenience method that returns the IoC Registry.
Registry registry() {
config.registry
}
** Fantom Bug: http://fantom.org/sidewalk/topic/2163#c13978
@Operator
private Obj? get(Obj key) { null }
** Sets a key / value pair to the service configuration with optional ordering constraints.
**
** The constraints string is a CSV list.
** Each value must start with the prefix 'BEFORE:' or 'AFTER:'.
**
** pre>
** config["Breakfast"] = eggs
** config["Dinner"] = pie
** config.set("Lunch", ham, "AFTER: breakfast, BEFORE: dinner")
** <pre
**
** If the configuration uses non-Str keys, then use 'key.toStr()' in the constraint strings.
**
** If the end service configuration is a List, then the keys are discarded and only the values passed in.
** Typically, 'Str' keys are used in these situations, for ease of use.
**
** Configuration contributions are ordered across modules.
**
** 'key' and 'value' are coerced to the service's contribution type.
@Operator
This set(Obj key, Obj? value, Str? constraints := null) {
config.set(key, value, constraints)
return this
}
** Adds a value to the service configuration with optional ordering constraints.
**
** Because the keys of *added* values are unknown, they cannot be overridden.
** For that reason it is advised to use 'set()' instead.
**
** 'value' is coerced to the service's contribution type.
@Operator
This add(Obj value, Str? constraints := null) {
config.add(value, constraints)
return this
}
** Adds a placeholder. Placeholders are empty configurations used to aid ordering of actual values:
**
** pre>
** config.placeholder("end")
** config.set("wot", ever, ["BEFORE: end"])
** config.set("last", last, ["AFTER: end"])
** <pre
**
** Placeholders do not appear in the the resulting configuration and is never seen by the end service.
This addPlaceholder(Str key, Str? constraints := null) {
config.addPlaceholder(key, constraints)
return this
}
** Overrides and replaces a contributed value.
** The existing key must exist.
**
** 'existingKey' is the id / key of the value to be replaced.
** It may have been initially provided by 'set()' or have be the 'newKey' of a previous override.
**
** 'newKey' does not appear in the the resulting configuration and is never seen by the end service.
** It is only used as reference to this override, so this override itself may be overridden.
** 3rd party libraries, when overriding, should always supply a 'newKey'.
** 'newKey' may be defined as 'Obj' but sane and level headed people will *always* pass in a 'Str'.
**
** 'newValue' is coerced to the service's contribution type.
This overrideValue(Obj existingKey, Obj? newValue, Str? newConstraints := null, Obj? newKey := null) {
config.overrideValue(existingKey, newValue, newConstraints, newKey)
return this
}
** A special kind of override whereby, should this be the last override applied, the value is
** removed from the configuration.
**
** 'existingKey' is the id / key of the value to be replaced.
** It may have been initially provided by 'set()' or have be the 'newKey' of a previous override.
**
** 'newKey' does not appear in the the resulting configuration and is never seen by the end service.
** It is only used as reference to this override, so this override itself may be overridden.
** 3rd party libraries, when overriding, should always supply a 'newKey'.
** 'newKey' may be defined as 'Obj' but sane and level headed people will *always* pass in a 'Str'.
This remove(Obj existingKey, Obj? newKey := null) {
config.remove(existingKey, newKey)
return this
}
@NoDoc
override Str toStr() {
config.toStr
}
}
internal class ConfigurationImpl {
internal const Type contribType
private const ServiceDef serviceDef
private ObjLocator objLocator
private Int impliedCount
private Str? impliedConstraint
private Obj:Contrib config
private Obj:Contrib overrides
private Int overrideCount
private CachingTypeCoercer typeCoercer
internal new make(ObjLocator objLocator, ServiceDef serviceDef, Type contribType) {
if (contribType.name != "Map" && contribType.name != "List")
throw WtfErr("Contributions Type is NOT a Map or a List ???")
if (contribType.isGeneric)
throw IocErr(IocMessages.contributions_configTypeIsGeneric(contribType, serviceDef.serviceId))
this.contribType = contribType
this.serviceDef = serviceDef
this.objLocator = objLocator
this.impliedCount = 1
this.config = Utils.makeMap(Obj#, Contrib#)
this.overrides = Utils.makeMap(Obj#, Contrib#)
this.overrideCount = 1
this.typeCoercer = CachingTypeCoercer()
}
Obj autobuild(Type type, Obj?[]? ctorArgs := null, [Field:Obj?]? fieldVals := null) {
objLocator.trackAutobuild(type, ctorArgs, fieldVals)
}
Registry registry() {
(Registry) objLocator
}
This set(Obj key, Obj? value, Str? constraints := null) {
key = validateKey(key, false)
value = validateVal(value)
if (constraints == null || constraints.isEmpty) {
constraints = impliedConstraint ?: Str.defVal
// keep an implied ordering for anything that doesn't have its own constraints
impliedCount++
impliedConstraint = "after: $key"
}
if (config.containsKey(key))
throw IocErr(IocMessages.contributions_configKeyAlreadyDefined(key.toStr))
config[key] = Contrib(key, value, constraints)
return this
}
This add(Obj value, Str? constraints := null) {
if (keyType != Str#)
throw IocErr(IocMessages.contributions_keyTypeNotKnown(keyType))
key := "afIoc.unordered-" + impliedCount.toStr.padl(2)
return set(key, value, constraints)
}
This addPlaceholder(Str key, Str? constraints := null) {
set(key, Orderer.placeholder, constraints)
}
This overrideValue(Obj existingKey, Obj? newValue, Str? newConstraints := null, Obj? newKey := null) {
if (newKey == null)
newKey = "afIoc.override-" + overrideCount.toStr.padl(2)
overrideCount = overrideCount + 1
newKey = validateKey(newKey, true)
existingKey = validateKey(existingKey, true)
newValue = validateVal(newValue)
if (overrides.containsKey(existingKey))
throw IocErr(IocMessages.contributions_configOverrideKeyAlreadyDefined(existingKey.toStr, overrides[existingKey].key.toStr))
if (config.containsKey(newKey))
throw IocErr(IocMessages.contributions_configOverrideKeyAlreadyExists(newKey.toStr))
if (overrides.vals.map { it.key }.contains(newKey))
throw IocErr(IocMessages.contributions_configOverrideKeyAlreadyExists(newKey.toStr))
overrides[existingKey] = Contrib(newKey, newValue, newConstraints)
return this
}
This remove(Obj existingKey, Obj? newKey := null) {
overrideValue(existingKey, Orderer.delete, null, newKey)
}
// ---- Internal Methods ----------------------------------------------------------------------
** dynamically invoked - just a reset method
internal Void reset() {
// implied ordering only per contrib method
impliedConstraint = null
}
internal Int size() {
config.size
}
internal List toConfigList() {
contribs := orderedContribs
config := (Obj?[]) List.make(valueType, contribs.size)
contribs.each { config.add(it.val) }
return config
}
internal Map toConfigMap() {
mapType := Map#.parameterize(["K":keyType, "V":valueType])
config := (Obj:Obj?) Map.make(mapType) { ordered = true }
orderedContribs.each {
config[it.key] = it.val
}
return config
}
private Contrib[] orderedContribs() {
keys := Utils.makeMap(keyType, keyType)
config.each |val, key| { keys[key] = key }
// don't alter the class state so getConfig() may be called more than once
config := (Obj:Contrib) this.config.dup
InjectionTracker.track("Applying config overrides to '$serviceDef.serviceId'") |->| {
// normalise keys -> map all keys to orig key and apply overrides
norm := (Obj:Contrib) this.overrides.dup
found := true
while (!norm.isEmpty && found) {
found = false
norm = norm.exclude |val, existingKey| {
overrideKey := val.key
if (keys.containsKey(existingKey)) {
keys[overrideKey] = keys[existingKey]
found = true
InjectionTracker.log("'${overrideKey}' overrides '${existingKey}'")
config[keys[existingKey]] = val
// dispose of the override key
val.key = keys[existingKey]
return true
} else {
return false
}
}
}
if (!norm.isEmpty) {
overrideKeys := norm.vals.map { it.key.toStr }.join(", ")
existingKeys := norm.keys.map { it.toStr }.join(", ")
throw IocErr(IocMessages.contributions_overrideDoesNotExist(existingKeys, overrideKeys))
}
}
// we need to convert ALL keys to string for ordering because the "before: XXX, after: XXX" constraints are strings.
ordered := (Contrib[]) InjectionTracker.track("Ordering configuration contributions") |->Contrib[]| {
orderer := Orderer()
config.each |val, key| {
if (val.val === Orderer.delete || val.val === Orderer.placeholder)
orderer.addOrdered(key.toStr, val.val, val.con)
else
orderer.addOrdered(key.toStr, val, val.con)
}
return orderer.toOrderedList
}
return ordered
}
// ---- Helper Methods ------------------------------------------------------------------------
private Obj validateKey(Obj key, Bool isOverrideKey) {
// don't use ReflectUtils.fits() - let TypeCoercer do a proper job.
if (key.typeof.fits(keyType))
return key
if (isOverrideKey)
return key
if (typeCoercer.canCoerce(key.typeof, keyType))
return typeCoercer.coerce(key, keyType)
throw IocErr(IocMessages.contributions_configTypeMismatch("key", key.typeof, keyType))
}
private Obj? validateVal(Obj? val) {
if (val === Orderer.delete || val === Orderer.placeholder)
return val
if (val == null) {
if (!valueType.isNullable)
throw IocErr(IocMessages.contributions_configTypeMismatch("value", null, valueType))
return val
}
// don't use ReflectUtils.fits() - let TypeCoercer do a proper job.
if (val.typeof.fits(valueType))
return val
// empty lists and maps can always be converted
if (!isEmptyList(val) && !isEmptyMap(val))
if (!typeCoercer.canCoerce(val.typeof, valueType))
throw IocErr(IocMessages.contributions_configTypeMismatch("value", val.typeof, valueType))
return typeCoercer.coerce(val, valueType)
}
private Bool isEmptyList(Obj val) {
(val is List) && (((List) val).isEmpty)
}
private Bool isEmptyMap(Obj val) {
(val is Map) && (((Map) val).isEmpty)
}
private once Type keyType() {
contribType.name == "Map" ? contribType.params["K"] : Str#
}
private once Type valueType() {
contribType.params["V"]
}
@NoDoc
override Str toStr() {
"${contribType.name} Configuration of '${contribType.signature}' for '${serviceDef.serviceId}'".replace("sys::", "")
}
}
internal class Contrib {
Obj key; Obj? val; Str? con
new make(Obj key, Obj? val, Str? con) {
this.key = key
this.val = val
this.con = con
}
override Str toStr() {
"[$key:$val]"
}
static Void main() {
l:=(Str?[]) List.make(Str?#, 3)
l.add(null)
}
}