How to use interface type as a model in mgo (Go)?
Asked Answered
D

2

5

Suppose you have a workflow that consists of multiple embedded nodes of different types. Since nodes are of different types, I thought of using Golang interfaces here and came up with following:

type Workflow struct {
   CreatedAt time.Time
   StartedAt time.Time
   CreatedBy string
   Nodes []Node
}

type Node interface {
  Exec() (int, error)
}

type EmailNode struct {
   From string
   To string
   Subject string
   Body string
}

type TwitterNode struct {
   Tweet string
   Image []byte
}

func (n *EmailNode) Exec() (int, error){
   //send email
   return 0, nil
}

func (n *TwitterNode) Exec() (int, error) {
   //send tweet
   return 0, nil
}

These workflows are stored in MongoDB and I have sample seed data in it. Using mgo, when I try to find a workflow (given its ID):

w = &Workflow{}
collection.FindID(bson.ObjectIdHex(id)).One(w)

I get the error - value of type bson.M is not assignable to type Node.

It also feels a bit weird to me that how would mgo unmarshal embedded Node documents into a Go struct without any type information. May be I need to look at the problem from another point of view.

Any suggestions would be highly appreciated.

Denishadenison answered 25/9, 2014 at 13:14 Comment(0)
H
9

You cannot use an interface in a document for the reason you noted. The decoder has no information about the type to create.

One way to handle this is to define a struct to hold the type information:

type NodeWithType struct {
   Node Node `bson:"-"`
   Type string
}

type Workflow struct {
   CreatedAt time.Time
   StartedAt time.Time
   CreatedBy string
   Nodes []NodeWithType
}

Implement the SetBSON function on this type. This function should decode the type string, create a value of the correct type based on that string and unmarshal to that value.

func (nt *NodeWithType) SetBSON(r bson.Raw) error {
}
Houchens answered 25/9, 2014 at 14:8 Comment(1)
Can you give an example of how the setBSON function would be implemented? I don't know how to decode the type string.Aircool
B
7

Following Simon Fox's answer regarding the implementation of SetBSON, here is a more precise answer.

Let's take the original piece of code:

type Workflow struct {
   CreatedAt time.Time
   StartedAt time.Time
   CreatedBy string
   Nodes     []Node
}

type Node interface {
  Exec() (int, error)
}

type EmailNode struct {
   From    string
   To      string
   Subject string
   Body    string
}

type TwitterNode struct {
   Tweet string
   Image []byte
}

func (n *EmailNode) Exec() (int, error){
   //send email
   return 0, nil
}

func (n *TwitterNode) Exec() (int, error) {
   //send tweet
   return 0, nil
}

What you want to do now is: once you unmarshal the BSON object from Mongo, you want to be able to know if each node is either an EmailNode or a TwitterNode.

As you are storing the nodes as a Node interface, mgo has no way to know what struct to implement, so you have to tell it explicitly. Here comes SetBSON.

In your example, the problem comes from this Workflow.Nodes, which is a slice of Node interface. As it is a simple slice, the best is that you create a custom type that mgo will be able to call when unmarshalling the BSON:

type NodesList []Node

// The updated Workflow struct:
type Workflow struct {
    CreatedAt time.Time
    StartedAt time.Time
    CreatedBy string
    Nodes     NodesList
}

Now, you can implement SetBSON on this NodesList and describe how it works. Please note that as you are using a pointer, you can define what is contained inside the variable:

// Note that you must use a pointer to the slice
func (list *NodesList) SetBSON(raw raw.BSON) error {
    // Now you need to create the objects according to your
    // own domain logic and what's contained inside "raw":
    if raw.something {
        *list = append(*list, &TwitterNode{})
    } else {
        *list = append(*list, &EmailNode{})
    }

    return nil
}
Broddy answered 28/10, 2017 at 12:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.