** Passed into module contribution methods to allow the method to, err, contribute!
**
** A service can *collect* contributions in three different ways:
** - As an unordered list of values
** - As an ordered list of values
** - As a map of keys and values
**
** 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.
**
** @see `TypeCoercer`
class OrderedConfig {
internal const Type contribType
private const ServiceDef serviceDef
private InjectionCtx ctx
private Orderer orderer
private Int impliedCount
private Str[]? impliedConstraint
private Int overrideCount
private Str:OrderedOverride config
private Str:OrderedOverride overrides
private TypeCoercer typeCoercer
internal new make(InjectionCtx ctx, ServiceDef serviceDef, Type contribType) {
if (contribType.name != "List")
throw WtfErr("Ordered Contrib Type is NOT list???")
if (contribType.isGeneric)
throw IocErr(IocMessages.orderedConfigTypeIsGeneric(contribType, serviceDef.serviceId))
this.ctx = ctx
this.serviceDef = serviceDef
this.contribType = contribType
this.orderer = Orderer()
this.impliedCount = 1
this.overrideCount = 1
this.overrides = Utils.makeMap(Str#, OrderedOverride#)
this.config = Utils.makeMap(Str#, OrderedOverride#)
this.typeCoercer = TypeCoercer()
}
** A helper method that instantiates an object, injecting any dependencies. See `Registry.autobuild`.
Obj autobuild(Type type, Obj?[] ctorArgs := Obj#.emptyList) {
return ctx.objLocator.trackAutobuild(ctx, type, ctorArgs)
}
** Adds an unordered object to a service's configuration.
** An attempt is made to coerce the object to the contrib type.
@Operator
This add(Obj object) {
id := "Unordered${impliedCount}"
addOrdered(id, object)
return this
}
** Adds all the unordered objects to a service's configuration.
** An attempt is made to coerce the objects to the contrib type.
This addAll(Obj[] objects) {
objects.each |obj| {
add(obj)
}
return this
}
** Adds an ordered object to a service's contribution. Each object has a unique id (case
** insensitive) that is used by the constraints for ordering. Each constraint must start with
** the prefix 'BEFORE:' or 'AFTER:'.
**
** pre>
** config.addOrdered("Breakfast", eggs)
** config.addOrdered("Dinner", pie)
** config.addOrdered("Lunch", ham, ["AFTER: breakfast", "BEFORE: dinner"])
** <pre
**
** Configuration contributions are ordered across modules.
**
** An attempt is made to coerce the object to the contrib type.
This addOrdered(Str id, Obj? value, Str[] constraints := Str#.emptyList) {
value = validateVal(value)
if (constraints.isEmpty) {
constraints = impliedConstraint ?: constraints
// keep an implied ordering for anything that doesn't have its own constraints
impliedCount++
impliedConstraint = ["after: $id"]
}
config[id] = OrderedOverride(id, value, constraints)
// this orderer is throwaway, we just use to fail fast on dup key errs
orderer.addOrdered(id, value, constraints)
return this
}
** Adds a placeholder. Placeholders are empty configurations used to aid ordering.
**
** pre>
** config.addPlaceholder("End")
** config.addOrdered("Wot", ever, ["BEFORE: end"])
** config.addOrdered("Last", last, ["AFTER: end"])
** <pre
**
** Placeholders do not appear in the the resulting ordered list.
**
** @since 1.2.0
This addPlaceholder(Str id, Str[] constraints := Str#.emptyList) {
addOrdered(id, Orderer.placeholder, constraints)
return this
}
** Overrides a contributed ordered object. The original object must exist.
** An attempt is made to coerce the override to the contrib type.
**
** Note: Unordered configurations can not be overridden.
**
** Note: If a 'newId' is supplied then this override itself may be overridden by other
** contributions. 3rd party libraries, when overriding, should always supply a 'newId'.
**
** @since 1.2.0
This addOverride(Str existingId, Obj? newValue, Str[] newConstraints := Str#.emptyList, Str? newId := null) {
newValue = validateVal(newValue)
if (overrides.containsKey(existingId))
throw IocErr(IocMessages.configOverrideKeyAlreadyDefined(existingId.toStr, overrides[existingId].key.toStr))
if (newId == null)
newId = "Override${overrideCount}"
overrideCount = overrideCount + 1
overrides[existingId] = OrderedOverride(newId, newValue, newConstraints)
return this
}
** A special kind of override whereby, should this be the last override applied, the value is
** removed from the configuration.
**
** Note: If a 'newId' is supplied then this override itself may be overridden by other
** contributions. 3rd party libraries, when overriding, should always supply a 'newId'.
**
** @since 1.4.0
This remove(Str existingId, Str? newId := null) {
addOverride(existingId, Orderer.delete, Str#.emptyList, newId)
}
** dynamically invoked
internal Void contribute(InjectionCtx ctx, Contribution contribution) {
// implied ordering only per contrib method
impliedConstraint = null
contribution.contributeOrdered(ctx, this)
}
** dynamically invoked
internal List getConfig() {
ctx.track("Applying config overrides to '$serviceDef.serviceId'") |->List| {
keys := Utils.makeMap(Str#, Str#)
config.each |val, key| { keys[key] = key }
// don't alter the class state so getConfig() may be called more than once
Str:OrderedOverride config := this.config.dup
// normalise keys -> map all keys to orig key and apply overrides
Str:OrderedOverride norm := 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
ctx.log("'${overrideKey}' overrides '${existingKey}'")
config[keys[existingKey]] = val
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.contribOverrideDoesNotExist(existingKeys, overrideKeys))
}
orderer := Orderer()
config.each |val, key| {
orderer.addOrdered(key, val.val, val.con)
}
return ctx.track("Ordering configuration contributions") |->List| {
contribs := orderer.toOrderedList
return List.make(listType, contribs.size).addAll(contribs)
}
}
}
internal Int size() {
config.size
}
private once Type listType() {
contribType.params["V"]
}
private Obj? validateVal(Obj? object) {
if (object == Orderer.delete || object == Orderer.placeholder)
return object
if (object == null) {
if (!listType.isNullable)
throw IocErr(IocMessages.orderedConfigTypeMismatch(null, listType))
return object
}
if (object.typeof.fits(listType))
return object
if (typeCoercer.canCoerce(object.typeof, listType))
return typeCoercer.coerce(object, listType)
throw IocErr(IocMessages.orderedConfigTypeMismatch(object.typeof, listType))
}
override Str toStr() {
"OrderedConfig of $listType"
}
}
internal class OrderedOverride {
Str key; Obj? val; Str[] con
new make(Str key, Obj? val, Str[] con) {
this.key = key
this.val = val
this.con = con
}
override Str toStr() {
"[$key:$val]"
}
}