sourceafMorphia::BsonConvs.fan

using afBson::Binary
using afBson::MaxKey
using afBson::MinKey
using afBson::ObjectId
using afBson::Timestamp
using afBson::BsonType
using afBeanUtils::BeanBuilder

** (Service) - 
** Converts Fantom objects to and from their BSON representation.
const mixin BsonConvs {

    ** Returns a new 'BsonConvs' instance.
    ** 
    ** If 'converters' is 'null' then 'defConvs' is used. Some defaults are:
    ** 
    **   pickleMode        : false
    **   makeEntityFn      : |Type type, Field:Obj? fieldVals->Obj?| { BeanBuilder.build(type, vals) }
    **   makeBsonObjFn     : |->Str:Obj? |    { Str:Obj?[:] { ordered = true } }
    **   makeMapFn         : |Type t->Map|    { Map((t.isGeneric ? Obj:Obj?# : t).toNonNullable) { it.ordered = true } }
    **   docToTypeFn       : |Str:Obj?->Type| { null }
    **   storeNullFields   : false
    **   strictMode        : false
    **   propertyCache     : BsonPropCache()
    ** 
    ** Override 'makeEntityFn' to have IoC create entity instances.
    ** 
    ** Set 'strictMode' to 'true' to Err if the BSON contains unmapped data.
    ** 
    ** *pickleMode* is where all non '@Transient' fields are converted, regardless of any '@BsonProp' facets. 
    ** Data from '@BsonProp' facets, however, will still honoured if defined.
    static new make([Type:BsonConv]? converters := null, [Str:Obj?]? options := null) {
        BsonConvsImpl(converters ?: defConvs, options)
    }

    ** Returns a new 'BsonConvs' whose options are overridden with the given ones.
    abstract BsonConvs withOptions(Str:Obj? newOptions)
    
    ** Returns the 'Converter' instance used to convert the given type. 
    @Operator
    abstract BsonConv get(Type type)

    ** The default set of BSON <-> Fantom converters.
    static Type:BsonConv defConvs() {
        BsonConvsImpl._defConvs
    }



    @NoDoc  // not sure why we'd want these to be pubic?
    internal abstract Obj? _toBsonCtx(Obj? fantomObj, BsonConvCtx ctx)

    @NoDoc  // not sure why we'd want these to be pubic?
    internal abstract Obj? _fromBsonCtx(Obj? bsonVal, BsonConvCtx ctx)
    
    

    ** Converts the given Fantom object to its BSON representation.
    ** 
    ** 'fantomObj' is nullable so converters can create empty / default objects.
    ** 'fantomType' in case 'fantomObj' is null, but defaults to 'fantomObj?.typeof'. 
    abstract Obj? toBsonVal(Obj? fantomObj, Type? fantomType := null)
    
    ** Converts a BSON value to the given Fantom type.
    ** If 'fantomType' is 'null' then the obj is inspected for a '_type' property,
    ** else a reasonable *guess* is made (and the option 'docToTypeFn' is then called as a last resort.)
    ** 
    ** 'bsonVal' is nullable so converters can choose whether or not to create empty lists and maps.
    abstract Obj? fromBsonVal(Obj? bsonVal, Type? fantomType := null)   


    
    ** Converts the given Fantom object to its BSON object representation.
    ** 
    ** Convenience for calling 'toBsonVal()' with a cast.
    abstract [Str:Obj?]? toBsonDoc(Obj? fantomObj)
    
    ** Converts a BSON object to the given Fantom type.
    ** 
    ** Convenience for calling 'fromBsonVal()' with a cast.
    ** If 'fantomType' is 'null' then the obj is inspected for a '_type' property.
    abstract Obj? fromBsonDoc([Str:Obj?]? bsonObj, Type? fantomType := null)
    
    
    ** Returns the 'BsonPropCache' which stores '@BsonProp' meta for Types.
    abstract BsonPropCache propertyCache()
}

internal const class BsonConvsImpl : BsonConvs {
    override const BsonPropCache    propertyCache
             const BsonTypeLookup   typeLookup
             const Unsafe           optionsRef  // use Unsafe because JS can't handle immutable functions

    new make(|This| f) { f(this) }
    
    new makeArgs(Type:BsonConv converters, [Str:Obj?]? options) {
        this.typeLookup = BsonTypeLookup(converters)
        this.optionsRef = Unsafe(Str:Obj?[
            "makeEntityFn"  : |Type type, Field:Obj? vals->Obj?| { BeanBuilder.build(type, vals) },
            "makeBsonObjFn" : |->Str:Obj? | { Str:Obj?[:] { ordered = true } },
            "makeMapFn"     : |Type t->Map| { Map((t.isGeneric ? Obj:Obj?# : t).toNonNullable) { it.ordered = true } },
            "strictMode"    : false,
            "propertyCache" : BsonPropCache(),
        ])
        
        if (options != null)
            this.optionsRef = Unsafe(this.options.rw.setAll(options))

        if (Env.cur.runtime != "js")
            // JS can't handle immutable functions, but I'd still like them to be thread safe in Java
            optionsRef = Unsafe(optionsRef.val.toImmutable)
        
        this.propertyCache  = this.options["propertyCache"]
    }

    Str:Obj? options() { optionsRef.val }
    
    override BsonConvs withOptions(Str:Obj? newOptions) {
        BsonConvsImpl {
            it.optionsRef       = Unsafe(this.options.rw.setAll(newOptions))
            it.propertyCache    = it.options["propertyCache"] ?: this.propertyCache
            it.typeLookup       = this.typeLookup
        }
    }
    
    override Obj? _toBsonCtx(Obj? fantomObj, BsonConvCtx ctx) {
        hookVal := ctx.toBsonHookFn(fantomObj)      
        return get(ctx.type).toBsonVal(fantomObj, ctx)
    }

    override Obj? _fromBsonCtx(Obj? bsonVal, BsonConvCtx ctx) {
        hookVal := ctx.fromBsonHookFn(bsonVal)
        return get(ctx.type).fromBsonVal(hookVal, ctx)
    }

    override Obj? toBsonVal(Obj? fantomObj, Type? fantomType := null) {
        if (fantomType == null) fantomType = fantomObj?.typeof
        if (fantomType == null) return null // this null is just convenience to allow [args].map { it?.typeof }
        ctx := BsonConvCtx.makeTop(this, fantomType, fantomObj, options)
        return _toBsonCtx(fantomObj, ctx)
    }

    override Obj? fromBsonVal(Obj? bsonVal, Type? fantomType := null) {

        // if type is not supplied, take our best guess!
        if (fantomType == null) {
            // convenience to allow [args].map { it?.typeof }
            if (bsonVal == null)
                return null

            if (fantomType == null)
                // convert BSON literals, don't just return them
                // that way users can override conversions if need be
                fantomType = BsonType.fromType(bsonVal.typeof, false)?.type

            if (fantomType == null)
                // convert lists so we may infer what the inner objects are
                if (bsonVal is List)
                    fantomType = Obj?[]#

            if (fantomType == null)
                if (bsonVal is Map) {
                    _type := ((Obj:Obj?) bsonVal).get("_type")
                    if (_type is Str)
                        _type = Type.find(_type, false) // "false" because _type may not exist in the calling application 
                    if (_type is Type)
                        fantomType = _type

                    if (fantomType == null) {
                        fn := options["docToTypeFn"] as |Str:Obj? -> Type|
                        fantomType = fn?.call(bsonVal)  
                    }
                    
                    if (fantomType == null)
                        fantomType = [Str:Obj?]#
                }

            if (fantomType == null)
                throw ArgErr("Do not know how to convert BSON val, please supply a fantomType arg - ${bsonVal.typeof}")
        }

        ctx := BsonConvCtx.makeTop(this, fantomType, bsonVal, options)
        return _fromBsonCtx(bsonVal, ctx)
    }

    override [Str:Obj?]? toBsonDoc(Obj? fantomObj) {
        // let's not dick about - just convert null to null
        if (fantomObj == null) return null
        return toBsonVal(fantomObj, fantomObj.typeof)
    }
    
    override Obj? fromBsonDoc([Str:Obj?]? bsonVal, Type? fantomType := null) {
        fromBsonVal(bsonVal, fantomType)
    }

    override BsonConv get(Type type) {
        // if a specific converter can't be found then embed a record
        typeLookup.findParent(type)
    }
    
    static Type:BsonConv _defConvs() {
        config              := Type:BsonConv[:]
        bsonLiteral         := BsonLiteralConv()

        // BSON Literals - https://bson.org/
        config[Bool#]       = bsonLiteral
        config[Binary#]     = bsonLiteral
        config[Buf#]        = bsonLiteral
        config[DateTime#]   = bsonLiteral
        config[Float#]      = bsonLiteral
        config[Int#]        = bsonLiteral
        config[MaxKey#]     = bsonLiteral
        config[MinKey#]     = bsonLiteral
        config[ObjectId#]   = bsonLiteral
        config[Regex#]      = bsonLiteral
        config[Str#]        = bsonLiteral
        config[Timestamp#]  = bsonLiteral
        
        // Containers
        config[Obj#]        = BsonObjConv()
        config[Map#]        = BsonMapConv()
        config[List#]       = BsonListConv()

        // Fantom Literals
        config[Date#]       = BsonDateConv()
        config[Decimal#]    = BsonSimpleConv(Decimal#)
        config[Depend#]     = BsonSimpleConv(Depend#)
        config[Duration#]   = BsonSimpleConv(Duration#)
        config[Enum#]       = BsonEnumConv()
        config[Locale#]     = BsonSimpleConv(Locale#)
        config[MimeType#]   = BsonSimpleConv(MimeType#)
        config[Range#]      = BsonSimpleConv(Range#)
        config[Slot#]       = BsonSlotConv()
        config[Time#]       = BsonSimpleConv(Time#)
        config[TimeZone#]   = BsonSimpleConv(TimeZone#)
        config[Type#]       = BsonTypeConv()
        config[Unit#]       = BsonSimpleConv(Unit#)
        config[Uri#]        = BsonSimpleConv(Uri#)
        config[Uuid#]       = BsonSimpleConv(Uuid#)
        config[Version#]    = BsonSimpleConv(Version#)
        
        return config
    }
}