Reputation: 21
I looking for an elegant solution to unmarshall yaml with different kind of nested object without interface which requires to have a common method (polymorphism).
Consider the following yaml :
agents:
-
identifier: agent_1
type: lawyer
parameters:
name: "My agent 1"
speciality: "law"
sub_speciality: "criminal"
-
identifier: agent_2
type: accountant
parameters:
name: "My agent 2"
expertise: "accounting"
-
identifier: agent_3
type: carpenter
parameters:
name: "My agent 3"
tool: "hammer"
Description : This yaml contain a list of agents, each agent has a single type, several agent can have the same type or a different type.
In order to unmarshall this yaml into struct we need to define the following go function :
package main
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
)
const (
LawAgentType = "lawyer"
AccountAgentType = "accountant"
CarpenterAgentType = "carpenter"
)
type (
Agents struct {
Agent []Agent `yaml:"agents"`
}
Agent struct {
Identifier string `yaml:"identifier"`
Type string `yaml:"type"`
ParametersRaw yaml.Node `yaml:"parameters"`
Parameters ParametersInterface `yaml:"-"`
}
ParametersInterface interface {
GetName() string
}
LawParameters struct {
Parameters `yaml:",inline"`
Speciality string `yaml:"speciality"`
SubSpeciality string `yaml:"sub_speciality"`
}
AccountParameters struct {
Parameters `yaml:",inline"`
Expertise string `yaml:"expertise"`
}
CarpenterParameters struct {
Parameters `yaml:",inline"`
Tool string `yaml:"tool"`
}
Parameters struct {
Name string `yaml:"name"`
}
)
func (p *Parameters) GetName() string {
return p.Name
}
func main() {
f, err := os.ReadFile("config.yaml")
if err != nil {
log.Panic("Error reading config file")
}
var agents *Agents
err = yaml.Unmarshal(f, &agents)
if err != nil {
fmt.Println(err.Error())
}
for i, agent := range agents.Agent {
switch agent.Type {
case LawAgentType:
var parameters LawParameters
err = agent.ParametersRaw.Decode(¶meters)
if err != nil {
log.Panic("Error reading config file")
}
agents.Agent[i].Parameters = ¶meters
case AccountAgentType:
var parameters AccountParameters
err = agent.ParametersRaw.Decode(¶meters)
if err != nil {
log.Panic("Error reading config file")
}
agents.Agent[i].Parameters = ¶meters
case CarpenterAgentType:
var parameters CarpenterParameters
err = agent.ParametersRaw.Decode(¶meters)
if err != nil {
log.Panic("Error reading config file")
}
agents.Agent[i].Parameters = ¶meters
}
}
fmt.Println(agents)
}
This method returns the right data in right struct :
Agent
struct has a field ParametersRaw
which is a node.Yaml.
First, we unmarshall yaml to get the type, then we unmarshall ParametersRaw
into the right struct according to its type.
All the agent types have the same interface ParametersInterface
satisfied by GetName()
carried by the Parameters
struct and embedded in each agent type (LawAgentType
, AccountAgentType
, CarpenterAgentType
)
Because it's possible to store different object under parameters that don't always have a common denominator (ex in previous example: Name
), I'd now like to proceed in the same way, but without a common denominator.
Please, consider the following Yaml :
agents:
-
identifier: agent_1
type: lawyer
parameters:
speciality: "law"
sub_speciality: "criminal"
-
identifier: agent_2
type: accountant
parameters:
expertise: "accounting"
-
identifier: agent_3
type: carpenter
parameters:
tool: "hammer"
We no longer have the common denominator (field name
under Parameters
, nor Parameters
struct).
type (
Agents struct {
Agent []Agent `yaml:"agents"`
}
Agent struct {
Identifier string `yaml:"identifier"`
Type string `yaml:"type"`
ParametersRaw yaml.Node `yaml:"parameters"`
Parameters ???? `yaml:"-"`
}
LawParameters struct {
Speciality string `yaml:"speciality"`
SubSpeciality string `yaml:"sub_speciality"`
}
AccountParameters struct {
Expertise string `yaml:"expertise"`
}
CarpenterParameters struct {
Tool string `yaml:"tool"`
}
)
Is it possible to store different kind of parameters (LawAgentType
, AccountAgentType
, CarpenterAgentType
) under Agent
struct's Parameters
field without defining a common field to satisfy an interface ?
In other word, can we combine structures under a single struct/interface without identical fields like the classic inheritance in other OOP languages (even if go doesn't have an OOP model) ?
Define a struct which wrap all sub struct as follows:
agents:
-
identifier: agent_1
type: lawyer
parameters:
speciality: "law"
sub_speciality: "criminal"
-
identifier: agent_2
type: accountant
parameters:
expertise: "accounting"
-
identifier: agent_3
type: carpenter
parameters:
tool: "hammer"
package main
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
)
const (
LawAgentType = "lawyer"
AccountAgentType = "accountant"
CarpenterAgentType = "carpenter"
)
type (
Agents struct {
Agent []Agent `yaml:"agents"`
}
Agent struct {
Identifier string `yaml:"identifier"`
Type string `yaml:"type"`
ParametersRaw yaml.Node `yaml:"parameters"`
Parameters Parameters `yaml:"-"`
}
Parameters struct {
*LawParameters
*AccountParameters
*CarpenterParameters
}
LawParameters struct {
Speciality string `yaml:"speciality"`
SubSpeciality string `yaml:"sub_speciality"`
}
AccountParameters struct {
Expertise string `yaml:"expertise"`
}
CarpenterParameters struct {
Tool string `yaml:"tool"`
}
)
func main() {
f, err := os.ReadFile("config.yaml")
if err != nil {
log.Panic("Error reading config file")
}
var agents *Agents
err = yaml.Unmarshal(f, &agents)
if err != nil {
fmt.Println(err.Error())
}
for i, agent := range agents.Agent {
switch agent.Type {
case LawAgentType:
var parameters LawParameters
err = agent.ParametersRaw.Decode(¶meters)
if err != nil {
log.Panic("Error reading config file")
}
agents.Agent[i].Parameters.LawParameters = ¶meters
case AccountAgentType:
var parameters AccountParameters
err = agent.ParametersRaw.Decode(¶meters)
if err != nil {
log.Panic("Error reading config file")
}
agents.Agent[i].Parameters.AccountParameters = ¶meters
case CarpenterAgentType:
var parameters CarpenterParameters
err = agent.ParametersRaw.Decode(¶meters)
if err != nil {
log.Panic("Error reading config file")
}
agents.Agent[i].Parameters.CarpenterParameters = ¶meters
}
}
fmt.Println(agents)
}
Result :
I don't find this solution elegant since it implies having uninitialised struct under the Parameters
struct (ex: if LawParameters is set, AccountParameters and CarpenterParameters are not). I'm looking for a more appropriate solution if any of you have one.
Upvotes: 2
Views: 71
Reputation: 1
You can move Name
field into Agent
struct and set Parameters
field's type empty interface (interface{}
). To define each agent's parameters type you can implement yaml.Unmarshaler
interface in Agent
type like below.
package main
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
)
const (
LawAgentType = "lawyer"
AccountAgentType = "accountant"
CarpenterAgentType = "carpenter"
)
type (
Agents struct {
Agent []Agent `yaml:"agents"`
}
Agent struct {
Identifier string
Type string
Name string
Parameters interface{}
}
LawParameters struct {
Speciality string `yaml:"speciality"`
SubSpeciality string `yaml:"sub_speciality"`
}
AccountParameters struct {
Expertise string `yaml:"expertise"`
}
CarpenterParameters struct {
Tool string `yaml:"tool"`
}
)
func (a *Agent) UnmarshalYAML(value *yaml.Node) error {
a.Identifier = value.Content[1].Value
a.Type = value.Content[3].Value
a.Name = value.Content[5].Value
switch a.Type {
case LawAgentType:
a.Parameters = &LawParameters{}
case AccountAgentType:
a.Parameters = &AccountParameters{}
case CarpenterAgentType:
a.Parameters = &CarpenterParameters{}
default:
panic(fmt.Errorf("invalid agent type: %v", a.Type))
}
return value.Content[7].Decode(a.Parameters)
}
func main() {
f, err := os.ReadFile("config.yaml")
if err != nil {
log.Panic("Error reading config file")
}
var agents Agents
err = yaml.Unmarshal(f, &agents)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(agents)
}
as you can see Agent
's fields have no more tags, because unmarshalling logic is moved into UnmarshalYAML
method, but NOTE that order of agent fields in config.yaml file must be the same, because in my example identifier, type, name, parameters are taken by index from Content slice. You can add your logic using value
's fields to determine each fields value and do not depend on order from config file.
Also, you can use type switch construct to detect parameters type
for _, a := range agents.Agent {
switch params := a.Parameters.(type) {
case *LawParameters:
fmt.Println("Law", params.Speciality, params.SubSpeciality)
case *AccountParameters:
fmt.Println("Account", params.Expertise)
case *CarpenterParameters:
fmt.Println("Carpenter", params.Tool)
}
}
Upvotes: 0