Как стать автором
Обновить

Кодогенерация в GO на примере маршалинга и анмаршалинга интерфейсных типов данных

Время на прочтение10 мин
Количество просмотров5.2K

Суть проблемы

Есть интерфейс и есть несколько типов удовлетворяющих этому интерфейсу. Хочется сделать так, что бы можно было сохранить в JSON список таких интерфейсов а потом восстановить из JSON-а этот список.

Пример на геометрических фигурах
package geom

import (
	"math"
)

type PlaneShape interface {
	Area() float64
	Perimeter() float64
}

type PlaneShapes []PlaneShape

type Picture struct {
	Name        string
	PlaneShapes []PlaneShape
}

type Point struct {
	X, Y float64
}

type Line struct {
	X1, Y1, X2, Y2 float64
}

type Rectangle struct {
	X1, Y1, X2, Y2 float64
}

type Circle struct {
	X, Y, R float64
}

func (f *Point) Area() float64 {
	return 0
}
func (f *Line) Area() float64 {
	return 0
}
func (f *Rectangle) Area() float64 {
	return math.Abs(f.X1-f.X2) * math.Abs(f.Y1-f.Y2)
}
func (f *Circle) Area() float64 {
	return math.Pi * f.R * f.R
}

func (f *Point) Perimeter() float64 {
	return 0
}
func (f *Line) Perimeter() float64 {
	return math.Sqrt((f.X1-f.X2)*(f.X1-f.X2) + (f.Y1-f.Y2)*(f.Y1-f.Y2))
}
func (f *Rectangle) Perimeter() float64 {
	return (math.Abs(f.X1-f.X2) + math.Abs(f.Y1-f.Y2)) * 2
}
func (f *Circle) Perimeter() float64 {
	return math.Pi * f.R * 2
}

Хочется переопределить маршалинг и анмаршалинг для структур PlaneShapesи Picture

Как решить

Что бы получить типизацию, нужно добавить поле тип в сохраняемом JSON-е или сделать контейнер, который будет содержать тип и полезные данные. И при анмаршалинге использовать тип что бы получить нужную структуру.

примеры JSON

Контейнер:

{
  "_type": "point",
  "data": {
    "x": 2,
    "y": 7.5,
  }
}

Тип внутри структуры:

{
  "_type": "point",
  "x": 2,
  "y": 7.5,
}

Где _type - тип объекта

Тип решения

На GO можно решить такого рода проблему либо кодогенерацией, либо рефлексией либо комбинацией методов. Если решать вопрос рефлексией то нужно переписать весь механизм JSON. Это по сути ненужная задача, так как родной механизм достаточно хорош и есть неплохие альтернативы, которые хочется использовать при необходимости. По этому я решил попробовать решить задачу кодогенерацией. Так же не хочется усложнять маршалинг и анмаршалинг, так что в решении я буду использовать контейнерный подход.

Что будем использовать

Контейнер

Объекты будут маршалиться в контейнер:

//easyjson:json
type IStructView struct {
	Type string          `json:"_type"`
	Data json.RawMessage `json:"data"`
}

Сразу сделаем, что бы маршалинг этого объекта был через easyjson потому, что он мне нравится.

Фабрика объектов

Каждый объект должен иметь метод который позволит получить тип объекта.

Что бы по типу получить нужный объект, нужно иметь фабрику объектов по их типу c методами:

  • Добавить генератор

  • Добавить генератор NIL объекта

  • Получить объект

  • Получить NIL объект

Реализация
type JsonInterfaceMarshaller interface {
	UnmarshalJSONTypeName() string
}

type StructFactory struct {
	Generators    map[string]JsonUnmarshalObjectGenerate
	GeneratorsNil map[string]JsonUnmarshalObjectGenerate

	mx sync.RWMutex
}

var GlobalStructFactory = &StructFactory{
	Generators:    map[string]JsonUnmarshalObjectGenerate{},
	GeneratorsNil: map[string]JsonUnmarshalObjectGenerate{},
}

func (jsf *StructFactory) Add(name string, generator JsonUnmarshalObjectGenerate) {
	jsf.mx.Lock()
	defer jsf.mx.Unlock()
	jsf.Generators[name] = generator
}
func (jsf *StructFactory) AddNil(name string, generator JsonUnmarshalObjectGenerate) {
	jsf.mx.Lock()
	defer jsf.mx.Unlock()
	jsf.GeneratorsNil[name] = generator
}

