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

Kate

Administrator
Команда форума

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

Есть интерфейс и есть несколько типов удовлетворяющих этому интерфейсу. Хочется сделать так, что бы можно было сохранить в 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.(mfj.JsonInterfaceMarshaller); ok {
sw := mfj.IStructView{}
sw.Type = ujo.UnmarshalJSONTypeName()
sw.Data, err = json.Marshal(obj)
swl = sw
} else {
swl = 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.Type == "" {
objRaw = nil
} else if tmp.Data == nil {
to, er0 := mfj.GlobalStructFactory.GetNil(tmp.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 = toTrans
} else {
to, er0 := mfj.GlobalStructFactory.Get(tmp.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.Data, &toTrans)
if err != nil {
return err
}
objRaw = 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.(mfj.JsonInterfaceMarshaller); ok {
sw := mfj.IStructView{}
sw.Type = ujo.UnmarshalJSONTypeName()
sw.Data, err = json.Marshal(obj)
swl = sw
} else {
swl = 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.Type == "" {
objRaw = nil
} else if tmp.Data == nil {
to, er0 := mfj.GlobalStructFactory.GetNil(tmp.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 = toTrans
} else {
to, er0 := mfj.GlobalStructFactory.Get(tmp.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.Data, &toTrans)
if err != nil {
return err
}
objRaw = 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.(mfj.JsonInterfaceMarshaller); ok {
sw := mfj.IStructView{}
sw.Type = ujo.UnmarshalJSONTypeName()
sw.Data, err = json.Marshal(obj.PlaneShapes)
swl = sw
} else {
swl = 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.Type == "" {
obj.PlaneShapes = nil
} else if tmp.PlaneShapes.Data == nil {
to, er0 := mfj.GlobalStructFactory.GetNil(tmp.PlaneShapes.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 = toTrans
} else {
to, er0 := mfj.GlobalStructFactory.Get(tmp.PlaneShapes.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.Data, &toTrans)
if err != nil {
return err
}
obj.PlaneShapes = 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
})
}


 
Сверху