sqlboiler/boilingcore/boilingcore.go

394 lines
10 KiB
Go
Raw Permalink Normal View History

2017-01-14 07:13:32 +01:00
// Package boilingcore has types and methods useful for generating code that
// acts as a fully dynamic ORM might.
package boilingcore
import (
2016-09-04 12:27:19 +02:00
"encoding/json"
"fmt"
"go/build"
"os"
"path/filepath"
2016-09-04 15:44:54 +02:00
"regexp"
"strings"
"text/template"
"github.com/pkg/errors"
2017-07-31 05:34:54 +02:00
"github.com/volatiletech/sqlboiler/bdb"
"github.com/volatiletech/sqlboiler/bdb/drivers"
"github.com/volatiletech/sqlboiler/queries"
"github.com/volatiletech/sqlboiler/strmangle"
)
const (
templatesDirectory = "templates"
templatesSingletonDirectory = "templates/singleton"
templatesTestDirectory = "templates_test"
templatesSingletonTestDirectory = "templates_test/singleton"
templatesTestMainDirectory = "templates_test/main_test"
)
// State holds the global data needed by most pieces to run
type State struct {
Config *Config
2016-09-09 19:14:18 +02:00
Driver bdb.Interface
Tables []bdb.Table
Dialect queries.Dialect
Templates *templateList
TestTemplates *templateList
SingletonTemplates *templateList
SingletonTestTemplates *templateList
TestMainTemplate *template.Template
2017-02-05 11:17:11 +01:00
Importer importer
}
// New creates a new state based off of the config
func New(config *Config) (*State, error) {
s := &State{
Config: config,
}
err := s.initDriver(config.DriverName)
if err != nil {
return nil, err
}
// Connect to the driver database
if err = s.Driver.Open(); err != nil {
return nil, errors.Wrap(err, "unable to connect to the database")
}
2016-09-09 07:41:57 +02:00
err = s.initTables(config.Schema, config.WhitelistTables, config.BlacklistTables)
if err != nil {
return nil, errors.Wrap(err, "unable to initialize tables")
}
2016-09-04 12:27:19 +02:00
if s.Config.Debug {
b, err := json.Marshal(s.Tables)
if err != nil {
return nil, errors.Wrap(err, "unable to json marshal tables")
}
2016-09-11 18:22:20 +02:00
fmt.Printf("%s\n", b)
2016-09-04 12:27:19 +02:00
}
err = s.initOutFolder()
if err != nil {
return nil, errors.Wrap(err, "unable to initialize the output folder")
}
err = s.initTemplates()
if err != nil {
return nil, errors.Wrap(err, "unable to initialize templates")
}
2016-09-04 15:44:54 +02:00
err = s.initTags(config.Tags)
if err != nil {
return nil, errors.Wrap(err, "unable to initialize struct tags")
}
2017-02-05 11:17:11 +01:00
s.Importer = newImporter()
return s, nil
}
// Run executes the sqlboiler templates and outputs them to files based on the
// state given.
func (s *State) Run(includeTests bool) error {
2016-06-12 09:32:46 +02:00
singletonData := &templateData{
Tables: s.Tables,
Schema: s.Config.Schema,
DriverName: s.Config.DriverName,
UseLastInsertID: s.Driver.UseLastInsertID(),
PkgName: s.Config.PkgName,
NoHooks: s.Config.NoHooks,
NoAutoTimestamps: s.Config.NoAutoTimestamps,
2016-09-09 19:14:18 +02:00
Dialect: s.Dialect,
LQ: strmangle.QuoteCharacter(s.Dialect.LQ),
RQ: strmangle.QuoteCharacter(s.Dialect.RQ),
StringFuncs: templateStringMappers,
2016-06-12 09:32:46 +02:00
}
if err := generateSingletonOutput(s, singletonData); err != nil {
return errors.Wrap(err, "singleton template output")
}
if !s.Config.NoTests && includeTests {
2016-06-12 09:32:46 +02:00
if err := generateTestMainOutput(s, singletonData); err != nil {
return errors.Wrap(err, "unable to generate TestMain output")
}
2016-06-12 09:32:46 +02:00
if err := generateSingletonTestOutput(s, singletonData); err != nil {
return errors.Wrap(err, "unable to generate singleton test template output")
}
}
for _, table := range s.Tables {
if table.IsJoinTable {
continue
}
data := &templateData{
Tables: s.Tables,
Table: table,
Schema: s.Config.Schema,
DriverName: s.Config.DriverName,
UseLastInsertID: s.Driver.UseLastInsertID(),
PkgName: s.Config.PkgName,
NoHooks: s.Config.NoHooks,
NoAutoTimestamps: s.Config.NoAutoTimestamps,
2016-09-04 15:44:54 +02:00
Tags: s.Config.Tags,
2016-09-09 19:14:18 +02:00
Dialect: s.Dialect,
LQ: strmangle.QuoteCharacter(s.Dialect.LQ),
RQ: strmangle.QuoteCharacter(s.Dialect.RQ),
StringFuncs: templateStringMappers,
}
// Generate the regular templates
if err := generateOutput(s, data); err != nil {
return errors.Wrap(err, "unable to generate output")
}
// Generate the test templates
if !s.Config.NoTests && includeTests {
if err := generateTestOutput(s, data); err != nil {
return errors.Wrap(err, "unable to generate test output")
}
}
}
return nil
}
// Cleanup closes any resources that must be closed
func (s *State) Cleanup() error {
s.Driver.Close()
return nil
}
// initTemplates loads all template folders into the state object.
func (s *State) initTemplates() error {
var err error
basePath, err := getBasePath(s.Config.BaseDir)
if err != nil {
return err
}
s.Templates, err = loadTemplates(filepath.Join(basePath, templatesDirectory))
if err != nil {
return err
}
s.SingletonTemplates, err = loadTemplates(filepath.Join(basePath, templatesSingletonDirectory))
if err != nil {
return err
}
if !s.Config.NoTests {
s.TestTemplates, err = loadTemplates(filepath.Join(basePath, templatesTestDirectory))
if err != nil {
return err
}
s.SingletonTestTemplates, err = loadTemplates(filepath.Join(basePath, templatesSingletonTestDirectory))
if err != nil {
return err
}
2017-02-04 17:24:20 +01:00
s.TestMainTemplate, err = loadTemplate(filepath.Join(basePath, templatesTestMainDirectory), s.Config.DriverName+"_main.tpl")
if err != nil {
return err
}
}
return s.processReplacements()
}
// processReplacements loads any replacement templates
func (s *State) processReplacements() error {
basePath, err := getBasePath(s.Config.BaseDir)
if err != nil {
return err
}
for _, replace := range s.Config.Replacements {
splits := strings.Split(replace, ":")
if len(splits) != 2 {
return errors.Errorf("replace parameters must have 2 arguments, given: %s", replace)
}
var toReplaceFname string
toReplace, replaceWith := splits[0], splits[1]
inf, err := os.Stat(filepath.Join(basePath, toReplace))
if err != nil {
return errors.Errorf("cannot stat %q", toReplace)
}
if inf.IsDir() {
return errors.Errorf("replace argument must be a path to a file not a dir: %q", toReplace)
}
toReplaceFname = inf.Name()
inf, err = os.Stat(replaceWith)
if err != nil {
return errors.Errorf("cannot stat %q", replaceWith)
}
if inf.IsDir() {
return errors.Errorf("replace argument must be a path to a file not a dir: %q", replaceWith)
}
2017-02-04 17:24:20 +01:00
switch filepath.Dir(toReplace) {
case templatesDirectory:
err = replaceTemplate(s.Templates.Template, toReplaceFname, replaceWith)
case templatesSingletonDirectory:
err = replaceTemplate(s.SingletonTemplates.Template, toReplaceFname, replaceWith)
case templatesTestDirectory:
err = replaceTemplate(s.TestTemplates.Template, toReplaceFname, replaceWith)
case templatesSingletonTestDirectory:
err = replaceTemplate(s.SingletonTestTemplates.Template, toReplaceFname, replaceWith)
case templatesTestMainDirectory:
err = replaceTemplate(s.TestMainTemplate, toReplaceFname, replaceWith)
default:
return errors.Errorf("replace file's directory not part of any known folder: %s", toReplace)
}
if err != nil {
return err
}
}
return nil
}
2017-07-31 05:34:54 +02:00
var basePackage = "github.com/volatiletech/sqlboiler"
func getBasePath(baseDirConfig string) (string, error) {
if len(baseDirConfig) > 0 {
return baseDirConfig, nil
}
p, _ := build.Default.Import(basePackage, "", build.FindOnly)
if p != nil && len(p.Dir) > 0 {
return p.Dir, nil
}
return os.Getwd()
}
// initDriver attempts to set the state Interface based off the passed in
// driver flag value. If an invalid flag string is provided an error is returned.
func (s *State) initDriver(driverName string) error {
// Create a driver based off driver flag
switch driverName {
case "postgres":
s.Driver = drivers.NewPostgresDriver(
s.Config.Postgres.User,
s.Config.Postgres.Pass,
s.Config.Postgres.DBName,
s.Config.Postgres.Host,
s.Config.Postgres.Port,
2016-07-12 00:17:49 +02:00
s.Config.Postgres.SSLMode,
)
2016-09-09 08:04:58 +02:00
case "mysql":
s.Driver = drivers.NewMySQLDriver(
s.Config.MySQL.User,
s.Config.MySQL.Pass,
s.Config.MySQL.DBName,
s.Config.MySQL.Host,
s.Config.MySQL.Port,
s.Config.MySQL.SSLMode,
)
2017-03-13 10:55:26 +01:00
case "mssql":
s.Driver = drivers.NewMSSQLDriver(
s.Config.MSSQL.User,
s.Config.MSSQL.Pass,
s.Config.MSSQL.DBName,
s.Config.MSSQL.Host,
s.Config.MSSQL.Port,
s.Config.MSSQL.SSLMode,
)
case "mock":
s.Driver = &drivers.MockDriver{}
}
if s.Driver == nil {
return errors.New("An invalid driver name was provided")
}
2016-09-09 19:14:18 +02:00
s.Dialect.LQ = s.Driver.LeftQuote()
s.Dialect.RQ = s.Driver.RightQuote()
s.Dialect.IndexPlaceholders = s.Driver.IndexPlaceholders()
s.Dialect.UseTopClause = s.Driver.UseTopClause()
2016-09-09 19:14:18 +02:00
return nil
}
2016-08-15 14:42:40 +02:00
// initTables retrieves all "public" schema table names from the database.
2016-09-09 07:41:57 +02:00
func (s *State) initTables(schema string, whitelist, blacklist []string) error {
var err error
2016-09-09 07:41:57 +02:00
s.Tables, err = bdb.Tables(s.Driver, schema, whitelist, blacklist)
if err != nil {
return errors.Wrap(err, "unable to fetch table data")
}
if len(s.Tables) == 0 {
return errors.New("no tables found in database")
}
if err := checkPKeys(s.Tables); err != nil {
return err
}
return nil
}
2016-09-04 15:44:54 +02:00
// Tags must be in a format like: json, xml, etc.
var rgxValidTag = regexp.MustCompile(`[a-zA-Z_\.]+`)
// initTags removes duplicate tags and validates the format
// of all user tags are simple strings without quotes: [a-zA-Z_\.]+
func (s *State) initTags(tags []string) error {
s.Config.Tags = removeDuplicates(s.Config.Tags)
for _, v := range s.Config.Tags {
if !rgxValidTag.MatchString(v) {
return errors.New("Invalid tag format %q supplied, only specify name, eg: xml")
}
}
return nil
}
// initOutFolder creates the folder that will hold the generated output.
func (s *State) initOutFolder() error {
if s.Config.Wipe {
if err := os.RemoveAll(s.Config.OutFolder); err != nil {
return err
}
}
return os.MkdirAll(s.Config.OutFolder, os.ModePerm)
}
// checkPKeys ensures every table has a primary key column
func checkPKeys(tables []bdb.Table) error {
var missingPkey []string
for _, t := range tables {
if t.PKey == nil {
missingPkey = append(missingPkey, t.Name)
}
}
if len(missingPkey) != 0 {
return errors.Errorf("primary key missing in tables (%s)", strings.Join(missingPkey, ", "))
}
return nil
}