Что нужно генерировать

Для объекта который реализовывает интерфейс нужно добавить регистрацию в генераторе и добавить метод, который будет выдавать тип объекта.


func (obj *Point) UnmarshalJSONTypeName() string {
	return "geom.point"
}

func init() {
	mfj.GlobalStructFactory.Add("geom.point", func() mfj.JsonInterfaceMarshaller { return &Point{} })
	mfj.GlobalStructFactory.AddNil("geom.point", func() mfj.JsonInterfaceMarshaller {
		var out *Point
		return out
	})
}

А для объектов содержащих поля с интерфейсными типами нужно описать методы MarshalJSON и UnmarshalJSON.

Что бы это сделать для каждого такого типа создадим прокси тип. Данные основной структуры будем записывать в прокси структуру, а данные из интерфейсных типов будем сохранять в виде IStructView. Получившуюся прокси структуру будем маршалить стандартными способами.

Пример определения прокси типов
type PlaneShapes_mjson_wrap []mfj.IStructView


type Picture_mjson_wrap struct {
	Name string

	// PlaneShapes []PlaneShape
	PlaneShapes []mfj.IStructView
}
Пример определения методов
type PlaneShapes_mjson_wrap []mfj.IStructView
func (obj PlaneShapes) MarshalJSON() (res []byte, err error) {
	if obj == nil {
		var out PlaneShapes_mjson_wrap
		return json.Marshal(out)
	}
	out := make(PlaneShapes_mjson_wrap, len(obj))
	swl := make([]mfj.IStructView, len(obj))
	for i := 0; i < len(obj); i++ {
		if ujo, ok := obj[i].(mfj.JsonInterfaceMarshaller); ok {
			sw := mfj.IStructView{}
			sw.Type = ujo.UnmarshalJSONTypeName()
			sw.Data, err = json.Marshal(obj[i])
			swl[i] = sw
		} else {
			swl[i] = mfj.IStructView{}
		}
	}

	return json.Marshal(out)
}
func (obj *PlaneShapes) UnmarshalJSON(data []byte) (err error) {
	if data == nil {
		return nil
	}
	var tmp PlaneShapes_mjson_wrap
	err = json.Unmarshal(data, &tmp)
	if err != nil {
		return err
	}
	if tmp == nil {
		var d PlaneShapes
		*obj = d
		return nil
	}
	objRaw := make(PlaneShapes, len(tmp))
	*obj = objRaw
	for i := 0; i < len(tmp); i++ {
		if tmp[i].Type == "" {
			objRaw[i] = nil
		} else if tmp[i].Data == nil {
			to, er0 := mfj.GlobalStructFactory.GetNil(tmp[i].Type)
			if er0 != nil {
				return er0
			}
			toTrans, ok := to.(PlaneShape)
			if !ok {
				return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR, NIL)")
			}
			objRaw[i] = toTrans
		} else {
			to, er0 := mfj.GlobalStructFactory.Get(tmp[i].Type)
			if er0 != nil {
				return er0
			}
			toTrans, ok := to.(PlaneShape)
			if !ok {
				return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR)")
			}
			err = json.Unmarshal(tmp[i].Data, &toTrans)
			if err != nil {
				return err
			}
			objRaw[i] = toTrans
		}
	}
	return nil
}

Как генерировать

На хабре есть классная статья про кодогенерацию тут.

Распарсим файл с именемfilename. Для этого будем использовать пакет go/token.

	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
	if err != nil {
		return err
	}

Имя пакета лежит в node.Name.Name

В объекте поле node.Imports хранятся описания import из файла.

Если мы их будем перебирать, то

	for _, imp := range node.Imports {
  	// imp.Name - имя пакета если оно использовалось например так
    // log "github.com/sirupsen/logrus"
		if imp.Name != nil {
			// imp.Name.Name - само имя log
		}
    
		// imp.Path.Value - путь к пакету "github.com/sirupsen/logrus"
    // с кавычками
	}

