3
0
corteza/pkg/codegen/store.go
2020-08-24 15:38:42 +02:00

453 lines
11 KiB
Go

package codegen
import (
"fmt"
"github.com/cortezaproject/corteza-server/pkg/slice"
"gopkg.in/yaml.v2"
"os"
"path"
"path/filepath"
"strings"
"text/template"
)
type (
// definitions are in one file
storeDef struct {
Package string
App string
Source string
Filename string
Import []string `yaml:"import"`
// List of all locations where we should export the store interface to
Interface []string `yaml:"interface"`
// Tries to autogenerate type by changing it to singular and prefixing it with *types.
Types storeTypeDef `yaml:"types"`
// All known fields that we need to store on a particular type
//
// For now, this set does not variate between different implementation
// To support that, a (sub)set will need to be defined under each implementation (rdbms, mysql, mongo...)
//
Fields storeTypeFieldSetDef `yaml:"fields"`
Lookups []*storeTypeLookups `yaml:"lookups"`
PartialUpdates []*storeTypePartialUpdate `yaml:"partialUpdates"`
RDBMS *storeTypeRdbmsDef `yaml:"rdbms"`
Search storeTypeSearchDef `yaml:"search"`
}
storeTypeDef struct {
// Name of the package where type can be found
// (defaults to types)
Package string `yaml:"package"`
// Name of the base type
// (defaults to base name of the yaml file)
Base string `yaml:"base"`
// Singular variation of name
// (defaults to <Base> (s trimmed))
Singular string `yaml:"singular"`
// Plural variantion of name
// (defaults to <Singular> (s appended))
Plural string `yaml:"plural"`
// Name of the set go type
// (defaults to <Package>.<Singular>)
GoType string `yaml:"type"`
// Name of the set go type
// (defaults to <GoType>Set)
GoSetType string `yaml:"setType"`
// Name of the filter go type
// (defaults to <GoType>Filter)
GoFilterType string `yaml:"filterType"`
}
storeTypeRdbmsDef struct {
// Alias used in SQL queries
Alias string `yaml:"alias,omitempty"`
Table string `yaml:"table,omitempty"`
CustomRowScanner bool `yaml:"customRowScanner"`
CustomFilterConverter bool `yaml:"customFilterConverter"`
CustomEncoder bool `yaml:"customEncoder"`
}
storeTypeFieldSetDef []*storeTypeFieldDef
storeTypeFieldDef struct {
Field string `yaml:"field"`
// Autodiscovery logic (when not explicitly set)
// uint64: when field has "ID" suffix
// time.Time: when field equals with "created_at"
// *time.Time: when field ends with "_at"
// string: default
Type string `yaml:"type"`
// When not explicitly set, defaults to snake-cased value from field
//
// Exceptions:
// If field name ends with ID (<base>ID), it converts that to rel_<snake-cased-base>
Column string `yaml:"column"`
// If field is flagged as PK it is used in update & remove conditions
// Note: if no other field is set as primary and field with ID name
// exists, that field is auto-set as primary.
IsPrimaryKey bool `yaml:"isPrimaryKey"`
// FilterPreprocess sets preprocessing function used on
// conditions for lookup functions
//
// See specific implementation for details
LookupFilterPreprocess string `yaml:"lookupFilterPreprocessor"`
alias string
}
storeTypeLookups struct {
// LookupBy<suffix>
// When not explicitly defined, it names of all fields
Suffix string `yaml:"suffix"`
Description string `yaml:"description"`
Fields []string `yaml:"fields"`
Filter map[string]string `yaml:"filter"`
fields storeTypeFieldSetDef
}
storeTypePartialUpdate struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Set map[string]string `yaml:"set"`
XX_Args []string `yaml:"args"`
fields storeTypeFieldSetDef
}
storeTypeSearchDef struct {
Disable bool `yaml:"disable"`
DisablePaging bool `yaml:"disablePaging"`
}
)
var (
outputDir string = "store"
)
func procStore() ([]*storeDef, error) {
procDef := func(m string) (*storeDef, error) {
def := &storeDef{Source: m}
f, err := os.Open(m)
if err != nil {
return nil, fmt.Errorf("%s read failed: %w", m, err)
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(&def); err != nil {
return nil, err
}
def.Filename = path.Base(m)
def.Filename = def.Filename[:len(def.Filename)-5]
// Always generate interface in store/tests and store/bulk
def.Interface = append(def.Interface, "store/tests", "store/bulk")
if def.Types.Base == "" {
def.Types.Base = pubIdent(strings.Split(def.Filename, "_")...)
}
if def.Types.Singular == "" {
def.Types.Singular = strings.TrimRight(def.Types.Base, "s")
}
if def.Types.Plural == "" {
def.Types.Plural = def.Types.Singular + "s"
}
if def.Types.Package == "" {
def.Types.Package = "types"
}
if def.Types.GoType == "" {
def.Types.GoType = def.Types.Package + "." + pubIdent(def.Types.Singular)
}
if def.Types.GoSetType == "" {
def.Types.GoSetType = def.Types.GoType + "Set"
}
if def.Types.GoFilterType == "" {
def.Types.GoFilterType = def.Types.GoType + "Filter"
}
if def.RDBMS.Alias == "" {
def.RDBMS.Alias = def.Types.Base[0:1]
}
var hasPrimaryKey = false
for _, f := range def.Fields {
if f.IsPrimaryKey {
hasPrimaryKey = true
break
}
}
for _, f := range def.Fields {
if !hasPrimaryKey && f.Field == "ID" {
f.IsPrimaryKey = true
}
// copy alias from global spec so we can
// generate aliased columsn
f.alias = def.RDBMS.Alias
if f.Column == "" {
switch {
case f.Field != "ID" && strings.HasSuffix(f.Field, "ID"):
f.Column = "rel_" + cc2underscore(f.Field[:len(f.Field)-2])
default:
f.Column = cc2underscore(f.Field)
}
}
switch {
case f.Type != "":
// type set
case strings.HasSuffix(f.Field, "ID") || strings.HasSuffix(f.Field, "By"):
f.Type = "uint64"
case f.Field == "CreatedAt":
f.Type = "time.Time"
case strings.HasSuffix(f.Field, "At"):
f.Type = "uint64"
default:
f.Type = "string"
}
}
if len(def.PartialUpdates) > 0 && def.Fields.Find("ID") == nil {
return nil, fmt.Errorf("partial updates without ID field are not supported")
}
// Checking if filters exist in the fields
for i, p := range def.PartialUpdates {
// Check and normalize set
for f, v := range p.Set {
if def.Fields.Find(f) == nil {
return nil, fmt.Errorf("undefined field %q used in partialUpdate #%d set", f, i)
}
if v == "" {
// Set empty strings to nil
p.Set[f] = "nil"
}
}
for _, a := range p.XX_Args {
if def.Fields.Find(a) == nil {
return nil, fmt.Errorf("undefined field %q used in partialUpdate #%d arguments", a, i)
}
}
p.fields = def.Fields
}
for i, l := range def.Lookups {
if len(l.Fields) == 0 {
return nil, fmt.Errorf("define at least one lookup field in lookup #%d", i)
}
// Checking if fields exist in the fields
for _, f := range l.Fields {
if def.Fields.Find(f) == nil {
return nil, fmt.Errorf("undefined lookup field %q used", f)
}
}
// Checking if filters exist in the fields
for f, v := range l.Filter {
if def.Fields.Find(f) == nil {
return nil, fmt.Errorf("undefined lookup filter %q used", f)
}
if v == "" {
// Set empty strings to nil
l.Filter[f] = "nil"
}
}
if l.Suffix == "" {
l.Suffix = strings.Join(l.Fields, "")
}
l.fields = def.Fields
}
return def, nil
}
mm, err := filepath.Glob(filepath.Join(outputDir, "*.yaml"))
if err != nil {
return nil, fmt.Errorf("failed to glob: %w", err)
}
dd := []*storeDef{}
for _, m := range mm {
def, err := procDef(m)
if err != nil {
return nil, fmt.Errorf("failed to process %s: %w", m, err)
}
dd = append(dd, def)
}
return dd, nil
}
// genStore generates all store related code, functions, interfaces...
//
// Templates can be found under assets/store*.tpl
func genStore(tpl *template.Template, dd []*storeDef) (err error) {
var (
// general interfaces
tplInterfacesJoined = tpl.Lookup("store_interfaces_joined.gen.go.tpl")
tplInterfaces = tpl.Lookup("store_interfaces.gen.go.tpl")
// general tests
tplTestAll = tpl.Lookup("store_test_all.gen.go.tpl")
// @todo in-memory
// rdbms specific
tplRdbms = tpl.Lookup("store_rdbms.gen.go.tpl")
// @todo redis
// @todo mongodb
// @todo elasticsearch
// bulk specific
tplBulk = tpl.Lookup("store_bulk.gen.go.tpl")
dst string
joinedInterface = make(map[string][]*storeDef)
)
// Output all test setup into a single file
dst = path.Join(outputDir, "tests", "gen_test.go")
if err = goTemplate(dst, tplTestAll, dd); err != nil {
return
}
// Multi-file output
for _, d := range dd {
dst = path.Join(outputDir, "rdbms", d.Filename+".gen.go")
if err = goTemplate(dst, tplRdbms, d); err != nil {
return
}
dst = path.Join(outputDir, "bulk", d.Filename+".gen.go")
if err = goTemplate(dst, tplBulk, d); err != nil {
return
}
// Collect and map all interface output locations
// and their corresponding definitions
for _, dst = range d.Interface {
if err = genStoreInterfaces(tplInterfaces, path.Join(dst, "store_interface_"+d.Filename+".gen.go"), path.Base(dst), d); err != nil {
return
}
if joinedInterface[dst] == nil {
joinedInterface[dst] = make([]*storeDef, 0, len(dd))
}
joinedInterface[dst] = append(joinedInterface[dst], d)
}
}
// Add joined interfaces for each interface destination
for dst, dd := range joinedInterface {
if err = genStoreInterfacesJoined(tplInterfacesJoined, path.Join(dst, "store_interface.gen.go"), path.Base(dst), dd); err != nil {
return
}
}
//for _, d := range dd {
// d.Package = "tests"
// dst = path.Join("store/tests", "store_interface_"+d.Filename+".gen.go")
// if err = goTemplate(dst, tplInterfaces, d); err != nil {
// return
// }
//}
return nil
}
func genStoreInterfaces(tpl *template.Template, dst, pkg string, d *storeDef) error {
d.Package = pkg
return goTemplate(dst, tpl, d)
}
func genStoreInterfacesJoined(tpl *template.Template, dst, pkg string, dd []*storeDef) error {
payload := map[string]interface{}{
"Package": pkg,
"Definitions": dd,
"Import": collectStoreDefImports("", dd...),
}
return goTemplate(dst, tpl, payload)
}
func collectStoreDefImports(basePkg string, dd ...*storeDef) []string {
ii := make([]string, 0, len(dd))
for _, d := range dd {
for _, i := range d.Import {
if !slice.HasString(ii, i) && (basePkg == "" || !strings.HasSuffix(i, basePkg)) {
ii = append(ii, i)
}
}
}
return ii
}
func (s storeTypeFieldSetDef) Find(name string) *storeTypeFieldDef {
for _, f := range s {
if f.Field == name {
return f
}
}
return nil
}
func (f storeTypeFieldDef) Arg() string {
if f.Field == "ID" {
return f.Field
}
return strings.ToLower(f.Field[:1]) + f.Field[1:]
}
func (f storeTypeFieldDef) AliasedColumn() string {
return fmt.Sprintf("%s.%s", f.alias, f.Column)
}
func (p storeTypePartialUpdate) Args() []*storeTypeFieldDef {
ff := make([]*storeTypeFieldDef, len(p.XX_Args))
for a := range p.XX_Args {
ff[a] = p.fields.Find(p.XX_Args[a])
}
return ff
}