Home > Software design >  How to mimic a union type
How to mimic a union type

Time:11-25

I know that using custom types is a common question, but bear with me...

I would like to define a custom type 'ConnectionInfo' (see below):

type DataSource struct {
    gorm.Model

    Name           string
    Type           DataSourceType `sql:"type:ENUM('POSTGRES')" gorm:"column:data_source_type"`
    ConnectionInfo ConnectionInfo `gorm:"embedded"`
}

I would like to restrict ConnectionInfo to be one of a limited number of types, i.e.:

type ConnectionInfo interface {
    PostgresConnectionInfo | MySQLConnectionInfo
}

How can I do this?

My progress thus far:

I defined a ConnectionInfo interface (I now know this is invalid in GORM, but how do I get around it?)

type ConnectionInfo interface {
    IsConnectionInfoType() bool
}

I've then implemented this interface with two types (and implemented the scanner and valuer interfaces) like so:

type PostgresConnectionInfo struct {
    Host     string
    Port     int
    Username string
    Password string
    DBName   string
}

func (PostgresConnectionInfo) IsConnectionInfoType() bool {
    return true
}

func (p *PostgresConnectionInfo) Scan(value interface{}) error {
    bytes, ok := value.([]byte)
    if !ok {
        return fmt.Errorf("failed to unmarshal the following to a PostgresConnectionInfo value: %v", value)
    }

    result := PostgresConnectionInfo{}
    if err := json.Unmarshal(bytes, &result); err != nil {
        return err
    }
    *p = result

    return nil
}

func (p PostgresConnectionInfo) Value() (driver.Value, error) {
    return json.Marshal(p)
}

But of course I get the following error:

unsupported data type: <myproject>/models.ConnectionInfo

WORKING ANSWER

Thanks to Shahriar Ahmed, I have an idiomatic solution, I just wanted to add a little extra on how I am (trying) to ensure a bit of type safety when handling the ConnectionInfo type outside of the models package.

My ConnectionInfo field now looks like so:

type DataSource struct {
    gorm.Model

    ConnectionInfo connectionInfo `gorm:"type:jsonb;not null"`
}

Its type is how Shahriar advised and includes each of the subtypes/variants that also implement the Scanner and Valuer interfaces:

type connectionInfo struct {
    Postgres *PostgresConnectionInfo `gorm:"-" json:"postgres,omitempty"`
    MySQL    *MySQLConnectionInfo    `gorm:"-" json:"mysql,omitempty"`
}

func (c *connectionInfo) Scan(src any) error {
    switch src := src.(type) {
    case nil:
        return nil
    case []byte:
        var res connectionInfo
        err := json.Unmarshal(src, &res)
        *c = res
        return err
    default:
        return fmt.Errorf("unable to scan type %T into connectionInfo", src)
    }
}

func (c connectionInfo) Value() (driver.Value, error) {
    return json.Marshal(c)
}

However, I have not exported the 'connectionInfo' type (by using a lowercase 'c'), and I've created an exported 'ConnectionInfo' interface which the 'PostgresConnectionInfo' and 'MySQLConnectionInfo' types implement:

type ConnectionInfo interface {
    IsConnectionInfoType() bool
}

type PostgresConnectionInfo struct {
    Host     string `json:"host" binding:"required"`
    Port     int    `json:"port" binding:"required"`
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
    DBName   string `json:"dbName" binding:"required"`
}

func (PostgresConnectionInfo) IsConnectionInfoType() bool {
    return true
}

When wanting to reference the abstract type, I will use ConnectionInfo and then pass this to my models package which will use the below get the concrete type and instantiate a 'connectionInfo' type:

func getConcreteConnectionInfo(connInfo ConnectionInfo) connectionInfo {
    switch v := connInfo.(type) {
    case *PostgresConnectionInfo:
        return connectionInfo{Postgres: v}
    case *MySQLConnectionInfo:
        return connectionInfo{MySQL: v}
    default:
        panic(fmt.Sprintf("Unknown connection info type: %T", connInfo))
    }
}

CodePudding user response:

Instead of using this union, you can approach this way GITHUB LINK. You can clone these repo and run the code. This is working.

package storage

import (
    "database/sql/driver"
    "encoding/json"
    "fmt"
    "log"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

type DataSourceType string

const (
    POSTGRES DataSourceType = "POSTGRES"
    MYSQL    DataSourceType = "MYSQL"
)

type PostgresConnectionInfo struct {
    Host     string
    Port     int
    Username string
    Password string
    DBName   string
}

type MySQLConnectionInfo struct {
    Host     string
    Port     int
    Username string
    Password string
    DBName   string
}

type ConnectionInfo struct {
    Postgres *PostgresConnectionInfo `gorm:"-" json:"postgres,omitempty"`
    Mysql    *MySQLConnectionInfo    `gorm:"-" json:"mysql,omitempty"`
}
type DataSource struct {
    gorm.Model
    Name           string
    Type           DataSourceType `sql:"type:ENUM('POSTGRES')" gorm:"column:data_source_type"`
    ConnectionInfo ConnectionInfo `gorm:"type:json" `
}

func (a *ConnectionInfo) Scan(src any) error {
    switch src := src.(type) {
    case nil:
        return nil
    case []byte:
        var res ConnectionInfo
        err := json.Unmarshal(src, &res)
        *a = res
        return err

    default:
        return fmt.Errorf("scan: unable to scan type %T into struct", src)
    }

}

func (a ConnectionInfo) Value() (driver.Value, error) {
    ba, err := json.Marshal(a)
    return ba, err
}

func GormTest2() {
    db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        log.Fatal("could not open database")
    }
    err = db.AutoMigrate(&DataSource{})
    if err != nil {
        log.Fatal("could not migrate database")
    }
    createTestData1(db)
    fetchData1(db)
}

func createTestData1(db *gorm.DB) {
    ds := []DataSource{
        {
            Name: "Postgres",
            Type: POSTGRES,
            ConnectionInfo: ConnectionInfo{
                Postgres: &PostgresConnectionInfo{
                    Host:     "localhost",
                    Port:     333,
                    Username: "sdlfj",
                    Password: "sdfs",
                    DBName:   "sdfsd",
                },
            },
        },
        {
            Name: "Mysql",
            Type: MYSQL,
            ConnectionInfo: ConnectionInfo{
                Mysql: &MySQLConnectionInfo{
                    Host:     "localhost",
                    Port:     333,
                    Username: "sdlfj",
                    Password: "sdfs",
                    DBName:   "sdfsd",
                },
            },
        },
    }
    err := db.Create(&ds).Error
    if err != nil {
        log.Println("failed to create data")
    }
}

func fetchData1(db *gorm.DB) {
    var dsList []DataSource
    if err := db.Find(&dsList).Error; err != nil {
        log.Println("failed to load data")
    }
    log.Println(dsList)
}

  • Related