接口及其实现的自定义 UnmarshalYAML 接口 [英] Custom UnmarshalYAML interface for an interface and its implementations

查看:47
本文介绍了接口及其实现的自定义 UnmarshalYAML 接口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我实现了一个接口 Fruit 和它的两个实现:AppleBanana.

I implemented an interface Fruit and two implementations of it: Apple and Banana.

到两个实现的对象中我想从 yaml 文件加载数据:

Into objects of the two implementations I want to load data from a yaml file:

capacity: 4
Apple:
- name: "apple1"
  number: 1
- name: "apple2"
  number: 1
Banana:
- name: "banana1"
  number: 2

我实现了 UnmarshalYaml 接口以将数据加载到我的对象中:

I implemented the UnmarshalYaml interface to load data into my objects:

package main

import (
    "errors"
    "gopkg.in/yaml.v3"
    "log"
    "fmt"
)

type FruitBasket struct {
    Capacity int `yaml:"capacity"`
    Fruits []Fruit
}

func NewFruitBasket() *FruitBasket {
    fb := new(FruitBasket)

    return fb
}

type Fruit interface {
    GetFruitName() string
    GetNumber() int
}

type Apple struct {
    Name string `yaml:"name"`
    Number int `yaml:"number"`
}

type Banana struct {
    Name string `yaml:"name"`
    Number int `yaml:"number"`
}

func (apple *Apple) GetFruitName() string {
    return apple.Name
}

func (apple *Apple) GetNumber() int {
    return apple.Number
}

func (banana *Banana) GetFruitName() string {
    return banana.Name
}

func (banana *Banana) GetNumber() int {
    return banana.Number
}

type tmpFruitBasket struct {
    Capacity int `yaml:"capacity"`
    Fruits []map[string]yaml.Node
}

func (fruitBasket *FruitBasket) UnmarshalYAML(value *yaml.Node) error {
    var tmpFruitBasket tmpFruitBasket

    if err := value.Decode(&tmpFruitBasket); err != nil {
        return err
    }

    fruitBasket.Capacity = tmpFruitBasket.Capacity

    fruits := make([]Fruit, 0, len(tmpFruitBasket.Fruits))

    for i := 0; i < len(tmpFruitBasket.Fruits); i++ {
        for tag, node := range tmpFruitBasket.Fruits[i] {
            switch tag {
            case "Apple":
                apple := &Apple{}
                if err := node.Decode(apple); err != nil {
                    return err
                }

                fruits = append(fruits, apple)
            case "Banana":
                banana := &Banana{}
                if err := node.Decode(banana); err != nil {
                    return err
                }

                fruits = append(fruits, banana)
            default:
                return errors.New("Failed to interpret the fruit of type: \"" + tag + "\"")
            }
        }
    }

    fruitBasket.Fruits = fruits

    return nil
}

func main() {
    data := []byte(`
capacity: 4
Apple:
- name: "apple1"
  number: 1
- name: "apple2"
  number: 1
Banana:
- name: "banana1"
  number: 2
`)

    fruitBasket := NewFruitBasket()

    err := yaml.Unmarshal(data, &fruitBasket)

    if err != nil {
        log.Fatalf("error: %v", err)
    }

    fmt.Println(fruitBasket.Capacity)

    for i := 0; i < len(fruitBasket.Fruits); i++ {
        switch fruit := fruitBasket.Fruits[i].(type) {
        case *Apple:
            fmt.Println(fruit.Name)
            fmt.Println(fruit.Number)
        }
    }
}

然而,这是行不通的.似乎没有加载 AppleBanana 标签的数据.可能是因为在我的 tmpFruitBasket 结构中缺少 Fruits 切片的 yaml 标志.但是,由于 Fruit 是一个接口,我无法定义 yaml 标志.将来,我想实现其他表示具体水果(例如草莓)的结构,实现接口 Fruit.

However, this is not working. It seems that the data for the Apple and Banana tags are not loaded. Probably, because of the missing yaml flag for the Fruits slice in my tmpFruitBasket struct. But, as Fruit is an interface, I cannot define a yaml flag. In the future, I want to implement other structs representing concrete fruits (e.g., Strawberry) implementing the interface Fruit.

知道如何解决这个问题吗?

Any idea on how to solve this?

推荐答案

这是您需要的中间类型:

This is the intermediate type you need:

type tmpFruitBasket struct {
  Capacity int
  Apple    []yaml.Node `yaml:"Apple"`
  Banana   []yaml.Node `yaml:"Banana"`
}

然后,加载函数将如下所示:

Then, the loading function will look like:

// helper to load a list of nodes as a concrete type
func appendFruits(fruits []Fruit, kind reflect.Type, input []yaml.Node) ([]Fruit, error) {
  for i := range input {
    val := reflect.New(kind).Interface()
    if err := input[i].Decode(val); err != nil {
      return nil, err
    }
    fruits = append(fruits, val.(Fruit))
  }
  return fruits, nil
}


func (fruitBasket *FruitBasket) UnmarshalYAML(value *yaml.Node) error {
    var tmp tmpFruitBasket

    if err := value.Decode(&tmp); err != nil {
        return err
    }

    fruitBasket.Capacity = tmp.Capacity

    var fruits []Fruit
    var err error
    // sadly, there is no nicer way to get the reflect.Type of Apple / Banana
    fruits, err = appendFruits(
      fruits, reflect.TypeOf((*Apple)(nil)).Elem(), tmp.Apple)
    if err != nil {
      return err
    }
    fruits, err = appendFruits(
      fruits, reflect.TypeOf((*Banana)(nil)).Elem(), tmp.Banana)
    if err != nil {
      return err
    }

    fruitBasket.Fruits = fruits
    return nil
}

如果您坚持将每种类型分类到一个专用切片中,您当然可以直接将它们输入为 []Apple[]Banana 并合并它们.这个答案是从您之前的问题开始,继续深入研究将输入动态加载到不同类型的问题.仅当您在某个时候不再知道静态类型时,这样做才有意义(例如,如果您提供 API 以在运行时添加其他水果类型).

If you stick with sorting each type into a dedicated slice, you could of course directly type those as []Apple and []Banana and just merge them. This answer is a continuation of digging into the issue of dynamically loading input to different types, started with your previous questions. Doing this only makes sense if you at some point don't know the static type anymore (e.g. if you provide an API to add additional fruit types at runtime).

这篇关于接口及其实现的自定义 UnmarshalYAML 接口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