Описание
Кодогенератор создаёт код, который из конструкций вида map[string]interface{}
или map[interface{}]interface{}
позволяет получать структуру с данными. И соответственно обратно из структуры получить map[string]interface{}
. Работает без рефлексии.
Как это работает
Кодогенератор считывает proto файл, выполняет лексический анализ, далее синтаксический анализ и семантический, обходя дерево синтаксиса выражает модель proto с помощью Java классов. В модели классов написаны методы генерации, кокторые создают код маппера. Лексический анализ (токенезация) проводится с помощью регулярных выражений jFlex, семантический, используя Yacc и правила производства (построение дерева синтаксиса).
Поддержка языков
Имея модель с данными в виде Java классов можно написать кодогенератор для любых языков. На данный момент меня интересовала генерация для Go. На выходе получается маппер из map в struct и обратно. Для генерации под другой язык достаточно по аналогии реализовать generateGolangToMap и generateGolangFromMap (пакет model).
Использование
Создать файл schema.proto
, и выполнить java -jar Protogen.jar
на выходе будет создан каталог go, там и будет результат выполнения.
Возможные ошибки
При разработке кодогенератора не стояло цели реализовать 100% разбор proto файла, поэтому ограничьтесь только package и message. В конечном счёте это только инструмент, помощник.
Так сложилось
Проект разрабатывается с помощью Apache NetBeans, понимаю ваше негодование, но так сложилось.
Пример входного файла и результата кодогенерации
Исходный файл:
syntax = "proto3";
package model;
message ProductPrice {
int64 ProductId = 1;
int64 PriceId = 2;
optional string Description = 3;
repeated int64 PriceCharacteristicIds = 4;
}
Результат кодогенерации:
// generated by protogen
package model
type ProductPrice struct {
ProductId int64
PriceId int64
Description *string
PriceCharacteristicIds []int64
}
func (s ProductPrice) ToMap() (map[string]interface{}) {
m := make(map[string]interface{})
m["product_id"] = s.ProductId
m["price_id"] = s.PriceId
if s.Description != nil {
m["description"] = *(s.Description)
}
PriceCharacteristicIds := make([]interface{}, 0)
for _, e := range s.PriceCharacteristicIds {
PriceCharacteristicIds = append(PriceCharacteristicIds, e)
}
m["price_characteristic_ids"] = PriceCharacteristicIds
return m
}
func MapProductPrice(i interface{}) (ProductPrice, bool) {
var p ProductPrice
if m, ok := cast.ToMap(i); ok {
if v, ok := cast.ToInt64(m["product_id"]); ok {
p.ProductId = v
} else {
return p, false
}
if v, ok := cast.ToInt64(m["price_id"]); ok {
p.PriceId = v
} else {
return p, false
}
if v, ok := cast.ToString(m["description"]); ok {
p.Description = &v
}
if PriceCharacteristicIds, ok := cast.ToSlice(m["price_characteristic_ids"]); ok {
for _, e := range PriceCharacteristicIds {
if v, ok := cast.ToInt64(e); ok {
p.PriceCharacteristicIds = append(p.PriceCharacteristicIds, v)
}
}
}
return p, true
}
return p, false
}
Пример кода использующего результат кодогенерации
Пример кода:
func main() {
productPrice1 := model.ProductPrice{
ProductId: 1,
PriceId: 2,
}
fmt.Println(productPrice1.ToMap())
srcProductPrice := map[string]interface{}{
"product_id": 12,
"price_id": 34,
"description": "Привет",
"price_characteristic_ids": []interface{}{5446, 5447, 5449},
}
if productPrice, ok := model.MapProductPrice(srcProductPrice); ok {
fmt.Println(productPrice)
}
}
Вывод в консоль:
map[price_characteristic_ids:[] price_id:2 product_id:1]
{12 34 0xc000028300 [5446 5447 5449]}
Что еще потребуется
Потребуется написать небольшую библиотеку «cast», это очень просто. Вот несколько заготовок:
cast_int64.go:
package cast
func ToInt64(i interface{}) (int64, bool) {
switch v := i.(type) {
case int64:
return v, true
case int:
return int64(v), true
case int32:
return int64(v), true
// ...
}
return 0, false
}
cast_map.go:
package cast
func ToMap(i interface{}) (map[string]interface{}, bool) {
switch e := i.(type) {
case map[string]interface{}:
return e, true
case map[interface{}]interface{}:
m := map[string]interface{}{}
for k, v := range e {
m[k.(string)] = v
}
return m, true
}
return nil, false
}
cast_slice.go:
package cast
func ToSlice(i interface{}) ([]interface{}, bool) {
switch e := i.(type) {
case []interface{}:
return e, true
}
return nil, false
}
cast_string.go
package cast
func ToString(i interface{}) (string, bool) {
switch v := i.(type) {
case string:
return v, true
}
return "", false
}
Описание
Кодогенератор модели и маппера на основе proto файла