В node.Decls хранятся описания объектов. Нас интересуют только *ast.GenDecl которые содержат информацию об описанных объектах.

	for _, f := range node.Decls {
		genD, ok := f.(*ast.GenDecl)
		if !ok {
			continue
		}
    for _, spec := range genD.Specs {
    	currType, ok := spec.(*ast.TypeSpec)
      if !ok {
        // Нас интересуют только типы
				continue
			}
      
      switch currType.Type.(type) {
			case *ast.StructType:
				// это описание структуры
        // например:
        // type ABCD struct {
        // 		A int
        // }
			case *ast.ArrayType:
				// это тип слайс
        // например:
        // type ABCDList []ABCD
			case *ast.MapType:
				// это тип мап
        // например:
        // type ABCDs map[string]ABCD
			default:
				// Это всё остальное
			}
    }
    if genD.Doc != nil && genD.Doc.List != nil {
      // вот тут содержится комментарий к объекту
      // например:
      // //sometext
      // type ABCDs map[string]ABCD
      for _, comment := range genD.Doc.List {
        if strings.HasPrefix(comment.Text, "//sometext") {
          // Что-то делать если есть коммент
        }
      }
    }
    
    // Разберём структуру
    currType, ok := spec.(*ast.TypeSpec)
		if !ok {
			continue
		}
		currStruct, ok := currType.Type.(*ast.StructType)
		if !ok {
			continue
		}
    
    // Пройдём по полям структуры
    for idxField, field := range currStruct.Fields.List {
      if len(field.Names) == 0 {
				continue
			}
      // field.Names[0].Name имя поля
      // field.Type тип поля
      if field.Tag != nil {
        // Получаем теги если есть
				tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
				tagVal := tag.Get("json")
        // напрмер json
      }
    }
  }

Для разбора типов я написал функцию, которая получает что это за тип и список input-ов которые требуются.

Текст функции
func getType(at interface{}) (fieldType string, isArray bool, arrLen string, isMap bool, mapKeyType string, usedInputs map[string]struct{}) {
	usedInputs = make(map[string]struct{})
	switch at.(type) {
	case *ast.Ident:
		fieldType = at.(*ast.Ident).Name
	case *ast.SelectorExpr:
		fieldType = at.(*ast.SelectorExpr).Sel.Name
		if expX, ok := at.(*ast.SelectorExpr).X.(*ast.Ident); ok {
			fieldType = expX.Name + "." + fieldType
			usedInputs[expX.Name] = struct{}{}
		}
	case *ast.StarExpr:
		subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.StarExpr).X)
		for k := range subUsedInputs {
			usedInputs[k] = struct{}{}
		}
		switch {
		case !subIsArray && !subIsMap:
			fieldType = "*" + subFieldType
		case subIsArray:
			fieldType = "*[" + subArrLen + "]" + subFieldType
		case subIsMap:
			fieldType = "*map[" + subMapKeyType + "]" + subFieldType
		}
	case *ast.MapType:
		isMap = true
		{
			subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.MapType).Key)
			for k := range subUsedInputs {
				usedInputs[k] = struct{}{}
			}
			switch {
			case !subIsArray && !subIsMap:
				mapKeyType = subFieldType
			case subIsArray:
				mapKeyType = "[" + subArrLen + "]" + subFieldType
			case subIsMap:
				mapKeyType = "map[" + subMapKeyType + "]" + subFieldType
			}
		}
		{
			subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.MapType).Value)
			for k := range subUsedInputs {
				usedInputs[k] = struct{}{}
			}
			switch {
			case !subIsArray && !subIsMap:
				fieldType = subFieldType
			case subIsArray:
				fieldType = "[" + subArrLen + "]" + subFieldType
			case subIsMap:
				fieldType = "map[" + subMapKeyType + "]" + subFieldType
			}
		}
	case *ast.ArrayType:
		isArray = true
		if at.(*ast.ArrayType).Len != nil {
			arrLen = at.(*ast.ArrayType).Len.(*ast.BasicLit).Value
		}
		subFieldType, subIsArray, subArrLen, subIsMap, subMapKeyType, subUsedInputs := getType(at.(*ast.ArrayType).Elt)
		for k := range subUsedInputs {
			usedInputs[k] = struct{}{}
		}
		switch {
		case !subIsArray && !subIsMap:
			fieldType = subFieldType
		case subIsArray:
			fieldType = "[" + subArrLen + "]" + subFieldType
		case subIsMap:
			fieldType = "map[" + subMapKeyType + "]" + subFieldType
		}
	}
	return fieldType, isArray, arrLen, isMap, mapKeyType, usedInputs
}

Результат

Написан инструмент для генерации кода для того, что бы маршалить и анмаршалить типы содержащие поля типом interface.

Для запуска генерации нужно написать mfjson file_name.go

Структуры нужно пометить вот так: //mfjson:interface struct_type_name, что бы добавить тип в фабрику объектов (что бы можно было использовать как интерфейс)

