Test Failed
Pull Request — master (#30)
by
unknown
01:36
created

internal/cli/tables-to-go-cli.go   F

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Test Coverage

Coverage 85.63%

Importance

Changes 0
Metric Value
cc 73
eloc 192
dl 0
loc 337
ccs 137
cts 160
cp 0.8563
crap 88.813
rs 2.56
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
C cli.generateImports 0 27 9
A cli.getNullType 0 5 2
A cli.columnInfo.hasTrue 0 2 4
F cli.mapDbColumnTypeToGoType 0 46 14
C cli.createTableStructString 0 80 11
A cli.replaceSpace 0 5 3
A cli.indexCaseInsensitive 0 3 1
F cli.Run 0 64 14
B cli.formatColumnName 0 31 7
A cli.toInitialisms 0 10 3
A cli.validVariableName 0 7 5
1
package cli
2
3
import (
4
	"fmt"
5
	"strings"
6
	"unicode"
7
8
	"github.com/iancoleman/strcase"
9
10
	"github.com/fraenky8/tables-to-go/pkg/database"
11
	"github.com/fraenky8/tables-to-go/pkg/output"
12
	"github.com/fraenky8/tables-to-go/pkg/settings"
13
	"github.com/fraenky8/tables-to-go/pkg/tagger"
14
)
15
16
var (
17
	taggers tagger.Tagger
18
19
	// some strings for idiomatic go in column names
20
	// see https://github.com/golang/go/wiki/CodeReviewComments#initialisms
21
	initialisms = []string{"ID", "JSON", "XML", "HTTP", "URL"}
22
)
23
24
// Run runs the transformations by creating the concrete Database by the provided settings
25 1
func Run(settings *settings.Settings, db database.Database, out output.Writer) (err error) {
26
27 1
	taggers = tagger.NewTaggers(settings)
28
29 1
	fmt.Printf("running for %q...\r\n", settings.DbType)
30 1
31
	tables, err := db.GetTables()
32
	if err != nil {
33
		return fmt.Errorf("could not get tables: %v", err)
34 1
	}
35
36
	if settings.Verbose {
37
		fmt.Printf("> number of tables: %v\r\n", len(tables))
38 1
	}
39
40
	if err = db.PrepareGetColumnsOfTableStmt(); err != nil {
41
		return fmt.Errorf("could not prepare the get-column-statement: %v", err)
42 1
	}
43
44 1
	for _, table := range tables {
45
46
		if settings.Verbose {
47
			fmt.Printf("> processing table %q\r\n", table.Name)
48 1
		}
49
50
		if err = db.GetColumnsOfTable(table); err != nil {
51
			if !settings.Force {
52
				return fmt.Errorf("could not get columns of table %q: %v", table.Name, err)
53
			}
54
			fmt.Printf("could not get columns of table %q: %v\n", table.Name, err)
55
			continue
56 1
		}
57
58
		if settings.Verbose {
59
			fmt.Printf("\t> number of columns: %v\r\n", len(table.Columns))
60 1
		}
61 1
62
		tableName, content, err := createTableStructString(settings, db, table)
63
64
		if err != nil {
65
			if !settings.Force {
66
				return fmt.Errorf("could not create string for table %q: %v", table.Name, err)
67
			}
68
			fmt.Printf("could not create string for table %q: %v\n", table.Name, err)
69 1
			continue
70 1
		}
71
72
		fileName := strcase.ToCamel(tableName)
73
		if settings.IsFileNameFormatSnakeCase() {
74
			fileName = strcase.ToSnake(fileName)
75
		}
76
77
		err = out.Write(fileName, content)
78 1
		if err != nil {
79
			if !settings.Force {
80 1
				return fmt.Errorf("could not write struct for table %q: %v", table.Name, err)
81
			}
82
			fmt.Printf("could not write struct for table %q: %v\n", table.Name, err)
83
		}
84
	}
85
86
	fmt.Println("done!")
87
88
	return nil
89
}
90
91 1
type columnInfo struct {
92
	isNullable          bool
93
	isTemporal          bool
94
	isNullablePrimitive bool
95
	isNullableTemporal  bool
96 1
}
97 1
98
func (c columnInfo) hasTrue() bool {
99 1
	return c.isNullable || c.isTemporal || c.isNullableTemporal || c.isNullablePrimitive
100 1
}
101 1
102
func createTableStructString(settings *settings.Settings, db database.Database, table *database.Table) (string, string, error) {
103
104
	var structFields strings.Builder
105 1
	tableName := strings.Title(settings.Prefix + table.Name + settings.Suffix)
106
	// Replace any whitespace with underscores
107
	tableName = strings.Map(replaceSpace, tableName)
108
	if settings.IsOutputFormatCamelCase() {
109 1
		tableName = strcase.ToCamel(tableName)
110 1
	}
111
112 1
	// Check that the table name doesn't contain any invalid characters for Go variables
113 1
	if !validVariableName(tableName) {
114 1
		return "", "", fmt.Errorf("table name %q contains invalid characters", table.Name)
115
	}
116
117
	columnInfo := columnInfo{}
118
	columns := map[string]struct{}{}
119
120
	for _, column := range table.Columns {
121
		columnName, err := formatColumnName(settings, column.Name, table.Name)
122 1
		if err != nil {
123
			return "", "", err
124
		}
125 1
126
		// ISSUE-4: if columns are part of multiple constraints
127 1
		// then the sql returns multiple rows per column name.
128
		// Therefore we check if we already added a column with
129
		// that name to the struct, if so, skip.
130
		if _, ok := columns[columnName]; ok {
131 1
			continue
132
		}
133
		columns[columnName] = struct{}{}
134 1
135 1
		if settings.VVerbose {
136
			fmt.Printf("\t\t> %v\r\n", column.Name)
137 1
		}
138 1
139
		columnType, col := mapDbColumnTypeToGoType(settings, db, column)
140 1
141 1
		// save that we saw types of columns at least once
142
		if !columnInfo.isTemporal {
143
			columnInfo.isTemporal = col.isTemporal
144 1
		}
145 1
		if !columnInfo.isNullableTemporal {
146 1
			columnInfo.isNullableTemporal = col.isNullableTemporal
147 1
		}
148 1
		if !columnInfo.isNullablePrimitive {
149 1
			columnInfo.isNullablePrimitive = col.isNullablePrimitive
150
		}
151
152 1
		structFields.WriteString(columnName)
153
		structFields.WriteString(" ")
154
		structFields.WriteString(columnType)
155
		structFields.WriteString(" ")
156 1
		structFields.WriteString(taggers.GenerateTag(db, column))
157
		structFields.WriteString("\n")
158
	}
159 1
160 1
	if settings.IsMastermindStructableRecorder {
161 1
		structFields.WriteString("\t\nstructable.Recorder\n")
162
	}
163
164 1
	var fileContent strings.Builder
165
166
	// write header infos
167 1
	fileContent.WriteString("package ")
168 1
	fileContent.WriteString(settings.PackageName)
169 1
	fileContent.WriteString("\n\n")
170 1
171 1
	// write imports
172
	generateImports(&fileContent, settings, db, columnInfo)
173 1
174
	// write struct with fields
175
	fileContent.WriteString("type ")
176
	fileContent.WriteString(tableName)
177
	fileContent.WriteString(" struct {\n")
178 1
	fileContent.WriteString(structFields.String())
179 1
	fileContent.WriteString("}")
180
181
	return tableName, fileContent.String(), nil
182 1
}
183
184 1
func generateImports(content *strings.Builder, settings *settings.Settings, db database.Database, columnInfo columnInfo) {
185 1
186
	if !columnInfo.hasTrue() && !settings.IsMastermindStructableRecorder {
187
		return
188 1
	}
189 1
190
	content.WriteString("import (\n")
191
192 1
	if columnInfo.isNullablePrimitive && settings.IsNullTypeSQL() {
193 1
		content.WriteString("\t\"database/sql\"\n")
194 1
	}
195 1
196
	if columnInfo.isTemporal {
197
		content.WriteString("\t\"time\"\n")
198 1
	}
199
200
	if columnInfo.isNullableTemporal && settings.IsNullTypeSQL() {
201
		content.WriteString("\t\n")
202 1
		content.WriteString(db.GetDriverImportLibrary())
203
		content.WriteString("\n")
204
	}
205
206 1
	if settings.IsMastermindStructableRecorder {
207 1
		content.WriteString("\t\n\"github.com/Masterminds/structable\"\n")
208 1
	}
209 1
210 1
	content.WriteString(")\n\n")
211
}
212 1
213 1
func mapDbColumnTypeToGoType(s *settings.Settings, db database.Database, column database.Column) (goType string, columnInfo columnInfo) {
214 1
	if db.IsString(column) || db.IsText(column) {
215 1
		goType = "string"
216 1
		if db.IsNullable(column) {
217
			goType = getNullType(s, "*string", "sql.NullString")
218 1
			columnInfo.isNullable = true
219 1
		}
220 1
	} else if db.IsInteger(column) {
221 1
		goType = "int"
222 1
		if db.IsNullable(column) {
223
			goType = getNullType(s, "*int", "sql.NullInt64")
224 1
			columnInfo.isNullable = true
225 1
		}
226 1
	} else if db.IsFloat(column) {
227 1
		goType = "float64"
228
		if db.IsNullable(column) {
229 1
			goType = getNullType(s, "*float64", "sql.NullFloat64")
230 1
			columnInfo.isNullable = true
231 1
		}
232 1
	} else if db.IsTemporal(column) {
233
		if !db.IsNullable(column) {
234
			goType = "time.Time"
235
			columnInfo.isTemporal = true
236 1
		} else {
237
			goType = getNullType(s, "*time.Time", db.GetTemporalDriverDataType())
238 1
			columnInfo.isTemporal = s.Null == settings.NullTypeNative
239 1
			columnInfo.isNullableTemporal = true
240 1
			columnInfo.isNullable = true
241 1
		}
242
	} else {
243
		// TODO handle special data types
244
		switch column.DataType {
245
		case "boolean":
246
			goType = "bool"
247
			if db.IsNullable(column) {
248 1
				goType = getNullType(s, "*bool", "sql.NullBool")
249
				columnInfo.isNullable = true
250 1
			}
251
		default:
252
			goType = getNullType(s, "*string", "sql.NullString")
253
		}
254 1
	}
255 1
256
	columnInfo.isNullablePrimitive = columnInfo.isNullable && !db.IsTemporal(column)
257 1
258
	return goType, columnInfo
259
}
260
261 1
func getNullType(settings *settings.Settings, primitive string, sql string) string {
262 1
	if settings.IsNullTypeSQL() {
263
		return sql
264
	}
265 1
	return primitive
266
}
267 1
268 1
func toInitialisms(s string) string {
269
	for _, substr := range initialisms {
270
		idx := indexCaseInsensitive(s, substr)
271 1
		if idx == -1 {
272 1
			continue
273 1
		}
274
		toReplace := s[idx : idx+len(substr)]
275 1
		s = strings.ReplaceAll(s, toReplace, substr)
276
	}
277
	return s
278
}
279 1
280 1
func indexCaseInsensitive(s, substr string) int {
281 1
	s, substr = strings.ToLower(s), strings.ToLower(substr)
282 1
	return strings.Index(s, substr)
283
}
284 1
285 1
// ValidVariableName checks for the existence of any characters
286
// outside of Unicode letters, numbers and underscore.
287 1
func validVariableName(s string) bool {
288
	for _, r := range s {
289
		if !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_') {
290
			return false
291 1
		}
292 1
	}
293
	return true
294
}
295
296
// ReplaceSpace swaps any Unicode space characters for underscores
297
// to create valid Go identifiers
298 1
func replaceSpace(r rune) rune {
299 1
	if unicode.IsSpace(r) || r == '\u200B' {
300 1
		return '_'
301
	}
302
	return r
303 1
}
304
305
// FormatColumnName checks for invalid characters and transforms a column name
306
// according to the provided settings.
307
func formatColumnName(settings *settings.Settings, column, table string) (string, error) {
308
309 1
	// Replace any whitespace with underscores
310 1
	columnName := strings.Map(replaceSpace, column)
311
	columnName = strings.Title(columnName)
312 1
313
	if settings.IsOutputFormatCamelCase() {
314
		columnName = strcase.ToCamel(columnName)
315
	}
316
	if settings.ShouldInitialism() {
317
		columnName = toInitialisms(columnName)
318
	}
319
320 1
	// Check that the column name doesn't contain any invalid characters for Go variables
321 1
	if !validVariableName(columnName) {
322
		return "", fmt.Errorf("column name %q in table %q contains invalid characters", column, table)
323 1
	}
324 1
	// First character of an identifier in Go must be letter or _
325
	// We want it to be an uppercase letter to be a public field
326 1
	if !unicode.IsLetter([]rune(columnName)[0]) {
327 1
		prefix := "X_"
328
		if settings.IsOutputFormatCamelCase() {
329
			prefix = "X"
330
		}
331 1
		if settings.Verbose {
332 1
			fmt.Printf("\t\t>column %q in table %q doesn't start with a letter; prepending with %q\n", column, table, prefix)
333
		}
334
		columnName = prefix + columnName
335
	}
336 1
337 1
	return columnName, nil
338
}
339