You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
6.7 KiB
328 lines
6.7 KiB
// Copyright 2019 The Xorm Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"xorm.io/reverse/language"
|
|
|
|
"gitea.com/lunny/log"
|
|
_ "github.com/denisenkom/go-mssqldb"
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/gobwas/glob"
|
|
_ "github.com/lib/pq"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"gopkg.in/yaml.v2"
|
|
"xorm.io/core"
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
func reverse(rFile string) error {
|
|
f, err := os.Open(rFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
var cfg ReverseConfig
|
|
err = yaml.NewDecoder(f).Decode(&cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, target := range cfg.Targets {
|
|
if err := runReverse(&cfg.Source, &target); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ReverseSource struct {
|
|
Database string `yaml:"database"`
|
|
ConnStr string `yaml:"conn_str"`
|
|
}
|
|
|
|
// ReverseTarget represents a reverse target
|
|
type ReverseTarget struct {
|
|
Type string `yaml:"type"`
|
|
IncludeTables []string `yaml:"include_tables"`
|
|
ExcludeTables []string `yaml:"exclude_tables"`
|
|
TableMapper string `yaml:"table_mapper"`
|
|
ColumnMapper string `yaml:"column_mapper"`
|
|
TemplatePath string `yaml:"template_path"`
|
|
Template string `yaml:"template"`
|
|
MultipleFiles bool `yaml:"multiple_files"`
|
|
OutputDir string `yaml:"output_dir"`
|
|
TablePrefix string `yaml:"table_prefix"`
|
|
Language string `yaml:"language"`
|
|
|
|
Funcs map[string]string `yaml:"funcs"`
|
|
Formatter string `yaml:"formatter"`
|
|
Importter string `yaml:"importter"`
|
|
ExtName string `yaml:"ext_name"`
|
|
}
|
|
|
|
// ReverseConfig represents a reverse configuration
|
|
type ReverseConfig struct {
|
|
Kind string `yaml:"kind"`
|
|
Name string `yaml:"name"`
|
|
Source ReverseSource `yaml:"source"`
|
|
Targets []ReverseTarget `yaml:"targets"`
|
|
}
|
|
|
|
var (
|
|
formatters = map[string]func(string) (string, error){}
|
|
importters = map[string]func([]*core.Table) []string{}
|
|
defaultFuncs = template.FuncMap{
|
|
"UnTitle": unTitle,
|
|
"Upper": upTitle,
|
|
}
|
|
)
|
|
|
|
func unTitle(src string) string {
|
|
if src == "" {
|
|
return ""
|
|
}
|
|
|
|
if len(src) == 1 {
|
|
return strings.ToLower(string(src[0]))
|
|
} else {
|
|
return strings.ToLower(string(src[0])) + src[1:]
|
|
}
|
|
}
|
|
|
|
func upTitle(src string) string {
|
|
if src == "" {
|
|
return ""
|
|
}
|
|
|
|
return strings.ToUpper(src)
|
|
}
|
|
|
|
func filterTables(tables []*core.Table, target *ReverseTarget) []*core.Table {
|
|
var res = make([]*core.Table, 0, len(tables))
|
|
for _, tb := range tables {
|
|
var remove bool
|
|
for _, exclude := range target.ExcludeTables {
|
|
s, _ := glob.Compile(exclude)
|
|
remove = s.Match(tb.Name)
|
|
if remove {
|
|
break
|
|
}
|
|
}
|
|
if remove {
|
|
continue
|
|
}
|
|
if len(target.IncludeTables) == 0 {
|
|
res = append(res, tb)
|
|
continue
|
|
}
|
|
|
|
var keep bool
|
|
for _, include := range target.IncludeTables {
|
|
s, _ := glob.Compile(include)
|
|
keep = s.Match(tb.Name)
|
|
if keep {
|
|
break
|
|
}
|
|
}
|
|
if keep {
|
|
res = append(res, tb)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func newFuncs() template.FuncMap {
|
|
var m = make(template.FuncMap)
|
|
for k, v := range defaultFuncs {
|
|
m[k] = v
|
|
}
|
|
return m
|
|
}
|
|
|
|
func runReverse(source *ReverseSource, target *ReverseTarget) error {
|
|
orm, err := xorm.NewEngine(source.Database, source.ConnStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tables, err := orm.DBMetas()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// filter tables according includes and excludes
|
|
tables = filterTables(tables, target)
|
|
|
|
// load configuration from language
|
|
lang := language.GetLanguage(target.Language)
|
|
funcs := newFuncs()
|
|
formatter := formatters[target.Formatter]
|
|
importter := importters[target.Importter]
|
|
|
|
// load template
|
|
var bs []byte
|
|
if target.Template != "" {
|
|
bs = []byte(target.Template)
|
|
} else if target.TemplatePath != "" {
|
|
bs, err = ioutil.ReadFile(target.TemplatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if lang != nil {
|
|
if bs == nil {
|
|
bs = []byte(lang.Template)
|
|
}
|
|
for k, v := range lang.Funcs {
|
|
funcs[k] = v
|
|
}
|
|
if formatter == nil {
|
|
formatter = lang.Formatter
|
|
}
|
|
if importter == nil {
|
|
importter = lang.Importter
|
|
}
|
|
target.ExtName = lang.ExtName
|
|
}
|
|
if !strings.HasPrefix(target.ExtName, ".") {
|
|
target.ExtName = "." + target.ExtName
|
|
}
|
|
|
|
var tableMapper, colMapper core.IMapper
|
|
switch target.TableMapper {
|
|
case "gonic":
|
|
tableMapper = core.LintGonicMapper
|
|
case "same":
|
|
tableMapper = core.SameMapper{}
|
|
default:
|
|
tableMapper = core.SnakeMapper{}
|
|
}
|
|
switch target.ColumnMapper {
|
|
case "gonic":
|
|
colMapper = core.LintGonicMapper
|
|
case "same":
|
|
colMapper = core.SameMapper{}
|
|
default:
|
|
colMapper = core.SnakeMapper{}
|
|
}
|
|
funcs["TableMapper"] = tableMapper.Table2Obj
|
|
funcs["ColumnMapper"] = colMapper.Table2Obj
|
|
|
|
if bs == nil {
|
|
return errors.New("You have to indicate template / template path or a language")
|
|
}
|
|
|
|
t := template.New("reverse")
|
|
t.Funcs(funcs)
|
|
|
|
tmpl, err := t.Parse(string(bs))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, table := range tables {
|
|
if target.TablePrefix != "" {
|
|
table.Name = strings.TrimPrefix(table.Name, target.TablePrefix)
|
|
}
|
|
for _, col := range table.Columns() {
|
|
col.FieldName = colMapper.Table2Obj(col.Name)
|
|
}
|
|
}
|
|
|
|
err = os.MkdirAll(target.OutputDir, os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var w *os.File
|
|
if !target.MultipleFiles {
|
|
w, err = os.Create(filepath.Join(target.OutputDir, "models"+target.ExtName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer w.Close()
|
|
|
|
imports := importter(tables)
|
|
|
|
newbytes := bytes.NewBufferString("")
|
|
err = tmpl.Execute(newbytes, map[string]interface{}{
|
|
"Tables": tables,
|
|
"Imports": imports,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tplcontent, err := ioutil.ReadAll(newbytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var source string
|
|
if formatter != nil {
|
|
source, err = formatter(string(tplcontent))
|
|
if err != nil {
|
|
log.Warnf("%v", err)
|
|
source = string(tplcontent)
|
|
}
|
|
} else {
|
|
source = string(tplcontent)
|
|
}
|
|
|
|
w.WriteString(source)
|
|
w.Close()
|
|
} else {
|
|
for _, table := range tables {
|
|
// imports
|
|
tbs := []*core.Table{table}
|
|
imports := importter(tbs)
|
|
|
|
w, err := os.Create(filepath.Join(target.OutputDir, table.Name+target.ExtName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer w.Close()
|
|
|
|
newbytes := bytes.NewBufferString("")
|
|
err = tmpl.Execute(newbytes, map[string]interface{}{
|
|
"Tables": tbs,
|
|
"Imports": imports,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tplcontent, err := ioutil.ReadAll(newbytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var source string
|
|
if formatter != nil {
|
|
source, err = formatter(string(tplcontent))
|
|
if err != nil {
|
|
log.Warnf("%v", err)
|
|
source = string(tplcontent)
|
|
}
|
|
} else {
|
|
source = string(tplcontent)
|
|
}
|
|
|
|
w.WriteString(source)
|
|
w.Close()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|