** Coerces objects to a given type via 'fromXXX()' / 'toXXX()' ctors and methods.
** This is often useful for converting objects to and from Strs, but can be used for much more.
**
** 'TypeCoercer' inspects type parameters in Lists and Maps and also converts the contents of each.
** Example, coercing 'Int[1, 2, 3]' to 'Str[]' will convert each item of the list into a Str.
** Similarly, when coercing a map to a new map type, all the key and vals will be converted.
**
** The 'caseInsensitive' and 'ordered' attributes of new maps are preserved.
**
** If performance is required, then use [Concurrent]`http://www.fantomfactory.org/pods/afConcurrent`
** to create a 'TypeCoercer' that caches the functions used to convert between one type and another.
** Full code for a 'CachingTypeCoercer' is given below:
**
** pre>
** using afBeanUtils
** using afConcurrent
**
** ** A 'TypeCoercer' that caches its conversion methods.
** const class CachingTypeCoercer : TypeCoercer {
** private const AtomicMap cache := AtomicMap()
**
** ** Cache the conversion functions
** override protected |Obj->Obj?|? createCoercionFunc(Type fromType, Type toType) {
** key := "${fromType.qname}->${toType.qname}"
** return cache.getOrAdd(key) { doCreateCoercionFunc(fromType, toType) }
** }
**
** ** Clears the function cache
** Void clear() {
** cache.clear
** }
** }
** <pre
@Js
const class TypeCoercer {
** Returns 'true' if 'fromType' can be coerced to the given 'toType'.
Bool canCoerce(Type fromType, Type toType) {
if (fromType.name == "List" && toType.name == "List") {
valFunc := createCoercionFunc(fromType.params["V"] ?: Obj?#, toType.params["V"] ?: Obj?#)
return valFunc != null
}
if (fromType.name == "Map" && toType.name == "Map") {
keyFunc := createCoercionFunc(fromType.params["K"] ?: Obj#, toType.params["K"] ?: Obj#)
valFunc := createCoercionFunc(fromType.params["V"] ?: Obj?#, toType.params["V"] ?: Obj?#)
return keyFunc != null && valFunc != null
}
return createCoercionFunc(fromType, toType) != null
}
** Coerces the Obj to the given type.
**
** Coercion methods are looked up in the following order:
** 1. 'toXXX()'
** 2. 'fromXXX()'
** 3. 'makeFromXXX()'
**
** 'null' values are always coerced to 'null'.
Obj? coerce(Obj? value, Type toType) {
if (value == null) // return / dispose of nulls straight away, 'cos we don't know what type they are!
return toType.isNullable ? null : throw ArgErr(ErrMsgs.typeCoercer_notFound(null, toType))
if (value.typeof.name == "List" && toType.name == "List") {
toListType := toType.params["V"] ?: Obj?#
toList := (Obj?[]) toListType.emptyList.rw
((List) value).each {
toList.add(coerce(it, toListType))
}
return toList
}
if (value.typeof.name == "Map" && toType.name == "Map") {
toKeyType := toType.params["K"] ?: Obj#
toValType := toType.params["V"] ?: Obj?#
toMap := ([Obj:Obj?]?) null
if (((Map) value).caseInsensitive && toKeyType.fits(Str#))
toMap = Map.make(toType) { caseInsensitive = true }
if (((Map) value).ordered)
toMap = Map.make(toType) { ordered = true }
if (toMap == null)
toMap = toType.isGeneric ? Map.make(Obj:Obj?#) : Map.make(toType.toNonNullable)
((Map) value).each |v1, k1| {
k2 := coerce(k1, toKeyType)
v2 := coerce(v1, toValType)
toMap[k2] = v2
}
return toMap
}
meth := createCoercionFunc(value.typeof, toType)
if (meth == null)
throw ArgErr(ErrMsgs.typeCoercer_notFound(value.typeof, toType))
try {
return meth(value)
} catch (Err e) {
throw ArgErr(ErrMsgs.typeCoercer_fail(value.typeof, toType, value), e)
}
}
** Override this method should you wish to cache the conversion functions.
**
** @see http://fantom.org/sidewalk/topic/2289
@NoDoc
protected virtual |Obj->Obj?|? createCoercionFunc(Type fromType, Type toType) {
doCreateCoercionFunc(fromType, toType)
}
** It kinda sucks to need this method, but it's a workaround to
** [this issue]`http://fantom.org/sidewalk/topic/2289`.
@NoDoc
protected |Obj->Obj?|? doCreateCoercionFunc(Type fromType, Type toType) {
// check the basics first!
if (fromType.fits(toType))
return |Obj val -> Obj?| { val }
// first look for a 'toXXX()' instance method
toName := "to${toType.name}"
toXxxMeth := ReflectUtils.findMethod(fromType, toName, Obj#.emptyList, false, toType)
if (toXxxMeth != null)
return |Obj val -> Obj?| { toXxxMeth.callOn(val, null) }
// next look for a 'fromXXX()' static / ctor
// see http://fantom.org/sidewalk/topic/2154
fromName := "from${fromType.name}"
fromXxxMeth := ReflectUtils.findMethod(toType, fromName, [fromType], true)
if (fromXxxMeth != null)
return (|Obj val -> Obj?| { fromXxxMeth.call(val) }).toImmutable
fromXxxCtor := ReflectUtils.findCtor(toType, fromName, [fromType])
if (fromXxxCtor != null)
return (|Obj val -> Obj?| { fromXxxCtor.call(val) }).toImmutable
// one last chance - try 'makeFromXXX()' ctors
makefromName := "makeFrom${fromType.name}"
makeFromXxxMeth := ReflectUtils.findMethod(toType, makefromName, [fromType], true)
if (makeFromXxxMeth != null)
return |Obj val -> Obj?| { makeFromXxxMeth.call(val) }
makeFromXxxCtor := ReflectUtils.findCtor(toType, makefromName, [fromType])
if (makeFromXxxCtor != null)
return |Obj val -> Obj?| { makeFromXxxCtor.call(val) }
return null
}
}