sourceafBedSheet::ValueEncoders.fan

using afBeanUtils::BeanBuilder

** (Service) - Contribute your 'ValueEncoder' classes to this.
** 
** @uses a Configuration of 'Type:ValueEncoder' where 'Type' is what the 'ValueEncoder', um, encodes!
const mixin ValueEncoders {
    
    ** Converts the given 'value' to Str via a contributed `ValueEncoder`. If no 'ValueEncoder' is 
    ** found, 'toStr()' is used. 
    ** 
    ** 'valType' is NOT 'Str#'!!! But rather the Type of the Obj that is being converted. 
    ** Required to ensure the correct 'ValueEncoder' is used, and to convert 'null' to default instances.
    abstract Str toClient(Type valType, Obj? value)
    
    ** Converts the given 'clientValue' into the given 'valType' via a contributed `ValueEncoder`. 
    ** If no 'ValueEncoder' is found the value is [coerced]`afBeanUtils::TypeCoercer`.
    abstract Obj? toValue(Type valType, Str clientValue)

    ** Finds a 'ValueEncoder' for the given type. Returns 'null' if none found.
    abstract ValueEncoder? find(Type valType)
}

internal const class ValueEncodersImpl : ValueEncoders {
    private const CachingTypeCoercer    typeCoercer := CachingTypeCoercer()
    private const CachingTypeLookup     valueEncoderLookup
    
    internal new make(Type:ValueEncoder valueEncoders) {
        this.valueEncoderLookup = CachingTypeLookup(valueEncoders)
    }
    
    override Str toClient(Type valType, Obj? value) {
        // don't shortcut strings - someone (like me!) may want special str valEnc handling!
        // assuming no-one would want to deviate from the default is a very annoying "Brian" thing!
        
        // give the val encs a chance to handle nulls
        valEnc := find(valType)
        if (valEnc != null)
            try {
                return valEnc.toClient(value)
            } catch (ReProcessErr reprocess) {
                throw reprocess
            } catch (Err cause) {
                throw ValueEncodingErr(BsErrMsgs.valueEncoding_buggered(value, Str#), cause)
            }
        
        // don't bother with a TypeCoercer, just toStr it.
        // by default, represent null as an empty string
        return value?.toStr ?: Str.defVal
    }

    override Obj? toValue(Type valType, Str clientValue) {
        // don't shortcut strings - someone (like me!) may want special str valEnc handling!
        // assuming no-one would want to deviate from the default is a very annoying "Brian" thing!

        // give the val encs a chance to handle nulls
        valEnc := find(valType)
        if (valEnc != null) {
            value := null
            try {
                value = valEnc.toValue(clientValue)
            } catch (ReProcessErr reprocess) {
                throw reprocess
            } catch (Err cause) {
                throw ValueEncodingErr(BsErrMsgs.valueEncoding_buggered(clientValue, valType), cause)
            }
            // a final null compatibility check
            if (value == null && !valType.isNullable)
                throw ValueEncodingErr(BsErrMsgs.valueEncoding_buggered(clientValue, valType))
            return value
        }

        // empty string values WILL ALWAYS DIE in the coercer, so treat them as null and create a default value
        if (clientValue.trim.isEmpty)
            try return BeanBuilder.defVal(valType)
            catch (Err cause)
                throw ValueEncodingErr(BsErrMsgs.valueEncoding_buggered(clientValue, valType), cause)
        
        try return typeCoercer.coerce(clientValue, valType) 
        catch (Err cause)
            throw ValueEncodingErr(BsErrMsgs.valueEncoding_buggered(clientValue, valType), cause)
    }

    override ValueEncoder? find(Type valueType) {
        valueEncoderLookup.findParent(valueType, false)
    }
}