Skip to content

Commit

Permalink
feat: Customize direction name in migration files
Browse files Browse the repository at this point in the history
Export allowed directional labels for use in validations and
usage text for the create-migration subcommand.
  • Loading branch information
rafaelespinoza committed Jan 8, 2022
1 parent 21e26df commit a4ac74b
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 138 deletions.
15 changes: 8 additions & 7 deletions direction.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ const (
)

func (d Direction) String() string {
return [...]string{"Unknown", "Forward", "Reverse"}[d]
return [...]string{"unknown", "forward", "reverse"}[d]
}

// Allowed names for directions in migration filenames.
var (
forwardDirections = []string{
strings.ToLower(DirForward.String()),
ForwardDirections = []string{
"forward",
"migrate",
"up",
}
reverseDirections = []string{
strings.ToLower(DirReverse.String()),
ReverseDirections = []string{
"reverse",
"rollback",
"down",
}
Expand All @@ -42,14 +43,14 @@ type Indirection struct {

func parseIndirection(basename string) (ind Indirection) {
lo := strings.ToLower(basename)
for _, pre := range forwardDirections {
for _, pre := range ForwardDirections {
if strings.HasPrefix(lo, pre) {
ind.Value = DirForward
ind.Label = pre
return
}
}
for _, pre := range reverseDirections {
for _, pre := range ReverseDirections {
if strings.HasPrefix(lo, pre) {
ind.Value = DirReverse
ind.Label = pre
Expand Down
12 changes: 7 additions & 5 deletions godfish.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ func figureOutBasename(directoryPath string, direction Direction, version string

var directionNames []string
if direction == DirForward {
directionNames = forwardDirections
directionNames = ForwardDirections
} else if direction == DirReverse {
directionNames = reverseDirections
directionNames = ReverseDirections
}

for _, fn := range filenames {
Expand Down Expand Up @@ -228,7 +228,9 @@ func Info(driver Driver, directoryPath string, direction Direction, finishAtVers

// Config is for various runtime settings.
type Config struct {
PathToFiles string `json:"path_to_files"`
PathToFiles string `json:"path_to_files"`
ForwardLabel string `json:"forward_label"`
ReverseLabel string `json:"reverse_label"`
}

// Init creates a configuration file at pathToFile unless it already exists.
Expand Down Expand Up @@ -433,13 +435,13 @@ func (m *migrationFinder) filter(applied, available []Migration) (out []Migratio
label: mig.Label(),
version: mig.Version(),
}
for i, fwd := range forwardDirections {
for i, fwd := range ForwardDirections {
// Another assumption, the filename format will never
// change. If it does change, for example: it is
// "${version}-${direction}-${label}", instead of
// "${direction}-${version}-${label}", then this won't work.
if mig.Indirection().Label == fwd {
mut.indirection.Label = reverseDirections[i]
mut.indirection.Label = ReverseDirections[i]
break
}
}
Expand Down
17 changes: 12 additions & 5 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import (
)

var (
// commonArgs are flag inputs for use in any subcommand.
// commonArgs values are read from a configuration file, if available. The
// subcommand code is written so that flag values may take precedence over
// values in here.
commonArgs struct {
Conf string
Files string
Files string
DefaultFwdLabel, DefaultRevLabel string
}
// bin is the name of the binary.
bin = os.Args[0]
Expand Down Expand Up @@ -87,7 +89,8 @@ Examples:
strings.Join(del.DescribeSubcommands(), "\n\t"), bin)
}

rootFlags.StringVar(&commonArgs.Conf, "conf", ".godfish.json", "path to godfish config file")
var pathToConfig string
rootFlags.StringVar(&pathToConfig, "conf", ".godfish.json", "path to godfish config file")
rootFlags.StringVar(
&commonArgs.Files,
"files",
Expand All @@ -102,7 +105,7 @@ Examples:
// Look for config file and if present, merge those values with
// input flag values.
var conf godfish.Config
if data, ierr := os.ReadFile(commonArgs.Conf); ierr != nil {
if data, ierr := os.ReadFile(pathToConfig); ierr != nil {
// probably no config file present, rely on arguments instead.
} else if ierr = json.Unmarshal(data, &conf); ierr != nil {
return ierr
Expand All @@ -111,6 +114,10 @@ Examples:
commonArgs.Files = conf.PathToFiles
}

// Subcommands may override these with their own flags.
commonArgs.DefaultFwdLabel = conf.ForwardLabel
commonArgs.DefaultRevLabel = conf.ReverseLabel

return nil
},
}
Expand Down
84 changes: 77 additions & 7 deletions internal/cmd/create-migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ import (
"context"
"flag"
"fmt"
"strings"

"github.com/rafaelespinoza/alf"
"github.com/rafaelespinoza/godfish"
)

func makeCreateMigration(name string) alf.Directive {
var label string
func makeCreateMigration(subcmdName string) alf.Directive {
const fwdlabelFlagname, revlabelFlagname = "fwdlabel", "revlabel"
var migrationName, fwdlabelValue, revlabelValue string
var reversible bool

// Other subcommands scope the flagset within the Setup func. However, this
// one is scoped up here to check if some flags were specified at runtime.
flags := flag.NewFlagSet(subcmdName, flag.ExitOnError)

return &alf.Command{
Description: "generate migration files",
Setup: func(p flag.FlagSet) *flag.FlagSet {
flags := flag.NewFlagSet(name, flag.ExitOnError)
flags.StringVar(
&label,
&migrationName,
"name",
"",
"label the migration, ie: create_foos_table, update_bars_qux",
Expand All @@ -29,28 +34,93 @@ func makeCreateMigration(name string) alf.Directive {
true,
"create a reversible migration?",
)
flags.StringVar(
&fwdlabelValue,
fwdlabelFlagname,
godfish.ForwardDirections[0],
"customize the directional part of the filename for forward migration",
)
flags.StringVar(
&revlabelValue,
revlabelFlagname,
godfish.ReverseDirections[0],
"customize the directional part of the filename for reverse migration",
)
flags.Usage = func() {
fmt.Printf(`Usage: %s [godfish-flags] %s [%s-flags]
Generate migration files: one meant for the "forward" direction,
another meant for "reverse". Optionally create a migration in the forward
direction only by passing the flag "-reversible=false". The "name" flag has
no effects other than on the generated filename. The output filename
automatically has a "version". Timestamp format: %s.
automatically has a "version". Timestamp layout: %s.
Acceptable values for the %q and %q flags are:
- %s
- %s
`,
bin, name, name, godfish.TimeFormat,
bin, subcmdName, subcmdName, godfish.TimeFormat,
fwdlabelFlagname, revlabelFlagname,
strings.Join(godfish.ForwardDirections, ", "), strings.Join(godfish.ReverseDirections, ", "),
)
printFlagDefaults(&p)
printFlagDefaults(flags)
}
return flags
},
Run: func(_ context.Context) error {
migration, err := godfish.NewMigrationParams(label, reversible, commonArgs.Files)
// Allow this subcommand's flags to override names for directional
// part of the filename. But allow for the configuration file to
// have a say if the flag wasn't passed in at runtime.
var passedFwd, passedRev bool
flags.Visit(func(f *flag.Flag) {
switch f.Name {
case fwdlabelFlagname:
passedFwd = true
case revlabelFlagname:
passedRev = true
default:
break
}
})
if !passedFwd && commonArgs.DefaultFwdLabel != "" {
fwdlabelValue = commonArgs.DefaultFwdLabel
}
if !passedRev && commonArgs.DefaultRevLabel != "" {
revlabelValue = commonArgs.DefaultRevLabel
}

// Should also consider values from the config file, so validate
// after allowing it the opportunity to set variable values.
if err := validateDirectionLabel(godfish.ForwardDirections, fwdlabelFlagname, fwdlabelValue); err != nil {
return err
}
if err := validateDirectionLabel(godfish.ReverseDirections, revlabelFlagname, revlabelValue); err != nil {
return err
}

migration, err := godfish.NewMigrationParams(migrationName, reversible, commonArgs.Files, fwdlabelValue, revlabelValue)
if err != nil {
return err
}
return migration.GenerateFiles()
},
}
}

func validateDirectionLabel(okVals []string, flagName, flagVal string) (err error) {
var ok bool
for _, okVal := range okVals {
if flagVal == okVal {
ok = true
break
}
}
if !ok {
err = fmt.Errorf(
"invalid value (%q) for flag %q; should be one of: %s",
flagVal, flagName, strings.Join(okVals, ", "),
)
}
return
}
2 changes: 1 addition & 1 deletion internal/info/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func mustMakeMigrations(names ...string) []godfish.Migration {
out := make([]godfish.Migration, len(names))

for i := 0; i < len(names); i++ {
params, err := godfish.NewMigrationParams(names[i], false, dir)
params, err := godfish.NewMigrationParams(names[i], false, dir, "", "")
if err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func generateMigrationFiles(pathToTestDir string, stubs []testDriverStub) error
panic(fmt.Errorf("test setup should have content in forward direction"))
}

if params, err = godfish.NewMigrationParams(strconv.Itoa(i), reversible, pathToTestDir); err != nil {
if params, err = godfish.NewMigrationParams(strconv.Itoa(i), reversible, pathToTestDir, "", ""); err != nil {
return err
}

Expand Down
22 changes: 15 additions & 7 deletions migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ type MigrationParams struct {
// NewMigrationParams constructs a MigrationParams that's ready to use. Passing
// in true for reversible means that a complementary SQL file will be made for
// rolling back. The dirpath is the path to the directory for the files. An
// error is returned if dirpath doesn't actually represent a directory.
func NewMigrationParams(label string, reversible bool, dirpath string) (*MigrationParams, error) {
// error is returned if dirpath doesn't actually represent a directory. Names
// for directions in the filename could be overridden from their default values
// (forward and reverse) with the input vars fwdLabel, revLabel when non-empty.
func NewMigrationParams(name string, reversible bool, dirpath, fwdLabel, revLabel string) (*MigrationParams, error) {
var (
err error
info os.FileInfo
Expand All @@ -100,25 +102,31 @@ func NewMigrationParams(label string, reversible bool, dirpath string) (*Migrati
now = time.Now().UTC()
version = timestamp{value: now.Unix(), label: now.Format(TimeFormat)}

if fwdLabel == "" {
fwdLabel = ForwardDirections[0]
}
if revLabel == "" {
revLabel = ReverseDirections[0]
}
return &MigrationParams{
Reversible: reversible,
Dirpath: dirpath,
Forward: &mutation{
indirection: Indirection{Value: DirForward, Label: forwardDirections[0]},
label: label,
indirection: Indirection{Value: DirForward, Label: fwdLabel},
label: name,
version: &version,
},
Reverse: &mutation{
indirection: Indirection{Value: DirReverse, Label: reverseDirections[0]},
label: label,
indirection: Indirection{Value: DirReverse, Label: revLabel},
label: name,
version: &version,
},
directory: directory,
}, nil
}

// GenerateFiles creates the migration files. If the migration is reversible it
// generates files in forward and reverse directions; otherwise is generates
// generates files in forward and reverse directions; otherwise it generates
// just one migration file in the forward direction. It closes each file handle
// when it's done.
func (m *MigrationParams) GenerateFiles() (err error) {
Expand Down
Loading

0 comments on commit a4ac74b

Please sign in to comment.