sourceafFancom::Variant.fan

using [java] com.jacob.com::Variant as JVariant

**
** A multi-format data type used for all COM communications.
** 
** A Variant's type may be queried using the 'isXXX()' methods and returned with the 'asXXX()' 
** methods. Variants have the notion of being null, the equivalent of VB Nothing. Whereas Variant 
** objects themselves are never null, their 'asXXX()' methods may return null.
** 
** The [asType()]`#asType` method can be used to return any Fantom literal ('Bool', 'Int', 'Emum', 
** etc...) and also Fancom Surrogates. Surrogates are user defined classes which wrap either a 
** `Dispatch` or a `Variant` object. Surrogates make it easy to write boiler plate Fantom classes 
** that mimic the behaviour of COM components. 
** 
** 
** Variant Surrogates [#surrogate]
** ===============================
** A Variant Surrogate is any object that conforms to the following rules. (Think of it as an 
** *implied* interface.)
** 
** A Variant Surrogate **must** have either:
**  - a ctor with the signature 'new makeFromVariant(Variant variant)' or
**  - a static factory method with the signature 'static MyClass fromVariant(Variant variant)'
** 
** A Variant Surrogate **must** also have either:
**  - a method with the signature 'Variant toVariant()' or
**  - a field with the signature 'Variant variant'
**  
** This allows Variant Surrogates to be passed in as parameters to a Dispatch object, and to be 
** instantiated from the [asType()]`#asType` method.
** 
** Enums are good candidates for converting into surrogates. Also see `Flag` as another example.
** 
** Note: Fancom Surrogate methods and fields may be of any visibility - which is useful if don't 
** wish to expose them.
**  
** Note: Fancom Surrogates don't require you to implement any Mixins, reflection is used to find 
** your wrapper methods.
**
**
** Variant Types [#variantTypes]
** ===========================
** The conversion of types between the various underlying systems is given below:
** 
** pre>
**   Automation Type | JACOB Type | Fancom Type | Remarks
**   ---------------------------------------------------------------------
**    0 VT_EMPTY       null         null          Equivalent to VB Nothing
**    1 VT_NULL        null         null          Equivalent to VB Null
**    2 VT_I2          short        Int           
**    3 VT_I4          int          Int / Enum    A Long in VC
**    4 VT_R4          float        Float
**    5 VT_R8          double       Float
**    6 VT_CY          Currency     ---
**    7 VT_DATE        Date         ---           TODO -> DateTime
**    8 VT_BSTR        String       Str
**    9 VT_DISPATCH    Dispatch     Dispatch
**   10 VT_ERROR       int          ---           TODO -> throw Err
**   11 VT_BOOL        boolean      Bool
**   12 VT_VARIANT     Variant      ---           TODO -> Variant
**   14 VT_DECIMAL     BigDecimal   ---           TODO -> Decimal
**   17 VT_UI1         byte         Int
**   20 VT_I8          long         Int
** <pre
** 
** All other value types are unsupported by JACOB and hence unsupported by Fancom. For more information 
** on Automation Types see [VARENUM enumeration (Automation)]`http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170%28v=vs.85%29.aspx` on MSDN. 
** 
class Variant {
    ** The JACOB Variant this object wraps
    JVariant variant { private set }

    // names taken from the JACOB Variant class
    private Str[] typeNames := "Nothing, Null, Short, Int, Float, Double, Currency, Date, String, Dispatch, Error, Boolean, Variant, Object, Decimal, ???, VT_I1, Byte, VT_UI2, VT_UI4, Long, VT_UI8, VT_INT, VT_UNIT, VT_VOID, VT_HRESULT, Pointer, VT_SAFEARRAY, VT_CARRARY, VT_USERDEFINED".split(',', true)

    // ---- Constructors --------------------------------------------------------------------------

    ** Make a 'null' Variant
    new make() {
        this.variant = JVariant()
    }

    ** Make a Variant representing a 'Bool' value
    new makeFromBool(Bool value) {
        this.variant = JVariant(value)
    }
    
    ** Make a Variant representing a 'Float' value
    new makeFromFloat(Float value) {
        this.variant = JVariant(value)
    }
    
    ** Make a Variant representing an 'Int' value
    new makeFromInt(Int value) {
        this.variant = fromLong(value)
    }

    ** Make a Variant representing a 'Str' value
    new makeFromStr(Str value) {
        this.variant = JVariant(value)
    }
    
    ** Make a Variant wrapping the given JACOB Variant
    internal new makeFromVariant(JVariant variant) {
        this.variant = variant
    }

    // ---- Methods -------------------------------------------------------------------------------
    
    ** Returns 'true' if this Variant represents a 'null' value
    Bool isNull() {
        variant.isNull
    }
    
    ** Returns 'true' if this Variant represents a 'Bool' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isBool() {
        variantType == JVariant.VariantBoolean
    }

    ** Returns 'true' if this Variant represents a `Dispatch` object.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isDispatch() {
        variantType == JVariant.VariantDispatch
    }

    ** Returns 'true' if this Variant can be converted into the given 'Enum' type, checking that 
    ** the Variant value is a valid ordinal for the type. 
    ** Throws 'ArgErr' if the given type is not a subclass of 'Enum'.
    Bool isEnum(Type enumType) {
        if (isNull)
            return false
        if (!enumType.isEnum)
            throw ArgErr(Messages.wrongType(enumType, Enum#))
        vals := enumType.field("vals").get as Obj[]
        if (asInt >= vals.size)
            return false
        return true
    }

    ** Returns 'true' if this Variant represents a 'Float' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isFloat() {
        (variantType == JVariant.VariantFloat)  ||
        (variantType == JVariant.VariantDouble)
    }

    ** Returns 'true' if this Variant represents a 'Int' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isInt() {
        (variantType == JVariant.VariantByte)   ||
        (variantType == JVariant.VariantShort)  ||
        (variantType == JVariant.VariantInt)    ||
        (variantType == JVariant.VariantLongInt)
    }
    
    ** Returns 'true' if this Variant represents a 'Str' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isStr() {
        variantType == JVariant.VariantString
    }
    
    ** Returns the Variant value as a 'Bool' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Bool' type.
    Bool? asBool() {
        if (isNull) return null
        return isBool ? variant.getBoolean : throw FancomErr(Messages.wrongVariantType(this, Bool#))
    }
    
    ** Returns the Variant value as a `Dispatch` object or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a `Dispatch` type.
    Dispatch? asDispatch() {
        if (isNull) return null
        return isDispatch ? Dispatch(variant.getDispatch) : throw FancomErr(Messages.wrongVariantType(this, Dispatch#))
    }
    
    ** Returns the Variant value as a 'Float' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Float' type.
    Float? asFloat() {
        if (isNull) 
            return null
        if (variantType == JVariant.VariantFloat)
            return variant.getFloat
        if (variantType == JVariant.VariantDouble)
            return variant.getDouble
        throw FancomErr(Messages.wrongVariantType(this, Float#))
    }

    ** Returns the Variant value as an 'Int' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not an 'Int' type.
    Int? asInt() {
        if (isNull)
            return null
        if (variantType == JVariant.VariantByte)
            return variant.getByte
        if (variantType == JVariant.VariantShort)
            return variant.getShort
        if (variantType == JVariant.VariantInt)
            return variant.getInt
        if (variantType == JVariant.VariantLongInt)
            return variant.getLong
        throw FancomErr(Messages.wrongVariantType(this, Int#))
    }

    ** Returns the Variant value as a 'Str' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Str' type.
    Str? asStr() {
        if (isNull) return null
        return isStr ? variant.getString : throw FancomErr(Messages.wrongVariantType(this, Str#))
    }
    
    ** Returns the Variant value as an instance of the supplied 'Enum' type or 'null' if the 
    ** Variant represents 'null'. 
    ** 
    ** Throws 'ArgErr' if the given type is not a subclass of 'Enum'.
    ** Throws `FancomErr` if the Variant is not an 'Enum' ('Int') type or if the 'Int' value is not a valid ordinal for the enum.
    Enum? asEnum(Type enumType) {
        if (isNull) 
            return null
        if (!isEnum(enumType))
            throw FancomErr(Messages.wrongVariantType(this, enumType))
        vals := enumType.field("vals").get as Obj[]
        if (asInt >= vals.size)
            throw FancomErr(Messages.enumOrdinalOutOfRange(enumType, vals.size, asInt))
        return vals[asInt] 
    }

    ** A Swiss Army knife this method is; attempts to convert the Variant value into the given Type, be 
    ** it a 'Bool', 'Enum' or a Fancom wrapper. Throws `FancomErr` if unsuccessful.
    Obj? asType(Type type) {
        if (isNull) return null
        return Helper.toFantomObj(this, type) ?: throw FancomErr(Messages.wrongVariantType(this, type))
    }

    ** Returns 'true' if this Variant represents an array.
    Bool isArray() {
        variant.getvt.and(JVariant.VariantArray) == JVariant.VariantArray 
    }
    
    ** Returns 'true' if this Variant is a 'ByRef' value.
    Bool isByRef() {
        variant.getvt.and(JVariant.VariantByref) == JVariant.VariantByref
    }
    
    ** Returns details of the underlying COM value.
    override Str toStr() {
        // toString() on a safe(!) byte array gave errors, so we just disp '...' instead
        typeIndex := variantType
        typeName  := (typeNames.size > typeIndex) ? typeNames[typeIndex] : typeIndex.toStr
        byRef     := isByRef ? "*" : ""
        return isArray ? "(${typeName}[])..." : "(${typeName}${byRef})$variant.toString"
    }
    
    private Int variantType() {
        variant.getvt.and(JVariant.VariantTypeMask)
    }
    
    internal static native JVariant fromLong(Int value)
}