Структуры в которых нужно использовать интерфейсные типы в полях нужно пометить вот так: //mfjson:marshal , а для полей добавить атрибут mfjson:"true"

Код находится тут.

Пример использования ниже:

test_struct.go
package geom

import (
	"math"
)

//go:generate mfjson test_struct.go

type PlaneShape interface {
	Area() float64
	Perimeter() float64
}

//mfjson:marshal
type PlaneShapes []PlaneShape

//mfjson:marshal
type Picture struct {
	Name        string
	PlaneShapes []PlaneShape `json:"shapes" mfjson:"true"`
}

//mfjson:interface geom.point
type Point struct {
	X, Y float64
}

//mfjson:interface geom.line
type Line struct {
	X1, Y1, X2, Y2 float64
}

//mfjson:interface geom.rectangle
type Rectangle struct {
	X1, Y1, X2, Y2 float64
}

//mfjson:interface geom.circle
type Circle struct {
	X, Y, R float64
}

func (f *Point) Area() float64 {
	return 0
}
func (f *Line) Area() float64 {
	return 0
}
func (f *Rectangle) Area() float64 {
	return math.Abs(f.X1-f.X2) * math.Abs(f.Y1-f.Y2)
}
func (f *Circle) Area() float64 {
	return math.Pi * f.R * f.R
}

func (f *Point) Perimeter() float64 {
	return 0
}
func (f *Line) Perimeter() float64 {
	return math.Sqrt((f.X1-f.X2)*(f.X1-f.X2) + (f.Y1-f.Y2)*(f.Y1-f.Y2))
}
func (f *Rectangle) Perimeter() float64 {
	return (math.Abs(f.X1-f.X2) + math.Abs(f.Y1-f.Y2)) * 2
}
func (f *Circle) Perimeter() float64 {
	return math.Pi * f.R * 2
}
test_struct.mfjson.go
// Code generated by mfjson for marshaling/unmarshaling. DO NOT EDIT.
// https://github.com/myfantasy/json

package geom

import (
	"encoding/json"

	"github.com/myfantasy/mft"

	mfj "github.com/myfantasy/json"

)

type PlaneShapes_mjson_wrap []mfj.IStructView
func (obj PlaneShapes) MarshalJSON() (res []byte, err error) {
	if obj == nil {
		var out PlaneShapes_mjson_wrap
		return json.Marshal(out)
	}
	out := make(PlaneShapes_mjson_wrap, len(obj))
	swl := make([]mfj.IStructView, len(obj))
	for i := 0; i < len(obj); i++ {
		if ujo, ok := obj[i].(mfj.JsonInterfaceMarshaller); ok {
			sw := mfj.IStructView{}
			sw.Type = ujo.UnmarshalJSONTypeName()
			sw.Data, err = json.Marshal(obj[i])
			swl[i] = sw
		} else {
			swl[i] = mfj.IStructView{}
		}
	}

	return json.Marshal(out)
}
func (obj *PlaneShapes) UnmarshalJSON(data []byte) (err error) {
	if data == nil {
		return nil
	}
	var tmp PlaneShapes_mjson_wrap
	err = json.Unmarshal(data, &tmp)
	if err != nil {
		return err
	}
	if tmp == nil {
		var d PlaneShapes
		*obj = d
		return nil
	}
	objRaw := make(PlaneShapes, len(tmp))
	*obj = objRaw
	for i := 0; i < len(tmp); i++ {
		if tmp[i].Type == "" {
			objRaw[i] = nil
		} else if tmp[i].Data == nil {
			to, er0 := mfj.GlobalStructFactory.GetNil(tmp[i].Type)
			if er0 != nil {
				return er0
			}
			toTrans, ok := to.(PlaneShape)
			if !ok {
				return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR, NIL)")
			}
			objRaw[i] = toTrans
		} else {
			to, er0 := mfj.GlobalStructFactory.Get(tmp[i].Type)
			if er0 != nil {
				return er0
			}
			toTrans, ok := to.(PlaneShape)
			if !ok {
				return mft.ErrorS("Type 'PlaneShape' not valid in generations 'PlaneShapes' (ARR)")
			}
			err = json.Unmarshal(tmp[i].Data, &toTrans)
			if err != nil {
				return err
			}
			objRaw[i] = toTrans
		}
	}
	return nil
}

