...

вторник, 23 апреля 2019 г.

[Из песочницы] Динамическое изменение схемы JSON в Go с помощью gob

Значимо изменить маршализацию структуры в json можно только через метод MarshalJSON(), написав там полную реализацию маршализации. Как именно? На это документация Go ни ответов, ни рекомендаций не даёт, предоставляя, так сказать, полную свободу. А как воспользоваться этой свободой так, чтобы не нагородить кучу костылей, перенеся всю логику в MarshalJSON(), и потом не перепиливать эту бедную постоянно разрастающуюся функцию при очередных кастомизациях json?

Решение на самом деле простое:


  1. Будь честен (честна).

(Второго пункта не будет, первого хватит.)

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

Действительно ли нам нужно изменить структуру нашего объекта и напихать кучу костылей? Действительно ли нам вдруг стала мешать строгость языка, которая предусматривает взаимнооднозначное соответствие атрибутов json и самой структуры?

Исходная задача — получить такие-то JSON структуры каких-то утверждённых форматов. В исходной задаче про костыли ничего не сказано. Сказано про разные структуры данных. А у нас для хранения этих данных используется один и тот же тип данных (struct). Таким образом наша единая сущность должна иметь несколько представлений. Вот мы и получили правильную интерпретацию задачи.

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

Итак, у нас появляется ещё одна сущность — представление.

И давайте уже к примерам и, собственно, к коду.

Пусть у нас есть книжный магазин, который продаёт книги. Всё у нас построено на микросервисах, и один из них отдаёт данные по запросам в формате json. Книги сначала выгружались только на витрину сайта. Потом мы подключись к разного рода партнёрским сетям, и, предоставляем, например, учащимся университетов книги по специальной цене. А недавно, наши маркетологи вдруг решили проводить какие-то промо-акции и им тоже нужна своя цена и ещё какой-то свой текст. Вычислением цен и подготовкой текстов пусть занимается какой-то другой микросервис, который складывает уже готовые данные в базу данных.

Итак, эволюция нашей модели книги дошла до такого безобразия:

type Book struct {
        Id               int64
        Title            string
        Description      string
        Partner2Title    string
        Price            int64
        PromoPrice       int64
        PromoDescription string
        Partner1Price    int64
        Partner2Price    int64
        UpdatedAt        time.Time
        CreatedAt        time.Time
        view             BookView
}

Последний атрибут (view) — неэкспортируемый (приватный), он не является частью данных, а является местом хранения того самого представления, в котором и содержится информация, в какой json сворачиваться объекту. В простейшем случае это просто interface{}

type BookView interface{}

Мы также можем добавить в интерфейс нашего представления какой-либо метод, например Prepare(), который будет вызываться в MarshalJSON() и как-то подготавливать, валидировать, или логировать выходную структуру.

Теперь давайте опишем наши представления и саму функцию

type SiteBookView struct {
        Id          int64  `json:"sku"`
        Title       string `json:"title"`
        Description string `json:"description"`
        Price       int64  `json:"price"`
}

type Partner1BookView struct {
        Id            int64  `json:"bid"`
        Title         string `json:"title"`
        Partner1Price int64  `json:"price"`
}

type Partner2BookView struct {
        Id            int64  `json:"id"`
        Partner2Title string `json:"title"`
        Description   string `json:"description"`
        Partner2Price int64  `json:"price"`
}

type PromoBookView struct {
        Id               int64  `json:"ref"`
        Title            string `json:"title"`
        Description      string `json:"description"`
        PromoPrice       int64  `json:"price"`
        PromoDescription string `json:"promo,omitempty"`
}

func (b Book) MarshalJSON() (data []byte, err error) {
        //сначала проверяем, установлено ли представление
        if b.view == nil {
                //если нет, то устанавливаем представление по умолчанию
                b.SetDefaultView()
        }
        //затем создаём буфер для перегона значений в представление
        var buff bytes.Buffer
        // создаём отправителя данных, который будет кодировать в некий бинарный формат и складывать в буфер
        enc := gob.NewEncoder(&buff)
        //создаём приёмник данных, который будет декодировать из бинарные данные, взятые из буфера
        dec := gob.NewDecoder(&buff)
        //отправляем данные из базовой структуры
        err = enc.Encode(b)
        if err != nil {
                return
        }
        //принимаем их в наше отображение
        err = dec.Decode(b.view)
        if err != nil {
                return
        }
        //маршализуем отображение стандартным способом
        return json.Marshal(b.view)
}

Отправка и приём данных между структурами происходит по принципу совпадения названий атрибутов, при этом типы не обязательно должны точно совпадать, можно, например, отправлять из int64, а принимать в int, но не в uint.

Последним шагом делаем маршализацию установленного представления с данными, используя всю мощь стандартного описания через теги json (`json:"promo,omitempty"`)

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

func init() {
        gob.Register(Book{})
        gob.Register(SiteBookView{})
        gob.Register(Partner1BookView{})
        gob.Register(Partner2BookView{})
        gob.Register(PromoBookView{})
}

Полный код модели:


Скрытый текст
import (
        "bytes"
        "encoding/gob"
        "encoding/json"
        "time"
)

func init() {
        gob.Register(Book{})
        gob.Register(SiteBookView{})
        gob.Register(Partner1BookView{})
        gob.Register(Partner2BookView{})
        gob.Register(PromoBookView{})
}

type BookView interface{}

type Book struct {
        Id               int64
        Title            string
        Description      string
        Partner2Title    string
        Price            int64
        PromoPrice       int64
        PromoDescription string
        Partner1Price    int64
        Partner2Price    int64
        UpdatedAt        time.Time
        CreatedAt        time.Time
        view             BookView
}

type SiteBookView struct {
        Id          int64  `json:"sku"`
        Title       string `json:"title"`
        Description string `json:"description"`
        Price       int64  `json:"price"`
}

type Partner1BookView struct {
        Id            int64  `json:"bid"`
        Title         string `json:"title"`
        Partner1Price int64  `json:"price"`
}

type Partner2BookView struct {
        Id            int64  `json:"id"`
        Partner2Title string `json:"title"`
        Description   string `json:"description"`
        Partner2Price int64  `json:"price"`
}

type PromoBookView struct {
        Id               int64  `json:"ref"`
        Title            string `json:"title"`
        Description      string `json:"description"`
        PromoPrice       int64  `json:"price"`
        PromoDescription string `json:"promo,omitempty"`
}

func (b *Book) SetDefaultView() {
        b.SetSiteView()
}

func (b *Book) SetSiteView() {
        b.view = &SiteBookView{}
}

func (b *Book) SetPartner1View() {
        b.view = &Partner1BookView{}
}

func (b *Book) SetPartner2View() {
        b.view = &Partner2BookView{}
}

func (b *Book) SetPromoView() {
        b.view = &PromoBookView{}
}

func (b Book) MarshalJSON() (data []byte, err error) {
        if b.view == nil {
                b.SetDefaultView()
        }
        var buff bytes.Buffer
        enc := gob.NewEncoder(&buff)
        dec := gob.NewDecoder(&buff)
        err = enc.Encode(b)
        if err != nil {
                return
        }
        err = dec.Decode(b.view)
        if err != nil {
                return
        }
        return json.Marshal(b.view)
}


В контролере будет примерно такой код:

func GetBooksForPartner2(ctx *gin.Context) {
    books := LoadBooksForPartner2()

    for i := range books {
        books[i].SetPartner2View()
    }

    ctx.JSON(http.StatusOK, books)
}

Теперь для «ещё одного» изменения json достаточно просто добавить ещё одно представление и не забыть зарегистрировать его в init().

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий