github.com/Ali-iotechsys/sqlboiler/v4@v4.0.0-20221208124957-6aec9a5f1f71/boilingcore/output.go (about) 1 package boilingcore 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "go/format" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strconv" 12 "strings" 13 "text/template" 14 15 "github.com/friendsofgo/errors" 16 "github.com/volatiletech/sqlboiler/v4/importers" 17 ) 18 19 // Copied from the go source 20 // see: https://github.com/golang/go/blob/master/src/go/build/syslist.go 21 var ( 22 goosList = stringSliceToMap(strings.Fields("aix android darwin dragonfly freebsd hurd illumos ios js linux nacl netbsd openbsd plan9 solaris windows zos")) 23 24 goarchList = stringSliceToMap(strings.Fields("386 amd64 amd64p32 arm armbe arm64 arm64be loong64 mips mipsle mips64 mips64le mips64p32 mips64p32le ppc ppc64 ppc64le riscv riscv64 s390 s390x sparc sparc64 wasm")) 25 ) 26 27 var ( 28 noEditDisclaimerFmt = `// Code generated by SQLBoiler%s(https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 29 // This file is meant to be re-generated in place and/or deleted at any time. 30 31 ` 32 noEditDisclaimer = []byte(fmt.Sprintf(noEditDisclaimerFmt, " ")) 33 ) 34 35 var ( 36 // templateByteBuffer is re-used by all template construction to avoid 37 // allocating more memory than is needed. This will later be a problem for 38 // concurrency, address it then. 39 templateByteBuffer = &bytes.Buffer{} 40 41 rgxRemoveNumberedPrefix = regexp.MustCompile(`^[0-9]+_`) 42 rgxSyntaxError = regexp.MustCompile(`(\d+):\d+: `) 43 44 testHarnessWriteFile = os.WriteFile 45 ) 46 47 type executeTemplateData struct { 48 state *State 49 data *templateData 50 51 templates *templateList 52 dirExtensions dirExtMap 53 54 importSet importers.Set 55 importNamedSet importers.Map 56 57 combineImportsOnType bool 58 isTest bool 59 } 60 61 // generateOutput builds the file output and sends it to outHandler for saving 62 func generateOutput(state *State, dirExts dirExtMap, data *templateData) error { 63 return executeTemplates(executeTemplateData{ 64 state: state, 65 data: data, 66 templates: state.Templates, 67 importSet: state.Config.Imports.All, 68 combineImportsOnType: true, 69 dirExtensions: dirExts, 70 }) 71 } 72 73 // generateTestOutput builds the test file output and sends it to outHandler for saving 74 func generateTestOutput(state *State, dirExts dirExtMap, data *templateData) error { 75 return executeTemplates(executeTemplateData{ 76 state: state, 77 data: data, 78 templates: state.TestTemplates, 79 importSet: state.Config.Imports.Test, 80 combineImportsOnType: false, 81 isTest: true, 82 dirExtensions: dirExts, 83 }) 84 } 85 86 // generateSingletonOutput processes the templates that should only be run 87 // one time. 88 func generateSingletonOutput(state *State, data *templateData) error { 89 return executeSingletonTemplates(executeTemplateData{ 90 state: state, 91 data: data, 92 templates: state.Templates, 93 importNamedSet: state.Config.Imports.Singleton, 94 }) 95 } 96 97 // generateSingletonTestOutput processes the templates that should only be run 98 // one time. 99 func generateSingletonTestOutput(state *State, data *templateData) error { 100 return executeSingletonTemplates(executeTemplateData{ 101 state: state, 102 data: data, 103 templates: state.TestTemplates, 104 importNamedSet: state.Config.Imports.TestSingleton, 105 isTest: true, 106 }) 107 } 108 109 func executeTemplates(e executeTemplateData) error { 110 if e.data.Table.IsJoinTable { 111 return nil 112 } 113 114 var imps importers.Set 115 imps.Standard = e.importSet.Standard 116 imps.ThirdParty = e.importSet.ThirdParty 117 if e.combineImportsOnType { 118 colTypes := make([]string, len(e.data.Table.Columns)) 119 for i, ct := range e.data.Table.Columns { 120 colTypes[i] = ct.Type 121 } 122 123 imps = importers.AddTypeImports(imps, e.state.Config.Imports.BasedOnType, colTypes) 124 } 125 126 for dir, dirExts := range e.dirExtensions { 127 for ext, tplNames := range dirExts { 128 out := templateByteBuffer 129 out.Reset() 130 131 isGo := filepath.Ext(ext) == ".go" 132 if isGo { 133 pkgName := e.state.Config.PkgName 134 if len(dir) != 0 { 135 pkgName = filepath.Base(dir) 136 } 137 writeFileDisclaimer(out) 138 writePackageName(out, pkgName) 139 writeImports(out, imps) 140 } 141 142 prevLen := out.Len() 143 for _, tplName := range tplNames { 144 if err := executeTemplate(out, e.templates.Template, tplName, e.data); err != nil { 145 return err 146 } 147 } 148 149 fName := getOutputFilename(e.data.Table.Name, e.isTest, isGo) 150 fName += ext 151 if len(dir) != 0 { 152 fName = filepath.Join(dir, fName) 153 } 154 155 // Skip writing the file if the content is empty 156 if out.Len()-prevLen < 1 { 157 fmt.Fprintf(os.Stderr, "skipping empty file: %s/%s\n", e.state.Config.OutFolder, fName) 158 continue 159 } 160 161 if err := writeFile(e.state.Config.OutFolder, fName, out, isGo); err != nil { 162 return err 163 } 164 } 165 } 166 167 return nil 168 } 169 170 func executeSingletonTemplates(e executeTemplateData) error { 171 if e.data.Table.IsJoinTable { 172 return nil 173 } 174 175 out := templateByteBuffer 176 for _, tplName := range e.templates.Templates() { 177 normalized, isSingleton, isGo, usePkg := outputFilenameParts(tplName) 178 if !isSingleton { 179 continue 180 } 181 182 dir, fName := filepath.Split(normalized) 183 fName = fName[:strings.IndexByte(fName, '.')] 184 185 out.Reset() 186 187 if isGo { 188 imps := importers.Set{ 189 Standard: e.importNamedSet[denormalizeSlashes(fName)].Standard, 190 ThirdParty: e.importNamedSet[denormalizeSlashes(fName)].ThirdParty, 191 } 192 193 pkgName := e.state.Config.PkgName 194 if !usePkg { 195 pkgName = filepath.Base(dir) 196 } 197 writeFileDisclaimer(out) 198 writePackageName(out, pkgName) 199 writeImports(out, imps) 200 } 201 202 if err := executeTemplate(out, e.templates.Template, tplName, e.data); err != nil { 203 return err 204 } 205 206 if err := writeFile(e.state.Config.OutFolder, normalized, out, isGo); err != nil { 207 return err 208 } 209 } 210 211 return nil 212 } 213 214 // writeFileDisclaimer writes the disclaimer at the top with a trailing 215 // newline so the package name doesn't get attached to it. 216 func writeFileDisclaimer(out *bytes.Buffer) { 217 _, _ = out.Write(noEditDisclaimer) 218 } 219 220 // writePackageName writes the package name correctly, ignores errors 221 // since it's to the concrete buffer type which produces none 222 func writePackageName(out *bytes.Buffer, pkgName string) { 223 _, _ = fmt.Fprintf(out, "package %s\n\n", pkgName) 224 } 225 226 // writeImports writes the package imports correctly, ignores errors 227 // since it's to the concrete buffer type which produces none 228 func writeImports(out *bytes.Buffer, imps importers.Set) { 229 if impStr := imps.Format(); len(impStr) > 0 { 230 _, _ = fmt.Fprintf(out, "%s\n", impStr) 231 } 232 } 233 234 // writeFile writes to the given folder and filename, formatting the buffer 235 // given. 236 func writeFile(outFolder string, fileName string, input *bytes.Buffer, format bool) error { 237 var byt []byte 238 var err error 239 if format { 240 byt, err = formatBuffer(input) 241 if err != nil { 242 return err 243 } 244 } else { 245 byt = input.Bytes() 246 } 247 248 path := filepath.Join(outFolder, fileName) 249 if err := testHarnessWriteFile(path, byt, 0664); err != nil { 250 return errors.Wrapf(err, "failed to write output file %s", path) 251 } 252 253 return nil 254 } 255 256 // executeTemplate takes a template and returns the output of the template 257 // execution. 258 func executeTemplate(buf *bytes.Buffer, t *template.Template, name string, data *templateData) (err error) { 259 defer func() { 260 if r := recover(); r != nil { 261 err = errors.Errorf("failed to execute template: %s\npanic: %+v\n", name, r) 262 } 263 }() 264 265 if err := t.ExecuteTemplate(buf, name, data); err != nil { 266 return errors.Wrapf(err, "failed to execute template: %s", name) 267 } 268 return nil 269 } 270 271 func formatBuffer(buf *bytes.Buffer) ([]byte, error) { 272 output, err := format.Source(buf.Bytes()) 273 if err == nil { 274 return output, nil 275 } 276 277 matches := rgxSyntaxError.FindStringSubmatch(err.Error()) 278 if matches == nil { 279 return nil, errors.Wrap(err, "failed to format template") 280 } 281 282 lineNum, _ := strconv.Atoi(matches[1]) 283 scanner := bufio.NewScanner(buf) 284 errBuf := &bytes.Buffer{} 285 line := 1 286 for ; scanner.Scan(); line++ { 287 if delta := line - lineNum; delta < -5 || delta > 5 { 288 continue 289 } 290 291 if line == lineNum { 292 errBuf.WriteString(">>>> ") 293 } else { 294 fmt.Fprintf(errBuf, "% 4d ", line) 295 } 296 errBuf.Write(scanner.Bytes()) 297 errBuf.WriteByte('\n') 298 } 299 300 return nil, errors.Wrapf(err, "failed to format template\n\n%s\n", errBuf.Bytes()) 301 } 302 303 func getLongExt(filename string) string { 304 index := strings.IndexByte(filename, '.') 305 return filename[index:] 306 } 307 308 func getOutputFilename(tableName string, isTest, isGo bool) string { 309 if strings.HasPrefix(tableName, "_") { 310 tableName = "und" + tableName 311 } 312 313 if isGo && endsWithSpecialSuffix(tableName) { 314 tableName += "_model" 315 } 316 317 if isTest { 318 tableName += "_test" 319 } 320 321 return tableName 322 } 323 324 // See: https://pkg.go.dev/cmd/go#hdr-Build_constraints 325 func endsWithSpecialSuffix(tableName string) bool { 326 parts := strings.Split(tableName, "_") 327 328 // Not enough parts to have a special suffix 329 if len(parts) < 2 { 330 return false 331 } 332 333 lastPart := parts[len(parts)-1] 334 335 if lastPart == "test" { 336 return true 337 } 338 339 if _, ok := goosList[lastPart]; ok { 340 return true 341 } 342 343 if _, ok := goarchList[lastPart]; ok { 344 return true 345 } 346 347 return false 348 } 349 350 func stringSliceToMap(slice []string) map[string]struct{} { 351 Map := make(map[string]struct{}, len(slice)) 352 for _, v := range slice { 353 Map[v] = struct{}{} 354 } 355 356 return Map 357 } 358 359 // fileFragments will take something of the form: 360 // templates/singleton/hello.go.tpl 361 // templates_test/js/hello.js.tpl 362 func outputFilenameParts(filename string) (normalized string, isSingleton, isGo, usePkg bool) { 363 fragments := strings.Split(filename, string(os.PathSeparator)) 364 isSingleton = fragments[len(fragments)-2] == "singleton" 365 366 var remainingFragments []string 367 for _, f := range fragments[1:] { 368 if f != "singleton" { 369 remainingFragments = append(remainingFragments, f) 370 } 371 } 372 373 newFilename := remainingFragments[len(remainingFragments)-1] 374 newFilename = strings.TrimSuffix(newFilename, ".tpl") 375 newFilename = rgxRemoveNumberedPrefix.ReplaceAllString(newFilename, "") 376 remainingFragments[len(remainingFragments)-1] = newFilename 377 378 ext := filepath.Ext(newFilename) 379 isGo = ext == ".go" 380 381 usePkg = len(remainingFragments) == 1 382 normalized = strings.Join(remainingFragments, string(os.PathSeparator)) 383 384 return normalized, isSingleton, isGo, usePkg 385 }