Passed
Pull Request — master (#17)
by Frank
06:11 queued 03:57
created

cli.createTableStructString   C

Complexity

Conditions 11

Size

Total Lines 78
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 11.0411

Importance

Changes 0
Metric Value
cc 11
eloc 44
nop 3
dl 0
loc 78
ccs 40
cts 43
cp 0.9302
crap 11.0411
rs 5.4
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.createTableStructString 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
7
	"github.com/fraenky8/tables-to-go/pkg/database"
8
	"github.com/fraenky8/tables-to-go/pkg/output"
9
	"github.com/fraenky8/tables-to-go/pkg/settings"
10
	"github.com/fraenky8/tables-to-go/pkg/tagger"
11
)
12
13
var (
14
	taggers tagger.Tagger
15
16
	// some strings for idiomatic go in column names
17
	// see https://github.com/golang/go/wiki/CodeReviewComments#initialisms
18
	initialisms = []string{"ID", "JSON", "XML", "HTTP", "URL"}
19
)
20
21
// Run runs the transformations by creating the concrete Database by the provided settings
22
func Run(settings *settings.Settings, db database.Database, out output.Writer) (err error) {
23
24 1
	taggers = tagger.NewTaggers(settings)
25
26 1
	fmt.Printf("running for %q...\r\n", settings.DbType)
27
28 1
	tables, err := db.GetTables()
29 1
	if err != nil {
30
		return fmt.Errorf("could not get tables: %v", err)
31
	}
32
33 1
	if settings.Verbose {
34
		fmt.Printf("> number of tables: %v\r\n", len(tables))
35
	}
36
37 1
	if err = db.PrepareGetColumnsOfTableStmt(); err != nil {
38
		return fmt.Errorf("could not prepare the get-column-statement: %v", err)
39
	}
40
41 1
	for _, table := range tables {
42
43 1
		if settings.Verbose {
44
			fmt.Printf("> processing table %q\r\n", table.Name)
45
		}
46
47 1
		if err = db.GetColumnsOfTable(table); err != nil {
48
			return fmt.Errorf("could not get columns of table %s: %v", table.Name, err)
49
		}
50
51 1
		if settings.Verbose {
52
			fmt.Printf("\t> number of columns: %v\r\n", len(table.Columns))
53
		}
54
55 1
		tableName, content := createTableStructString(settings, db, table)
56
57 1
		err = out.Write(tableName, content)
58 1
		if err != nil {
59
			return fmt.Errorf("could not write struct for table %s: %v", table.Name, err)
60
		}
61
	}
62
63 1
	fmt.Println("done!")
64
65 1
	return nil
66
}
67
68
type columnInfo struct {
69
	isNullable          bool
70
	isTemporal          bool
71
	isNullablePrimitive bool
72
	isNullableTemporal  bool
73
}
74
75
func (c columnInfo) hasTrue() bool {
76 1
	return c.isNullable || c.isTemporal || c.isNullableTemporal || c.isNullablePrimitive
77
}
78
79
func createTableStructString(settings *settings.Settings, db database.Database, table *database.Table) (string, string) {
80
81 1
	var structFields strings.Builder
82
83 1
	columnInfo := columnInfo{}
84 1
	columns := map[string]struct{}{}
85
86 1
	for _, column := range table.Columns {
87
88 1
		columnName := strings.Title(column.Name)
89 1
		if settings.IsOutputFormatCamelCase() {
90 1
			columnName = camelCaseString(column.Name)
91
		}
92 1
		if settings.ShouldInitialism() {
93 1
			columnName = toInitialisms(columnName)
94
		}
95
96
		// ISSUE-4: if columns are part of multiple constraints
97
		// then the sql returns multiple rows per column name.
98
		// Therefore we check if we already added a column with
99
		// that name to the struct, if so, skip.
100 1
		if _, ok := columns[columnName]; ok {
101
			continue
102
		}
103 1
		columns[columnName] = struct{}{}
104
105 1
		if settings.VVerbose {
106
			fmt.Printf("\t\t> %v\r\n", column.Name)
107
		}
108
109 1
		columnType, col := mapDbColumnTypeToGoType(settings, db, column)
110
111
		// save that we saw types of columns at least once
112 1
		if !columnInfo.isTemporal {
113 1
			columnInfo.isTemporal = col.isTemporal
114
		}
115 1
		if !columnInfo.isNullableTemporal {
116 1
			columnInfo.isNullableTemporal = col.isNullableTemporal
117
		}
118 1
		if !columnInfo.isNullablePrimitive {
119 1
			columnInfo.isNullablePrimitive = col.isNullablePrimitive
120
		}
121
122 1
		structFields.WriteString(columnName)
123 1
		structFields.WriteString(" ")
124 1
		structFields.WriteString(columnType)
125 1
		structFields.WriteString(" ")
126 1
		structFields.WriteString(taggers.GenerateTag(db, column))
127 1
		structFields.WriteString("\n")
128
	}
129
130 1
	if settings.IsMastermindStructableRecorder {
131
		structFields.WriteString("\t\nstructable.Recorder\n")
132
	}
133
134 1
	var fileContent strings.Builder
135
136
	// write header infos
137 1
	fileContent.WriteString("package ")
138 1
	fileContent.WriteString(settings.PackageName)
139 1
	fileContent.WriteString("\n\n")
140
141
	// write imports
142 1
	generateImports(&fileContent, settings, db, columnInfo)
143
144 1
	tableName := strings.Title(settings.Prefix + table.Name + settings.Suffix)
145 1
	if settings.IsOutputFormatCamelCase() {
146 1
		tableName = camelCaseString(tableName)
147
	}
148
149
	// write struct with fields
150 1
	fileContent.WriteString("type ")
151 1
	fileContent.WriteString(tableName)
152 1
	fileContent.WriteString(" struct {\n")
153 1
	fileContent.WriteString(structFields.String())
154 1
	fileContent.WriteString("}")
155
156 1
	return tableName, fileContent.String()
157
}
158
159
func generateImports(content *strings.Builder, settings *settings.Settings, db database.Database, columnInfo columnInfo) {
160
161 1
	if !columnInfo.hasTrue() && !settings.IsMastermindStructableRecorder {
162 1
		return
163
	}
164
165 1
	content.WriteString("import (\n")
166
167 1
	if columnInfo.isNullablePrimitive && settings.IsNullTypeSQL() {
168 1
		content.WriteString("\t\"database/sql\"\n")
169
	}
170
171 1
	if columnInfo.isTemporal {
172 1
		content.WriteString("\t\"time\"\n")
173
	}
174
175 1
	if columnInfo.isNullableTemporal && settings.IsNullTypeSQL() {
176 1
		content.WriteString("\t\n")
177 1
		content.WriteString(db.GetDriverImportLibrary())
178 1
		content.WriteString("\n")
179
	}
180
181 1
	if settings.IsMastermindStructableRecorder {
182
		content.WriteString("\t\n\"github.com/Masterminds/structable\"\n")
183
	}
184
185 1
	content.WriteString(")\n\n")
186
}
187
188
func mapDbColumnTypeToGoType(s *settings.Settings, db database.Database, column database.Column) (goType string, columnInfo columnInfo) {
189 1
	if db.IsString(column) || db.IsText(column) {
190 1
		goType = "string"
191 1
		if db.IsNullable(column) {
192 1
			goType = getNullType(s, "*string", "sql.NullString")
193 1
			columnInfo.isNullable = true
194
		}
195 1
	} else if db.IsInteger(column) {
196 1
		goType = "int"
197 1
		if db.IsNullable(column) {
198 1
			goType = getNullType(s, "*int", "sql.NullInt64")
199 1
			columnInfo.isNullable = true
200
		}
201 1
	} else if db.IsFloat(column) {
202 1
		goType = "float64"
203 1
		if db.IsNullable(column) {
204 1
			goType = getNullType(s, "*float64", "sql.NullFloat64")
205 1
			columnInfo.isNullable = true
206
		}
207 1
	} else if db.IsTemporal(column) {
208 1
		if !db.IsNullable(column) {
209 1
			goType = "time.Time"
210 1
			columnInfo.isTemporal = true
211
		} else {
212 1
			goType = getNullType(s, "*time.Time", db.GetTemporalDriverDataType())
213 1
			columnInfo.isTemporal = s.Null == settings.NullTypeNative
214 1
			columnInfo.isNullableTemporal = true
215 1
			columnInfo.isNullable = true
216
		}
217
	} else {
218
		// TODO handle special data types
219 1
		switch column.DataType {
220
		case "boolean":
221 1
			goType = "bool"
222 1
			if db.IsNullable(column) {
223 1
				goType = getNullType(s, "*bool", "sql.NullBool")
224 1
				columnInfo.isNullable = true
225
			}
226
		default:
227
			goType = getNullType(s, "*string", "sql.NullString")
228
		}
229
	}
230
231 1
	columnInfo.isNullablePrimitive = columnInfo.isNullable && !db.IsTemporal(column)
232
233 1
	return goType, columnInfo
234
}
235
236
func getNullType(settings *settings.Settings, primitive string, sql string) string {
237 1
	if settings.IsNullTypeSQL() {
238 1
		return sql
239
	}
240 1
	return primitive
241
}
242
243
func camelCaseString(s string) (cc string) {
244 1
	splitted := strings.Split(s, "_")
245
246 1
	if len(splitted) == 1 {
247
		return strings.Title(s)
248
	}
249
250 1
	for _, part := range splitted {
251 1
		cc += strings.Title(strings.ToLower(part))
252
	}
253 1
	return cc
254
}
255
256
func toInitialisms(s string) string {
257 1
	for _, substr := range initialisms {
258 1
		idx := indexCaseInsensitive(s, substr)
259 1
		if idx == -1 {
260 1
			continue
261
		}
262 1
		toReplace := s[idx : idx+len(substr)]
263 1
		s = strings.ReplaceAll(s, toReplace, substr)
264
	}
265 1
	return s
266
}
267
268
func indexCaseInsensitive(s, substr string) int {
269 1
	s, substr = strings.ToLower(s), strings.ToLower(substr)
270 1
	return strings.Index(s, substr)
271
}
272