diff --git a/cli/config.go b/cli/config.go index 6bbd434..ff80ac1 100644 --- a/cli/config.go +++ b/cli/config.go @@ -107,4 +107,20 @@ type Config struct { // PluginDirs a set of absolute/relative paths that will be used for // plugin lookup. PluginDirs []string + + // OutputDir is the directory relative to the project root (where the + // gnorm.toml file is located) in which all the generated files are written + // to. + // + // This defaults to the current working directory i.e the directory in which + // gnorm.toml is found. + OutputDir string + + // StaticDir is the directory relative to the project root (where the + // gnorm.toml file is located) in which all static files , which are + // intended to be copied to the OutputDir are found. + // + // The directory structure is preserved when copying the files to the + // OutputDir + StaticDir string } diff --git a/cli/parse.go b/cli/parse.go index d548d9c..4afab8f 100644 --- a/cli/parse.go +++ b/cli/parse.go @@ -50,6 +50,9 @@ func parse(env environ.Values, r io.Reader) (*run.Config, error) { if len(c.ExcludeTables) > 0 && len(c.IncludeTables) > 0 { return nil, errors.New("both include tables and exclude tables") } + if c.OutputDir == "" { + c.OutputDir = "." + } include, err := parseTables(c.IncludeTables, c.Schemas) if err != nil { @@ -70,6 +73,8 @@ func parse(env environ.Values, r io.Reader) (*run.Config, error) { ExcludeTables: exclude, IncludeTables: include, }, + OutputDir: c.OutputDir, + StaticDir: c.StaticDir, } d, err := getDriver(strings.ToLower(c.DBType)) if err != nil { diff --git a/run/config.go b/run/config.go index 470cbc3..6bdeea9 100644 --- a/run/config.go +++ b/run/config.go @@ -52,6 +52,19 @@ type Config struct { // for different situations. The values in this field will be available in // the .Params value for all templates. Params map[string]interface{} + + // OutputDir is the directory relative to the project root (where the + // gnorm.toml file is located) in which all the generated files are written + // to. + OutputDir string + + // StaticDir is the directory relative to the project root (where the + // gnorm.toml file is located) in which all static files , which are + // intended to be copied to the OutputDir are found. + // + // The directory structure is preserved when copying the files to the + // OutputDir + StaticDir string } // OutputTarget contains a template that generates a filename to write to, and diff --git a/run/generate.go b/run/generate.go index 89e879b..3616449 100644 --- a/run/generate.go +++ b/run/generate.go @@ -3,6 +3,8 @@ package run // import "gnorm.org/gnorm/run" import ( "bytes" "context" + "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -47,7 +49,7 @@ func Generate(env environ.Values, cfg *Config) error { return err } } - return nil + return copyStaticFiles(env, cfg.StaticDir, cfg.OutputDir) } func generateSchemas(env environ.Values, cfg *Config, db *data.DBData) error { @@ -162,3 +164,65 @@ func doPostRun(env environ.Values, file string, postrun []string) error { } return nil } + +// copyStaticFiles copies files recursively from src directory to dest directory +// while preserving the directory structure +func copyStaticFiles(env environ.Values, src string, dest string) error { + if src == "" || dest == "" { + return nil + } + stat, err := os.Stat(src) + if err != nil { + return err + } + if !stat.IsDir() { + return fmt.Errorf("Outputdir specifies a directory path that already exists as file %s", dest) + } + var dstat os.FileInfo + dstat, err = os.Stat(dest) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(dest, stat.Mode()) + if err != nil { + return err + } + dstat = stat + } else { + return err + } + } + if !dstat.IsDir() { + return fmt.Errorf("%s is not a directory", dest) + } + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + base := filepath.Dir(path) + rel, err := filepath.Rel(src, base) + if err != nil { + return err + } + o := filepath.Join(dest, rel) + err = os.MkdirAll(o, stat.Mode()) + if err != nil { + return err + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + t, err := os.OpenFile(filepath.Join(o, filepath.Base(path)), os.O_RDWR|os.O_TRUNC|os.O_CREATE, info.Mode()) + if err != nil { + return err + } + defer t.Close() + _, err = io.Copy(t, f) + return err + }) +} diff --git a/run/generate_test.go b/run/generate_test.go index a2c1238..4356639 100644 --- a/run/generate_test.go +++ b/run/generate_test.go @@ -5,6 +5,9 @@ import ( "io/ioutil" "log" "os" + "path/filepath" + "reflect" + "sort" "testing" "text/template" @@ -41,3 +44,48 @@ func TestAtomicGenerate(t *testing.T) { t.Fatalf("Expected file to be unchanged, but was different. Expected: %q, got: %q", original, b) } } + +func TestCopyStaticFiles(t *testing.T) { + originPaths := []string{ + "base/base.md", + "base/level_one/level_one.md", + "base/level_two/level_two.md", + } + source := "testdata" + dest := "static_asset" + + err := copyStaticFiles(environ.Values{}, source, dest) + if err != nil { + t.Fatal(err) + } + defer func() { + err := os.RemoveAll(dest) + if err != nil { + t.Fatal(err) + } + }() + + // make sure the structure is preserved + var newPaths []string + filepath.Walk(dest, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + r, err := filepath.Rel(dest, path) + if err != nil { + return err + } + newPaths = append(newPaths, r) + return nil + }) + + sort.Strings(originPaths) + sort.Strings(newPaths) + + if !reflect.DeepEqual(originPaths, newPaths) { + t.Errorf("expected %v to equal %v", newPaths, originPaths) + } +} diff --git a/run/testdata/base/base.md b/run/testdata/base/base.md new file mode 100644 index 0000000..e69de29 diff --git a/run/testdata/base/level_one/level_one.md b/run/testdata/base/level_one/level_one.md new file mode 100644 index 0000000..e69de29 diff --git a/run/testdata/base/level_two/level_two.md b/run/testdata/base/level_two/level_two.md new file mode 100644 index 0000000..e69de29