commit
312f37f563
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
19 changed files with 1181 additions and 0 deletions
-
9.drone.yml
-
1.gitignore
-
27LICENSE
-
83README.md
-
83README_CN.md
-
328cmd/reverse.go
-
33cmd/reverse_test.go
-
49cmd/root.go
-
35example/custom.yml
-
14example/goxorm.yml
-
16example/template/go.tmpl
-
16example/template/goxorm.tmpl
-
17go.mod
-
176go.sum
-
233language/golang.go
-
35language/language.go
-
17main.go
-
9models/models.go
-
BINtestdata/test.db
@ -0,0 +1,9 @@ |
|||||
|
kind: pipeline |
||||
|
name: default |
||||
|
|
||||
|
steps: |
||||
|
- name: test |
||||
|
image: golang:1.12 |
||||
|
commands: |
||||
|
- go build |
||||
|
- go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... |
@ -0,0 +1 @@ |
|||||
|
reverse |
@ -0,0 +1,27 @@ |
|||||
|
Copyright (c) 2019 The Xorm Authors |
||||
|
All rights reserved. |
||||
|
|
||||
|
Redistribution and use in source and binary forms, with or without |
||||
|
modification, are permitted provided that the following conditions are met: |
||||
|
|
||||
|
* Redistributions of source code must retain the above copyright notice, this |
||||
|
list of conditions and the following disclaimer. |
||||
|
|
||||
|
* Redistributions in binary form must reproduce the above copyright notice, |
||||
|
this list of conditions and the following disclaimer in the documentation |
||||
|
and/or other materials provided with the distribution. |
||||
|
|
||||
|
* Neither the name of the {organization} nor the names of its |
||||
|
contributors may be used to endorse or promote products derived from |
||||
|
this software without specific prior written permission. |
||||
|
|
||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,83 @@ |
|||||
|
# Reverse |
||||
|
|
||||
|
A flexsible and powerful command line tool to convert database to codes. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
``` |
||||
|
go get xorm.io/reverse |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
``` |
||||
|
reverse -f example/custom.yml |
||||
|
``` |
||||
|
|
||||
|
## Configuration File |
||||
|
|
||||
|
How does the simplest configuration file look like? |
||||
|
|
||||
|
```yml |
||||
|
kind: reverse |
||||
|
name: mydb |
||||
|
source: |
||||
|
database: sqlite3 |
||||
|
conn_str: '../testdata/test.db' |
||||
|
targets: |
||||
|
- type: codes |
||||
|
language: golang |
||||
|
output_dir: ../models |
||||
|
``` |
||||
|
|
||||
|
A `language` defines some default configuration items, also you can define all yourselves. |
||||
|
|
||||
|
```yml |
||||
|
kind: reverse |
||||
|
name: mydb |
||||
|
source: |
||||
|
database: sqlite |
||||
|
conn_str: ../testdata/test.db |
||||
|
targets: |
||||
|
- type: codes |
||||
|
include_tables: # tables included, you can use ** |
||||
|
- a |
||||
|
- b |
||||
|
exclude_tables: # tables excluded, you can use ** |
||||
|
- c |
||||
|
table_mapper: snake # how table name map to class or struct name |
||||
|
column_mapper: snake # how column name map to class or struct field name |
||||
|
table_prefix: "" # table prefix |
||||
|
multiple_files: true # generate multiple files or one |
||||
|
template: | # template for code file, it has higher perior than template_path |
||||
|
package models |
||||
|
|
||||
|
{{$ilen := len .Imports}} |
||||
|
{{if gt $ilen 0}} |
||||
|
import ( |
||||
|
{{range .Imports}}"{{.}}"{{end}} |
||||
|
) |
||||
|
{{end}} |
||||
|
|
||||
|
{{range .Tables}} |
||||
|
type {{TableMapper .Name}} struct { |
||||
|
{{$table := .}} |
||||
|
{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}} {{Type $col}} `{{Tag $table $col}}` |
||||
|
{{end}} |
||||
|
} |
||||
|
{{end}} |
||||
|
template_path: ./template/goxorm.tmpl # template path for code file, it has higher perior than template field on language |
||||
|
output_dir: ./models # code output directory |
||||
|
``` |
||||
|
|
||||
|
## Template Funcs |
||||
|
|
||||
|
- *UnTitle*: Convert first charator of the word to lower. |
||||
|
- *Upper*: Convert word to all upper. |
||||
|
- *TableMapper*: Mapper method to convert table name to class/struct name. |
||||
|
- *ColumnMapper*: Mapper method to convert column name to class/struct field name. |
||||
|
|
||||
|
## Template Vars |
||||
|
|
||||
|
- *Tables*: All tables. |
||||
|
- *Imports*: All imports needed. |
@ -0,0 +1,83 @@ |
|||||
|
# Reverse |
||||
|
|
||||
|
一个灵活高效的数据库反转工具。 |
||||
|
|
||||
|
## 安装 |
||||
|
|
||||
|
``` |
||||
|
go get xorm.io/reverse |
||||
|
``` |
||||
|
|
||||
|
## 使用 |
||||
|
|
||||
|
``` |
||||
|
reverse -f example/custom.yml |
||||
|
``` |
||||
|
|
||||
|
## 配置文件 |
||||
|
|
||||
|
一个最简单的配置文件看起来如下: |
||||
|
|
||||
|
```yml |
||||
|
kind: reverse |
||||
|
name: mydb |
||||
|
source: |
||||
|
database: sqlite3 |
||||
|
conn_str: '../testdata/test.db' |
||||
|
targets: |
||||
|
- type: codes |
||||
|
language: golang |
||||
|
output_dir: ../models |
||||
|
``` |
||||
|
|
||||
|
`language` 定义了很多默认的配置,你也可以自己来进行配置。其中的模板是 Go 模板语法。 |
||||
|
|
||||
|
```yml |
||||
|
kind: reverse |
||||
|
name: mydb |
||||
|
source: |
||||
|
database: sqlite |
||||
|
conn_str: ../testdata/test.db |
||||
|
targets: |
||||
|
- type: codes |
||||
|
include_tables: # 包含的表,以下可以用 ** |
||||
|
- a |
||||
|
- b |
||||
|
exclude_tables: # 排除的表,以下可以用 ** |
||||
|
- c |
||||
|
table_mapper: snake # 表名到代码类或结构体的映射关系 |
||||
|
column_mapper: snake # 字段名到代码或结构体成员的映射关系 |
||||
|
table_prefix: "" # 表前缀 |
||||
|
multiple_files: true # 是否生成多个文件 |
||||
|
template: | # 生成模板,如果这里定义了,优先级比 template_path 高 |
||||
|
package models |
||||
|
|
||||
|
{{$ilen := len .Imports}} |
||||
|
{{if gt $ilen 0}} |
||||
|
import ( |
||||
|
{{range .Imports}}"{{.}}"{{end}} |
||||
|
) |
||||
|
{{end}} |
||||
|
|
||||
|
{{range .Tables}} |
||||
|
type {{TableMapper .Name}} struct { |
||||
|
{{$table := .}} |
||||
|
{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}} {{Type $col}} `{{Tag $table $col}}` |
||||
|
{{end}} |
||||
|
} |
||||
|
{{end}} |
||||
|
template_path: ./template/goxorm.tmpl # 生成的模板的路径,优先级比 template 低,但比 language 中的默认模板高 |
||||
|
output_dir: ./models # 代码生成目录 |
||||
|
``` |
||||
|
|
||||
|
## 模板函数 |
||||
|
|
||||
|
- *UnTitle*: 将单词的第一个字母大写。 |
||||
|
- *Upper*: 将单词转为全部大写。 |
||||
|
- *TableMapper*: 将表名转为结构体名的映射函数。 |
||||
|
- *ColumnMapper*: 将字段名转为结构体成员名的函数。 |
||||
|
|
||||
|
## 模板变量 |
||||
|
|
||||
|
- *Tables*: 所有表。 |
||||
|
- *Imports*: 所有需要的导入。 |
@ -0,0 +1,328 @@ |
|||||
|
// 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 |
||||
|
} |
@ -0,0 +1,33 @@ |
|||||
|
// 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 ( |
||||
|
"fmt" |
||||
|
"io/ioutil" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/stretchr/testify/assert" |
||||
|
) |
||||
|
|
||||
|
var result = fmt.Sprintf(`package models |
||||
|
|
||||
|
type A struct { |
||||
|
Id int %sxorm:"integer"%s |
||||
|
} |
||||
|
|
||||
|
type B struct { |
||||
|
Id int %sxorm:"INTEGER"%s |
||||
|
} |
||||
|
`, "`", "`", "`", "`") |
||||
|
|
||||
|
func TestReverse(t *testing.T) { |
||||
|
err := reverse("../example/goxorm.yml") |
||||
|
assert.NoError(t, err) |
||||
|
|
||||
|
bs, err := ioutil.ReadFile("../models/models.go") |
||||
|
assert.NoError(t, err) |
||||
|
assert.EqualValues(t, result, string(bs)) |
||||
|
} |
@ -0,0 +1,49 @@ |
|||||
|
// 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 ( |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/spf13/cobra" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
Version = "0.1+dev" |
||||
|
|
||||
|
reverseFile string |
||||
|
versionFlag *bool |
||||
|
|
||||
|
rootCmd = &cobra.Command{ |
||||
|
Version: Version, |
||||
|
Use: "reverse", |
||||
|
Short: "Reverse is a database reverse command line tool", |
||||
|
Long: `A flexsible and powerful command line tool to generate codes/docs from databases(SQLITE/Mysql/Postgres/MSSQL)`, |
||||
|
Run: func(cmd *cobra.Command, args []string) { |
||||
|
if versionFlag != nil && *versionFlag { |
||||
|
fmt.Printf("Reverse %s\n", Version) |
||||
|
return |
||||
|
} |
||||
|
if reverseFile == "" { |
||||
|
fmt.Println("Need reverse file") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
err := reverse(reverseFile) |
||||
|
if err != nil { |
||||
|
fmt.Println(err) |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
versionFlag = rootCmd.Flags().BoolP("version", "v", false, "version of the tool") |
||||
|
rootCmd.Flags().StringVarP(&reverseFile, "file", "f", "", "yml file to apply for reverse") |
||||
|
} |
||||
|
|
||||
|
func Execute() error { |
||||
|
return rootCmd.Execute() |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
kind: reverse |
||||
|
name: mydb |
||||
|
source: |
||||
|
database: sqlite |
||||
|
conn_str: ../testdata/test.db |
||||
|
targets: |
||||
|
- type: codes |
||||
|
include_tables: |
||||
|
- a |
||||
|
- b |
||||
|
exclude_tables: |
||||
|
- c |
||||
|
table_mapper: snake |
||||
|
column_mapper: snake |
||||
|
table_prefix: "" |
||||
|
multiple_files: true |
||||
|
template: | |
||||
|
package models |
||||
|
|
||||
|
{{$ilen := len .Imports}} |
||||
|
{{if gt $ilen 0}} |
||||
|
import ( |
||||
|
{{range .Imports}}"{{.}}"{{end}} |
||||
|
) |
||||
|
{{end}} |
||||
|
|
||||
|
{{range .Tables}} |
||||
|
type {{TableMapper .Name}} struct { |
||||
|
{{$table := .}} |
||||
|
{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}} {{Type $col}} `{{Tag $table $col}}` |
||||
|
{{end}} |
||||
|
} |
||||
|
{{end}} |
||||
|
template_path: ./template/goxorm.tmpl |
||||
|
output_dir: ./models |
@ -0,0 +1,14 @@ |
|||||
|
kind: reverse |
||||
|
name: mydb |
||||
|
source: |
||||
|
database: sqlite3 |
||||
|
conn_str: '../testdata/test.db' |
||||
|
targets: |
||||
|
- type: codes |
||||
|
include_tables: |
||||
|
- a |
||||
|
- b |
||||
|
exclude_tables: |
||||
|
- c |
||||
|
language: golang |
||||
|
output_dir: ../models |
@ -0,0 +1,16 @@ |
|||||
|
package models |
||||
|
|
||||
|
{{$ilen := len .Imports}} |
||||
|
{{if gt $ilen 0}} |
||||
|
import ( |
||||
|
{{range .Imports}}"{{.}}"{{end}} |
||||
|
) |
||||
|
{{end}} |
||||
|
|
||||
|
{{range .Tables}} |
||||
|
type {{TableMapper .Name}} struct { |
||||
|
{{$table := .}} |
||||
|
{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}} {{Type $col}} |
||||
|
{{end}} |
||||
|
} |
||||
|
{{end}} |
@ -0,0 +1,16 @@ |
|||||
|
package models |
||||
|
|
||||
|
{{$ilen := len .Imports}} |
||||
|
{{if gt $ilen 0}} |
||||
|
import ( |
||||
|
{{range .Imports}}"{{.}}"{{end}} |
||||
|
) |
||||
|
{{end}} |
||||
|
|
||||
|
{{range .Tables}} |
||||
|
type {{TableMapper .Name}} struct { |
||||
|
{{$table := .}} |
||||
|
{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}} {{Type $col}} `{{Tag $table $col}}` |
||||
|
{{end}} |
||||
|
} |
||||
|
{{end}} |
@ -0,0 +1,17 @@ |
|||||
|
module xorm.io/reverse |
||||
|
|
||||
|
go 1.13 |
||||
|
|
||||
|
require ( |
||||
|
gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e |
||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4 |
||||
|
github.com/go-sql-driver/mysql v1.4.1 |
||||
|
github.com/gobwas/glob v0.2.3 |
||||
|
github.com/lib/pq v1.0.0 |
||||
|
github.com/mattn/go-sqlite3 v1.10.0 |
||||
|
github.com/spf13/cobra v0.0.5 |
||||
|
github.com/stretchr/testify v1.4.0 |
||||
|
gopkg.in/yaml.v2 v2.2.2 |
||||
|
xorm.io/core v0.7.2 |
||||
|
xorm.io/xorm v0.8.1 |
||||
|
) |
@ -0,0 +1,176 @@ |
|||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= |
||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= |
||||
|
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= |
||||
|
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= |
||||
|
gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e h1:r1en/D7xJmcY24VkHkjkcJFa+7ZWubVWPBrvsHkmHxk= |
||||
|
gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e/go.mod h1:uJEsN4LQpeGYRCjuPXPZBClU7N5pWzGuyF4uqLpE/e0= |
||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
||||
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= |
||||
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= |
||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= |
||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= |
||||
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= |
||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= |
||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= |
||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= |
||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= |
||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= |
||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= |
||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= |
||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4 h1:YcpmyvADGYw5LqMnHqSkyIELsHCGF6PkrmM31V8rF7o= |
||||
|
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= |
||||
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= |
||||
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= |
||||
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= |
||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
||||
|
github.com/go-flutter-desktop/hover v0.37.0 h1:sO6M+HpegV3u2ihKtlyF4Xlc1xR54iUdqC2LIMs0sWc= |
||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= |
||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= |
||||
|
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= |
||||
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= |
||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= |
||||
|
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= |
||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= |
||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= |
||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= |
||||
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= |
||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= |
||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= |
||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= |
||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= |
||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= |
||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= |
||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= |
||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= |
||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= |
||||
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= |
||||
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= |
||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= |
||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= |
||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= |
||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= |
||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= |
||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= |
||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= |
||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= |
||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= |
||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= |
||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
||||
|
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= |
||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= |
||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= |
||||
|
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= |
||||
|
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= |
||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= |
||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= |
||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= |
||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= |
||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
||||
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
||||
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= |
||||
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= |
||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= |
||||
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= |
||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= |
||||
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= |
||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= |
||||
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= |
||||
|
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= |
||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= |
||||
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= |
||||
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= |
||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= |
||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= |
||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= |
||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= |
||||
|
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= |
||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= |
||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= |
||||
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= |
||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= |
||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= |
||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= |
||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= |
||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= |
||||
|
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= |
||||
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= |
||||
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= |
||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= |
||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= |
||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= |
||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= |
||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= |
||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= |
||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= |
||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= |
||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= |
||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= |
||||
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= |
||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
||||
|
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= |
||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= |
||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= |
||||
|
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= |
||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= |
||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= |
||||
|
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= |
||||
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= |
||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= |
||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= |
||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= |
||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= |
||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |
||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |
||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= |
||||
|
xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= |
||||
|
xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= |
||||
|
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw= |
||||
|
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= |
||||
|
xorm.io/xorm v0.8.1 h1:4f2KXuQxVdaX3RdI3Fw81NzMiSpZeyCZt8m3sEVeIkQ= |
||||
|
xorm.io/xorm v0.8.1/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= |
@ -0,0 +1,233 @@ |
|||||
|
// 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 language |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"go/format" |
||||
|
"html/template" |
||||
|
"reflect" |
||||
|
"sort" |
||||
|
"strings" |
||||
|
|
||||
|
"xorm.io/core" |
||||
|
) |
||||
|
|
||||
|
// Golang represents a golang language
|
||||
|
var Golang = Language{ |
||||
|
Name: "golang", |
||||
|
Template: defaultGolangTemplate, |
||||
|
Types: map[string]string{}, |
||||
|
Funcs: template.FuncMap{ |
||||
|
"Type": typestring, |
||||
|
"Tag": tag, |
||||
|
}, |
||||
|
Formatter: formatGo, |
||||
|
Importter: genGoImports, |
||||
|
ExtName: ".go", |
||||
|
} |
||||
|
|
||||
|
func init() { |
||||
|
RegisterLanguage(&Golang) |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
errBadComparisonType = errors.New("invalid type for comparison") |
||||
|
errBadComparison = errors.New("incompatible types for comparison") |
||||
|
errNoComparison = errors.New("missing argument for comparison") |
||||
|
defaultGolangTemplate = fmt.Sprintf(`package models |
||||
|
|
||||
|
{{$ilen := len .Imports}}{{if gt $ilen 0}}import ( |
||||
|
{{range .Imports}}"{{.}}"{{end}} |
||||
|
){{end}} |
||||
|
|
||||
|
{{range .Tables}} |
||||
|
type {{TableMapper .Name}} struct { |
||||
|
{{$table := .}}{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}} {{Type $col}} %s{{Tag $table $col}}%s |
||||
|
{{end}} |
||||
|
} |
||||
|
{{end}} |
||||
|
`, "`", "`") |
||||
|
) |
||||
|
|
||||
|
type kind int |
||||
|
|
||||
|
const ( |
||||
|
invalidKind kind = iota |
||||
|
boolKind |
||||
|
complexKind |
||||
|
intKind |
||||
|
floatKind |
||||
|
integerKind |
||||
|
stringKind |
||||
|
uintKind |
||||
|
) |
||||
|
|
||||
|
func basicKind(v reflect.Value) (kind, error) { |
||||
|
switch v.Kind() { |
||||
|
case reflect.Bool: |
||||
|
return boolKind, nil |
||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
|
return intKind, nil |
||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
||||
|
return uintKind, nil |
||||
|
case reflect.Float32, reflect.Float64: |
||||
|
return floatKind, nil |
||||
|
case reflect.Complex64, reflect.Complex128: |
||||
|
return complexKind, nil |
||||
|
case reflect.String: |
||||
|
return stringKind, nil |
||||
|
} |
||||
|
return invalidKind, errBadComparisonType |
||||
|
} |
||||
|
|
||||
|
func getCol(cols map[string]*core.Column, name string) *core.Column { |
||||
|
return cols[strings.ToLower(name)] |
||||
|
} |
||||
|
|
||||
|
func formatGo(src string) (string, error) { |
||||
|
source, err := format.Source([]byte(src)) |
||||
|
if err != nil { |
||||
|
return "", err |
||||
|
} |
||||
|
return string(source), nil |
||||
|
} |
||||
|
|
||||
|
func genGoImports(tables []*core.Table) []string { |
||||
|
imports := make(map[string]string) |
||||
|
results := make([]string, 0) |
||||
|
for _, table := range tables { |
||||
|
for _, col := range table.Columns() { |
||||
|
if typestring(col) == "time.Time" { |
||||
|
if _, ok := imports["time"]; !ok { |
||||
|
imports["time"] = "time" |
||||
|
results = append(results, "time") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return results |
||||
|
} |
||||
|
|
||||
|
func typestring(col *core.Column) string { |
||||
|
st := col.SQLType |
||||
|
t := core.SQLType2Type(st) |
||||
|
s := t.String() |
||||
|
if s == "[]uint8" { |
||||
|
return "[]byte" |
||||
|
} |
||||
|
return s |
||||
|
} |
||||
|
|
||||
|
func tag(table *core.Table, col *core.Column) template.HTML { |
||||
|
isNameId := col.FieldName == "Id" |
||||
|
isIdPk := isNameId && typestring(col) == "int64" |
||||
|
|
||||
|
var res []string |
||||
|
if !col.Nullable { |
||||
|
if !isIdPk { |
||||
|
res = append(res, "not null") |
||||
|
} |
||||
|
} |
||||
|
if col.IsPrimaryKey { |
||||
|
res = append(res, "pk") |
||||
|
} |
||||
|
if col.Default != "" { |
||||
|
res = append(res, "default "+col.Default) |
||||
|
} |
||||
|
if col.IsAutoIncrement { |
||||
|
res = append(res, "autoincr") |
||||
|
} |
||||
|
|
||||
|
/*if col.SQLType.IsTime() && include(created, col.Name) { |
||||
|
res = append(res, "created") |
||||
|
} |
||||
|
|
||||
|
if col.SQLType.IsTime() && include(updated, col.Name) { |
||||
|
res = append(res, "updated") |
||||
|
} |
||||
|
|
||||
|
if col.SQLType.IsTime() && include(deleted, col.Name) { |
||||
|
res = append(res, "deleted") |
||||
|
}*/ |
||||
|
|
||||
|
if /*supportComment &&*/ col.Comment != "" { |
||||
|
res = append(res, fmt.Sprintf("comment('%s')", col.Comment)) |
||||
|
} |
||||
|
|
||||
|
names := make([]string, 0, len(col.Indexes)) |
||||
|
for name := range col.Indexes { |
||||
|
names = append(names, name) |
||||
|
} |
||||
|
sort.Strings(names) |
||||
|
|
||||
|
for _, name := range names { |
||||
|
index := table.Indexes[name] |
||||
|
var uistr string |
||||
|
if index.Type == core.UniqueType { |
||||
|
uistr = "unique" |
||||
|
} else if index.Type == core.IndexType { |
||||
|
uistr = "index" |
||||
|
} |
||||
|
if len(index.Cols) > 1 { |
||||
|
uistr += "(" + index.Name + ")" |
||||
|
} |
||||
|
res = append(res, uistr) |
||||
|
} |
||||
|
|
||||
|
nstr := col.SQLType.Name |
||||
|
if col.Length != 0 { |
||||
|
if col.Length2 != 0 { |
||||
|
nstr += fmt.Sprintf("(%v,%v)", col.Length, col.Length2) |
||||
|
} else { |
||||
|
nstr += fmt.Sprintf("(%v)", col.Length) |
||||
|
} |
||||
|
} else if len(col.EnumOptions) > 0 { //enum
|
||||
|
nstr += "(" |
||||
|
opts := "" |
||||
|
|
||||
|
enumOptions := make([]string, 0, len(col.EnumOptions)) |
||||
|
for enumOption := range col.EnumOptions { |
||||
|
enumOptions = append(enumOptions, enumOption) |
||||
|
} |
||||
|
sort.Strings(enumOptions) |
||||
|
|
||||
|
for _, v := range enumOptions { |
||||
|
opts += fmt.Sprintf(",'%v'", v) |
||||
|
} |
||||
|
nstr += strings.TrimLeft(opts, ",") |
||||
|
nstr += ")" |
||||
|
} else if len(col.SetOptions) > 0 { //enum
|
||||
|
nstr += "(" |
||||
|
opts := "" |
||||
|
|
||||
|
setOptions := make([]string, 0, len(col.SetOptions)) |
||||
|
for setOption := range col.SetOptions { |
||||
|
setOptions = append(setOptions, setOption) |
||||
|
} |
||||
|
sort.Strings(setOptions) |
||||
|
|
||||
|
for _, v := range setOptions { |
||||
|
opts += fmt.Sprintf(",'%v'", v) |
||||
|
} |
||||
|
nstr += strings.TrimLeft(opts, ",") |
||||
|
nstr += ")" |
||||
|
} |
||||
|
res = append(res, nstr) |
||||
|
if len(res) > 0 { |
||||
|
return template.HTML(fmt.Sprintf(`xorm:"%s"`, strings.Join(res, " "))) |
||||
|
} |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
func include(source []string, target string) bool { |
||||
|
for _, s := range source { |
||||
|
if s == target { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
// 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 language |
||||
|
|
||||
|
import ( |
||||
|
"html/template" |
||||
|
|
||||
|
"xorm.io/core" |
||||
|
) |
||||
|
|
||||
|
// Language represents a languages supported when reverse codes
|
||||
|
type Language struct { |
||||
|
Name string |
||||
|
Template string |
||||
|
Types map[string]string |
||||
|
Funcs template.FuncMap |
||||
|
Formatter func(string) (string, error) |
||||
|
Importter func([]*core.Table) []string |
||||
|
ExtName string |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
languages = make(map[string]*Language) |
||||
|
) |
||||
|
|
||||
|
func RegisterLanguage(l *Language) { |
||||
|
languages[l.Name] = l |
||||
|
} |
||||
|
|
||||
|
// GetLanguage returns a language if exists
|
||||
|
func GetLanguage(name string) *Language { |
||||
|
return languages[name] |
||||
|
} |
@ -0,0 +1,17 @@ |
|||||
|
// 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 main |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
|
||||
|
"xorm.io/reverse/cmd" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
if err := cmd.Execute(); err != nil { |
||||
|
fmt.Println(err) |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
package models |
||||
|
|
||||
|
type A struct { |
||||
|
Id int `xorm:"integer"` |
||||
|
} |
||||
|
|
||||
|
type B struct { |
||||
|
Id int `xorm:"INTEGER"` |
||||
|
} |
Binary file not shown.
Write
Preview
Loading…
Cancel
Save
Reference in new issue