Пользовательский интерфейс UnmarshalYAML для интерфейса и его реализации - PullRequest
0 голосов
/ 11 марта 2020

Я реализовал интерфейс Fruit и две его реализации: Apple и Banana.

В объекты двух реализаций я хочу загрузить данные из файла yaml:

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

Я реализовал интерфейс UnmarshalYaml для загрузки данных в мои объекты:

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)
        }
    }
}

Однако это не работает. Похоже, что данные для тегов Apple и Banana не загружены. Возможно, из-за отсутствия флага yaml для среза Fruits в моей структуре tmpFruitBasket. Но, поскольку Fruit является интерфейсом, я не могу определить флаг yaml. В будущем я хочу реализовать другие структуры, представляющие конкретные фрукты (например, Strawberry), реализующие интерфейс Fruit.

Есть идеи, как решить эту проблему?

1 Ответ

2 голосов
/ 11 марта 2020

Это промежуточный тип, который вам нужен:

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

Тогда функция загрузки будет выглядеть следующим образом:

// 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 и просто объединить их. Этот ответ является продолжением изучения проблемы динамической загрузки входных данных различных типов, начатой ​​с ваших предыдущих вопросов. Это имеет смысл, только если в какой-то момент вы больше не знаете тип stati c (например, если вы предоставляете API для добавления дополнительных типов фруктов во время выполнения).

...