Суть проблемы
Есть интерфейс и есть несколько типов удовлетворяющих этому интерфейсу. Хочется сделать так, что бы можно было сохранить в 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()
jsf.mx.Unlock()
jsf.Generators[name] = generator
}
func (jsf *StructFactory) AddNil(name string, generator JsonUnmarshalObjectGenerate) {
jsf.mx.Lock()
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
})
}