Skip to content

Commit

Permalink
Abort generation if file exists and matches glob (#91)
Browse files Browse the repository at this point in the history
* Abort generation if file exists and matches glob

resolves #90

With this, a user can have Gnorm generate a file once and prevent
further generation (thus preserving future edits). Or prevent
accidental overwrites of files during generation. There are, I'm sure,
other use cases I can't envision at this time.

* Log when skipping file

* Remove extraneous NoOverwriteGlobs
  • Loading branch information
Etomyutikos authored and natefinch committed Feb 12, 2018
1 parent 25eb64a commit d696814
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 24 deletions.
5 changes: 5 additions & 0 deletions cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,9 @@ type Config struct {
// The directory structure is preserved when copying the files to the
// OutputDir
StaticDir string

// NoOverwriteGlobs is a list of globs
// (https://golang.org/pkg/path/filepath/#Match). If a filename matches a glob
// *and* a file exists with that name, it will not be generated.
NoOverwriteGlobs []string
}
8 changes: 6 additions & 2 deletions cli/gnorm.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
ConnStr = "dbname=mydb host=127.0.0.1 sslmode=disable user=admin"

# DBType holds the type of db you're connecting to. Possible values are
# "postgres" or "mysql".
# "postgres" or "mysql".
DBType = "postgres"

# Schemas holds the names of schemas to generate code for.
Expand Down Expand Up @@ -63,6 +63,10 @@ OutputDir = "gnorm"
# OutputDir
StaticDir = "static"

# NoOverwriteGlobs is a list of globs
# (https://golang.org/pkg/path/filepath/#Match). If a filename matches a glob
# *and* a file exists with that name, it will not be generated.
NoOverwriteGlobs = ["*.perm.go"]

# TablePaths is a map of output paths to template paths that tells Gnorm how to
# render and output its table info and where to save that output. Each template
Expand Down Expand Up @@ -146,4 +150,4 @@ StaticDir = "static"
# different situations. The values in this field will be available in the
# .Params value for all templates.
[Params]
mySpecialValue = "some value"
mySpecialValue = "some value"
23 changes: 12 additions & 11 deletions cli/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,18 @@ func parse(env environ.Values, r io.Reader) (*run.Config, error) {

cfg := &run.Config{
ConfigData: data.ConfigData{
ConnStr: c.ConnStr,
DBType: c.DBType,
Schemas: c.Schemas,
NullableTypeMap: c.NullableTypeMap,
TypeMap: c.TypeMap,
PostRun: c.PostRun,
ExcludeTables: exclude,
IncludeTables: include,
OutputDir: c.OutputDir,
StaticDir: c.StaticDir,
PluginDirs: c.PluginDirs,
ConnStr: c.ConnStr,
DBType: c.DBType,
Schemas: c.Schemas,
NullableTypeMap: c.NullableTypeMap,
TypeMap: c.TypeMap,
PostRun: c.PostRun,
ExcludeTables: exclude,
IncludeTables: include,
OutputDir: c.OutputDir,
StaticDir: c.StaticDir,
PluginDirs: c.PluginDirs,
NoOverwriteGlobs: c.NoOverwriteGlobs,
},
Params: c.Params,
}
Expand Down
7 changes: 4 additions & 3 deletions cli/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ func TestParseConfig(t *testing.T) {
"integer": "sql.NullInt64",
"numeric": "sql.NullFloat64",
},
PluginDirs: []string{"plugins"},
OutputDir: "gnorm",
StaticDir: "static",
PluginDirs: []string{"plugins"},
OutputDir: "gnorm",
StaticDir: "static",
NoOverwriteGlobs: []string{"*.perm.go"},
}
if diff := cmp.Diff(cfg.ConfigData, expected); diff != "" {
t.Fatalf("Actual differs from expected:\n%s", diff)
Expand Down
9 changes: 7 additions & 2 deletions cli/sample.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const sample = `# ConnStr is the connection string for the database. Any enviro
ConnStr = "dbname=mydb host=127.0.0.1 sslmode=disable user=admin"
# DBType holds the type of db you're connecting to. Possible values are
# "postgres" or "mysql".
# "postgres" or "mysql".
DBType = "postgres"
# Schemas holds the names of schemas to generate code for.
Expand Down Expand Up @@ -89,6 +89,10 @@ OutputDir = "gnorm"
# OutputDir
StaticDir = "static"
# NoOverwriteGlobs is a list of globs
# (https://golang.org/pkg/path/filepath/#Match). If a filename matches a glob
# *and* a file exists with that name, it will not be generated.
NoOverwriteGlobs = ["*.perm.go"]
# TablePaths is a map of output paths to template paths that tells Gnorm how to
# render and output its table info and where to save that output. Each template
Expand Down Expand Up @@ -172,5 +176,6 @@ StaticDir = "static"
# different situations. The values in this field will be available in the
# .Params value for all templates.
[Params]
mySpecialValue = "some value"`
mySpecialValue = "some value"
`
// [[[end]]]
5 changes: 5 additions & 0 deletions run/data/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ type ConfigData struct {
// The directory structure is preserved when copying the files to the
// OutputDir
StaticDir string

// NoOverwriteGlobs is a list of globs
// (https://golang.org/pkg/path/filepath/#Match). If a filename matches a glob
// *and* a file exists with that name, it will not be generated.
NoOverwriteGlobs []string
}

// Strings is a named type of []string to allow us to put methods on it.
Expand Down
22 changes: 18 additions & 4 deletions run/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func generateSchemas(env environ.Values, cfg *Config, db *data.DBData) error {
}
for _, target := range cfg.SchemaPaths {
env.Log.Printf("Generating output for schema %v", schema.Name)
if err := genFile(env, fileData, contents, target, cfg.PostRun, cfg.OutputDir); err != nil {
if err := genFile(env, fileData, contents, target, cfg.NoOverwriteGlobs, cfg.PostRun, cfg.OutputDir); err != nil {
return errors.WithMessage(err, "generating file for schema "+schema.Name)
}
}
Expand All @@ -82,7 +82,7 @@ func generateEnums(env environ.Values, cfg *Config, db *data.DBData) error {
Params: cfg.Params,
}
for _, target := range cfg.EnumPaths {
if err := genFile(env, fileData, contents, target, cfg.PostRun, cfg.OutputDir); err != nil {
if err := genFile(env, fileData, contents, target, cfg.NoOverwriteGlobs, cfg.PostRun, cfg.OutputDir); err != nil {
env.Log.Printf("Generating output for enum %v", enum.Name)
return errors.WithMessage(err, "generating file for enum "+enum.Name)
}
Expand All @@ -103,7 +103,7 @@ func generateTables(env environ.Values, cfg *Config, db *data.DBData) error {
}
fileData := struct{ Schema, Table string }{Schema: schema.Name, Table: table.Name}
for _, target := range cfg.TablePaths {
if err := genFile(env, fileData, contents, target, cfg.PostRun, cfg.OutputDir); err != nil {
if err := genFile(env, fileData, contents, target, cfg.NoOverwriteGlobs, cfg.PostRun, cfg.OutputDir); err != nil {
env.Log.Printf("Generating output for table %v", table.Name)
return errors.WithMessage(err, "generating file for table "+table.Name)
}
Expand All @@ -113,14 +113,28 @@ func generateTables(env environ.Values, cfg *Config, db *data.DBData) error {
return nil
}

func genFile(env environ.Values, filedata, contents interface{}, target OutputTarget, postrun []string, outputDir string) error {
func genFile(env environ.Values, filedata, contents interface{}, target OutputTarget, noOverwriteGlobs, postrun []string, outputDir string) error {
buf := &bytes.Buffer{}
err := target.Filename.Execute(buf, filedata)
if err != nil {
return errors.WithMessage(err, "failed to run Filename template")
}
outputPath := filepath.Join(outputDir, buf.String())

// if file exists and filename matches glob, abort
if _, err := os.Stat(outputPath); err == nil {
for _, glob := range noOverwriteGlobs {
m, err := filepath.Match(glob, buf.String())
if err != nil {
return errors.WithMessage(err, "error checking glob")
}
if m {
env.Log.Printf("Skipping generation for file %s", buf.String())
return nil
}
}
}

if err := os.MkdirAll(filepath.Dir(outputPath), 0700); err != nil {
return errors.WithMessage(err, "error creating template output directory")
}
Expand Down
76 changes: 75 additions & 1 deletion run/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestAtomicGenerate(t *testing.T) {
}
defer os.Remove(filename)
contents := "hello world"
err = genFile(env, filename, contents, target, nil, ".")
err = genFile(env, filename, contents, target, nil, nil, ".")
if err == nil {
t.Fatal("Unexpected nil error generating contents. Should have failed.")
}
Expand Down Expand Up @@ -89,3 +89,77 @@ func TestCopyStaticFiles(t *testing.T) {
t.Errorf("expected %v to equal %v", newPaths, originPaths)
}
}

func TestNoOverwriteGlobs(t *testing.T) {
target := OutputTarget{
Filename: template.Must(template.New("").Parse("{{.}}")),
Contents: template.Must(template.New("").Parse("{{.}}")),
}
env := environ.Values{
Log: log.New(ioutil.Discard, "", 0),
}

filename := "testfile.out"

t.Run("file exists and matches glob", func(t *testing.T) {
original := []byte("goodbye world")
err := ioutil.WriteFile(filename, original, 0600)
if err != nil {
t.Fatal(err)
}
defer os.Remove(filename)

err = genFile(env, filename, "hello world", target, []string{"*.out"}, nil, ".")
if err != nil {
t.Fatalf("Unexpected error generating contents: %s", err)
}

b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}

if !bytes.Equal(b, original) {
t.Fatalf("Expected file to be unchanged, but was different. Expected: %q, got: %q", original, b)
}

t.Run("does not match glob", func(t *testing.T) {
content := "hello world"
err = genFile(env, filename, content, target, []string{"bob"}, nil, ".")
if err != nil {
t.Fatalf("Unexpected error generating contents: %s", err)
}

b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}

if !bytes.Equal(b, []byte(content)) {
t.Fatalf("Expected file to contain content, but did not. Expected: %q, got: %q", content, b)
}
})
})

t.Run("file does not exist but matches glob", func(t *testing.T) {
if _, err := os.Stat(filename); err == nil {
t.Fatalf("File should not exist, but does.")
}

content := "hello world"
err := genFile(env, filename, content, target, []string{"*.out"}, nil, ".")
if err != nil {
t.Fatalf("Unexpected error generating contents: %s", err)
}
defer os.Remove(filename)

b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}

if !bytes.Equal(b, []byte(content)) {
t.Fatalf("Expected file to contain content, but did not. Expected: %q, got: %q", content, b)
}
})
}
7 changes: 6 additions & 1 deletion site/content/cli/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ gocog}}} -->
ConnStr = "dbname=mydb host=127.0.0.1 sslmode=disable user=admin"
# DBType holds the type of db you're connecting to. Possible values are
# "postgres" or "mysql".
# "postgres" or "mysql".
DBType = "postgres"
# Schemas holds the names of schemas to generate code for.
Expand Down Expand Up @@ -100,6 +100,10 @@ OutputDir = "gnorm"
# OutputDir
StaticDir = "static"
# NoOverwriteGlobs is a list of globs
# (https://golang.org/pkg/path/filepath/#Match). If a filename matches a glob
# *and* a file exists with that name, it will not be generated.
NoOverwriteGlobs = ["*.perm.go"]
# TablePaths is a map of output paths to template paths that tells Gnorm how to
# render and output its table info and where to save that output. Each template
Expand Down Expand Up @@ -184,5 +188,6 @@ StaticDir = "static"
# .Params value for all templates.
[Params]
mySpecialValue = "some value"
```
<!-- {{{end}}} -->

0 comments on commit d696814

Please sign in to comment.