Passed
Push — master ( c21070...5fc61a )
by Frank
02:16 queued 11s
created

cli.Run   D

Complexity

Conditions 13

Size

Total Lines 58
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 34.125

Importance

Changes 0
Metric Value
cc 13
eloc 33
nop 3
dl 0
loc 58
ccs 16
cts 32
cp 0.5
crap 34.125
rs 4.2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like cli.Run often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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