type Picture_mjson_wrap struct {
	Name string

	// PlaneShapes []PlaneShape `json:"shapes" mfjson:"true"`
	PlaneShapes []mfj.IStructView `json:"shapes" mfjson:"true"`
}

func (obj Picture) MarshalJSON() (res []byte, err error) {
	out := Picture_mjson_wrap{}
	out.Name = obj.Name
	{
		if obj.PlaneShapes == nil {
			out.PlaneShapes = nil
		} else {
			swl := make([]mfj.IStructView, len(obj.PlaneShapes))
			for i := 0; i < len(obj.PlaneShapes); i++ {
				if ujo, ok := obj.PlaneShapes[i].(mfj.JsonInterfaceMarshaller); ok {
					sw := mfj.IStructView{}
					sw.Type = ujo.UnmarshalJSONTypeName()
					sw.Data, err = json.Marshal(obj.PlaneShapes[i])
					swl[i] = sw
				} else {
					swl[i] = mfj.IStructView{}
				}
			}
			out.PlaneShapes = swl
		}
	}
	return json.Marshal(out)
}
func (obj *Picture) UnmarshalJSON(data []byte) (err error) {
	tmp := Picture_mjson_wrap{}
	if data == nil {
		return nil
	}
	err = json.Unmarshal(data, &tmp)
	if err != nil {
		return err
	}
	obj.Name = tmp.Name
	{
		if tmp.PlaneShapes == nil {
			obj.PlaneShapes = nil
		} else {
			obj.PlaneShapes = make([]PlaneShape, len(tmp.PlaneShapes))
			for i := 0; i < len(tmp.PlaneShapes); i++ {
				if tmp.PlaneShapes[i].Type == "" {
					obj.PlaneShapes[i] = nil
				} else if tmp.PlaneShapes[i].Data == nil {
					to, er0 := mfj.GlobalStructFactory.GetNil(tmp.PlaneShapes[i].Type)
					if er0 != nil {
						return er0
					}
					toTrans, ok := to.(PlaneShape)
					if !ok {
						return mft.ErrorS("Type 'PlaneShapes' not valid in generations 'PlaneShape..Picture' (NIL)")
					}
					obj.PlaneShapes[i] = toTrans
				} else {
					to, er0 := mfj.GlobalStructFactory.Get(tmp.PlaneShapes[i].Type)
					if er0 != nil {
						return er0
					}
					toTrans, ok := to.(PlaneShape)
					if !ok {
						return mft.ErrorS("Type 'PlaneShape' not valid in generations 'Picture..PlaneShapes'")
					}
					err = json.Unmarshal(tmp.PlaneShapes[i].Data, &toTrans)
					if err != nil {
						return err
					}
					obj.PlaneShapes[i] = toTrans
				}
			}
		}
	}
	return nil
}

func (obj *Point) UnmarshalJSONTypeName() string {
	return "geom.point"
}
func (obj *Line) UnmarshalJSONTypeName() string {
	return "geom.line"
}
func (obj *Rectangle) UnmarshalJSONTypeName() string {
	return "geom.rectangle"
}
func (obj *Circle) UnmarshalJSONTypeName() string {
	return "geom.circle"
}

func init() {
	mfj.GlobalStructFactory.Add("geom.point", func() mfj.JsonInterfaceMarshaller { return &Point{} })
	mfj.GlobalStructFactory.AddNil("geom.point", func() mfj.JsonInterfaceMarshaller {
		var out *Point
		return out
	})
	mfj.GlobalStructFactory.Add("geom.line", func() mfj.JsonInterfaceMarshaller { return &Line{} })
	mfj.GlobalStructFactory.AddNil("geom.line", func() mfj.JsonInterfaceMarshaller {
		var out *Line
		return out
	})
	mfj.GlobalStructFactory.Add("geom.rectangle", func() mfj.JsonInterfaceMarshaller { return &Rectangle{} })
	mfj.GlobalStructFactory.AddNil("geom.rectangle", func() mfj.JsonInterfaceMarshaller {
		var out *Rectangle
		return out
	})
	mfj.GlobalStructFactory.Add("geom.circle", func() mfj.JsonInterfaceMarshaller { return &Circle{} })
	mfj.GlobalStructFactory.AddNil("geom.circle", func() mfj.JsonInterfaceMarshaller {
		var out *Circle
		return out
	})
}
Теги:
Хабы:
+4
Комментарии2

Публикации

Истории

Работа

Go разработчик
130 вакансий

Ближайшие события