dati

A Go library/binary to parse & execute data against template langauges.
git clone git://src.gearsix.net/dati
Log | Files | Refs | Atom | README | LICENSE

commit f4477f9edeec387c38041fbf9d27bd4f29309bd9
parent 02fbd1aa507bd89f037f130bce7ae5d45053ed47
Author: gearsix <gearsix@tuta.io>
Date:   Sun, 21 Mar 2021 17:22:11 +0000

standardized pkg layout; bugfixes & refactors

Diffstat:
Acmd/suti.go | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adata.go | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adata_test.go | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rdoc/suti.txt -> docs/suti.txt | 0
Mgo.mod | 4----
Mgo.sum | 24+++++++++++++++++-------
Dsrc/data.go | 256-------------------------------------------------------------------------------
Dsrc/data_test.go | 212-------------------------------------------------------------------------------
Dsrc/suti.go | 163-------------------------------------------------------------------------------
Dsrc/template.go | 175-------------------------------------------------------------------------------
Dsrc/template_test.go | 213-------------------------------------------------------------------------------
Atemplate.go | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atemplate_test.go | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 1075 insertions(+), 1030 deletions(-)

diff --git a/cmd/suti.go b/cmd/suti.go @@ -0,0 +1,185 @@ +package main + +/* + Copyright (C) 2021 gearsix <gearsix@tuta.io> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import ( + "bufio" + "fmt" + "io" + "path/filepath" + "os" + "strings" + "git.gearsix.net/suti" +) + +type options struct { + RootPath string + PartialPaths []string + GlobalDataPaths []string + DataPaths []string + DataKey string + SortData string + ConfigFile string +} + +var opts options +var cwd string + +func warn(err error, msg string, args ...interface{}) { + warning := "WARNING " + if len(msg) > 0 { + warning += strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n") + if err != nil { + warning += ": " + } + } + if err != nil { + warning += err.Error() + } + fmt.Println(warning) +} + +func assert(err error, msg string, args ...interface{}) { + if err != nil { + fmt.Printf("ERROR %s\n%s\n", strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n"), err) + os.Exit(1) + } +} + +func basedir(path string) string { + if !filepath.IsAbs(path) { + path = filepath.Join(cwd, path) + } + return path +} + +func init() { + if len(os.Args) <= 1 { + fmt.Println("nothing to do") + os.Exit(0) + } + + opts = parseArgs(os.Args[1:], options{}) + if len(opts.ConfigFile) != 0 { + cwd = filepath.Dir(opts.ConfigFile) + opts = parseConfig(opts.ConfigFile, opts) + } + opts = setDefaultOptions(opts) +} + +func main() { + data, err := suti.LoadDataFiles("", opts.GlobalDataPaths...) + assert(err, "failed to load global data files") + global, conflicts := suti.MergeData(data...) + for _, key := range conflicts { + warn(nil, "merge conflict for global data key: '%s'", key) + } + + data, err = suti.LoadDataFiles(opts.SortData, opts.DataPaths...) + assert(err, "failed to load data files") + + super, err := suti.GenerateSuperData(opts.DataKey, global, data) + assert(err, "failed to generate super data") + + template, err := suti.LoadTemplateFile(opts.RootPath, opts.PartialPaths...) + assert(err, "unable to load templates") + + out, err := suti.ExecuteTemplate(template, super) + assert(err, "failed to execute template '%s'", opts.RootPath) + fmt.Print(out.String()) + + return +} + +// custom arg parser because golang.org/pkg/flag doesn't support list args +func parseArgs(args []string, existing options) (o options) { + o = existing + var flag string + for a := 0; a < len(args); a++ { + arg := args[a] + if arg[0] == '-' && flag != "--" { + flag = arg + ndelims := 0 + for len(flag) > 0 && flag[0] == '-' { + flag = flag[1:] + ndelims++ + } + + if ndelims > 2 { + warn(nil, "bad flag syntax: '%s'", arg) + flag = "" + } + + // set valid any flags that don't take arguments here + } else if (flag == "r" || flag == "root") && len(o.RootPath) == 0 { + o.RootPath = basedir(arg) + } else if flag == "p" || flag == "partial" { + o.PartialPaths = append(o.PartialPaths, basedir(arg)) + } else if flag == "gd" || flag == "globaldata" { + o.GlobalDataPaths = append(o.GlobalDataPaths, basedir(arg)) + } else if flag == "d" || flag == "data" { + o.DataPaths = append(o.DataPaths, basedir(arg)) + } else if flag == "dk" || flag == "datakey" && len(o.DataKey) == 0 { + o.DataKey = arg + } else if flag == "sd" || flag == "sortdata" && len(o.SortData) == 0 { + o.SortData = arg + } else if flag == "cfg" || flag == "config" && len(o.ConfigFile) == 0 { + o.ConfigFile = basedir(arg) + } else if len(flag) == 0 { + // skip unknown flag arguments + } else { + warn(nil, "ignoring flag: '%s'", flag) + flag = "" + } + } + + return +} + +func parseConfig(fpath string, existing options) options { + var err error + var cfgf *os.File + if cfgf, err = os.Open(fpath); err != nil { + warn(err, "error loading config file '%s'", fpath) + err = io.EOF + } + defer cfgf.Close() + + var args []string + scanf := bufio.NewScanner(cfgf) + for scanf.Scan() { + for i, arg := range strings.Split(scanf.Text(), "=") { + arg = strings.TrimSpace(arg) + if i == 0 { + arg = "-" + arg + } + args = append(args, arg) + } + } + return parseArgs(args, existing) +} + +func setDefaultOptions(o options) options { + if len(o.SortData) == 0 { + o.SortData = "filename" + } + if len(o.DataKey) == 0 { + o.DataKey = "data" + } + return o +} diff --git a/data.go b/data.go @@ -0,0 +1,255 @@ +package suti + +/* + Copyright (C) 2021 gearsix <gearsix@tuta.io> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import ( + "encoding/json" + "fmt" + "github.com/pelletier/go-toml" + "gopkg.in/yaml.v3" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +// Data is the data type used to represent parsed Data (in any format). +type Data map[string]interface{} + +func getDataType(path string) string { + return strings.TrimPrefix(filepath.Ext(path), ".") +} + +func loadGlobPaths(paths ...string) ([]string, error) { + var err error + var glob []string + for p, path := range paths { + if strings.Contains(path, "*") { + if glob, err = filepath.Glob(path); err == nil { + paths = append(paths, glob...) + if len(glob) > 0 { + paths = append(paths[:p], paths[p+1:]...) + } + } + } + } + return paths, err +} + +// LoadData reads all data from `in` and loads it in the format set in `lang`. +func LoadData(lang string, in io.Reader) (d Data, e error) { + var fbuf []byte + if fbuf, e = ioutil.ReadAll(in); e != nil { + return make(Data), e + } else if len(fbuf) == 0 { + return make(Data), nil + } + + if lang == "json" { + e = json.Unmarshal(fbuf, &d) + } else if lang == "yaml" { + e = yaml.Unmarshal(fbuf, &d) + } else if lang == "toml" { + e = toml.Unmarshal(fbuf, &d) + } else { + e = fmt.Errorf("'%s' is not a supported data language", lang) + } + + return +} + +// LoadDataFile loads all the data from the file found at `path` into the the +// format of that files file extension (e.g. "x.json" will be loaded as a json). +func LoadDataFile(path string) (d Data, e error) { + var f *os.File + if f, e = os.Open(path); e == nil { + d, e = LoadData(getDataType(path), f) + } + f.Close() + return +} + +// LoadDataFiles loads all files in `paths` recursively and sorted them in +// `order`. +func LoadDataFiles(order string, paths ...string) (data []Data, err error) { + var stat os.FileInfo + + paths, err = loadGlobPaths(paths...) + + loaded := make(map[string]Data) + + for i := 0; i < len(paths) && err == nil; i++ { + path := paths[i] + if stat, err = os.Stat(path); err == nil { + if stat.IsDir() { + err = filepath.Walk(path, + func(p string, fi os.FileInfo, e error) error { + if e == nil && !fi.IsDir() { + loaded[p], e = LoadDataFile(p) + } + return e + }) + } else { + loaded[path], err = LoadDataFile(path) + } + } + } + + if err == nil { + data, err = sortFileData(loaded, order) + } + + return data, err +} + +func sortFileData(data map[string]Data, order string) ([]Data, error) { + var err error + sorted := make([]Data, 0, len(data)) + + if strings.HasPrefix(order, "filename") { + if order == "filename-desc" { + sorted = sortFileDataFilename("desc", data) + } else if order == "filename-asc" { + sorted = sortFileDataFilename("asc", data) + } else { + sorted = sortFileDataFilename("asc", data) + } + } else if strings.HasPrefix(order, "modified") { + if order == "modified-desc" { + sorted, err = sortFileDataModified("desc", data) + } else if order == "modified-asc" { + sorted, err = sortFileDataModified("asc", data) + } else { + sorted, err = sortFileDataModified("asc", data) + } + } else { + for _, d := range data { + sorted = append(sorted, d) + } + } + + return sorted, err +} + +func sortFileDataFilename(direction string, data map[string]Data) []Data { + sorted := make([]Data, 0, len(data)) + + fnames := make([]string, 0, len(data)) + for fpath := range data { + fnames = append(fnames, filepath.Base(fpath)) + } + sort.Strings(fnames) + + if direction == "desc" { + for i := len(fnames) - 1; i >= 0; i-- { + for fpath, d := range data { + if fnames[i] == filepath.Base(fpath) { + sorted = append(sorted, d) + } + } + } + } else { + for _, fname := range fnames { + for fpath, d := range data { + if fname == filepath.Base(fpath) { + sorted = append(sorted, d) + } + } + } + } + + return sorted +} + +func sortFileDataModified(direction string, data map[string]Data) ([]Data, error) { + sorted := make([]Data, 0, len(data)) + + stats := make(map[string]os.FileInfo) + for fpath := range data { + if stat, err := os.Stat(fpath); err != nil { + return nil, err + } else { + stats[fpath] = stat + } + } + + modtimes := make([]time.Time, 0, len(data)) + for _, stat := range stats { + modtimes = append(modtimes, stat.ModTime()) + } + + if direction == "desc" { + sort.Slice(modtimes, func(i, j int) bool { + return modtimes[i].After(modtimes[j]) + }) + } else { + sort.Slice(modtimes, func(i, j int) bool { + return modtimes[i].Before(modtimes[j]) + }) + } + + for _, t := range modtimes { + for fpath, stat := range stats { + if stat.ModTime() == t { + sorted = append(sorted, data[fpath]) + delete(stats, fpath) + break + } + } + } + + return sorted, nil +} + +// GenerateSuperData merges all `global` Data and then adds `d` to the merged +// structure under the key provided in `datakey`. +func GenerateSuperData(datakey string, global Data, data []Data) (super Data, err error) { + super = global + + if len(datakey) == 0 { + datakey = "data" + } + + if super[datakey] != nil { + err = fmt.Errorf("datakey '%s' already exists", datakey) + } else { + super[datakey] = data + } + + return +} + +// MergeData combines all keys in `data` into a single Data object. If there's +// a conflict (duplicate key), the first found value is kept and the conflicting +// values are ignored. +func MergeData(data ...Data) (merged Data, conflicts []string) { + merged = make(Data) + for _, d := range data { + for key, val := range d { + if merged[key] == nil { + merged[key] = val + } else { + conflicts = append(conflicts, key) + } + } + } + return +} diff --git a/data_test.go b/data_test.go @@ -0,0 +1,227 @@ +package suti + +/* + Copyright (C) 2021 gearsix <gearsix@tuta.io> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + + +import ( + "encoding/json" + "github.com/pelletier/go-toml" + "gopkg.in/yaml.v3" + "os" + "strings" + "testing" + "time" +) + +var good = map[string]string{ + "json": `{"eg":0}`, + "yaml": `eg: 0 +`, + "toml": `eg = 0 +`, +} + +const badData = `{"json"!:2:]}}` + +func writeTestFile(t *testing.T, path string, Data string) { + f, e := os.Create(path) + defer f.Close() + if e != nil { + t.Skipf("setup failure: %s", e) + } + _, e = f.WriteString(Data) + if e != nil { + t.Skipf("setup failure: %s", e) + } + + return +} + +func validateData(t *testing.T, d Data, e error, lang string) { + var b []byte + + if e != nil { + t.Error(e) + } + if len(d) == 0 { + t.Error("no data loaded") + } + + switch lang { + case "json": + b, e = json.Marshal(d) + case "yaml": + b, e = yaml.Marshal(d) + case "toml": + b, e = toml.Marshal(d) + } + + if e != nil { + t.Error(e) + } + if string(b) != good[lang] { + t.Errorf("incorrect %s: %s does not match %s", lang, b, good[lang]) + } +} + +func TestLoadData(t *testing.T) { + var d Data + var e error + + for lang, data := range good { + d, e = LoadData(lang, strings.NewReader(data)) + validateData(t, d, e, lang) + + if d, e = LoadData(lang, strings.NewReader(badData)); e == nil || len(d) > 0 { + t.Errorf("bad %s passed", lang) + } + + if d, e = LoadData(lang, strings.NewReader("")); e != nil { + t.Errorf("empty file failed for json: %s, %s", d, e) + } + } + + if d, e = LoadData("invalid", strings.NewReader("shouldn't pass")); e == nil || len(d) > 0 { + t.Errorf("invalid data language passed: %s, %s", d, e) + } + + return +} + +func validateFileData(t *testing.T, e error, d []Data, dlen int, orderedLangs ...string) { + if e != nil { + t.Error(e) + } + + if dlen != len(orderedLangs) { + t.Errorf("invalid orderedLangs length (%d should be %d)", len(orderedLangs), dlen) + } + + if len(d) != dlen { + t.Errorf("invalid data length (%d should be %d)", len(d), dlen) + } + + for i, lang := range orderedLangs { + validateData(t, d[i], nil, lang) + } +} + +func TestLoadDataFiles(t *testing.T) { + var e error + var p []string + var d []Data + tdir := t.TempDir() + + p = append(p, tdir+"/1.yaml") + writeTestFile(t, p[len(p)-1], good["yaml"]) + time.Sleep(100 * time.Millisecond) + p = append(p, tdir+"/good.json") + writeTestFile(t, p[len(p)-1], good["json"]) + time.Sleep(100 * time.Millisecond) + p = append(p, tdir+"/good.toml") + writeTestFile(t, p[len(p)-1], good["toml"]) + + d, e = LoadDataFiles("filename", tdir) + validateFileData(t, e, d, len(p), "yaml", "json", "toml") + + d, e = LoadDataFiles("filename-desc", tdir+"/*") + validateFileData(t, e, d, len(p), "toml", "json", "yaml") + + d, e = LoadDataFiles("modified", p...) + validateFileData(t, e, d, len(p), "yaml", "json", "toml") + + d, e = LoadDataFiles("modified-desc", p...) + validateFileData(t, e, d, len(p), "toml", "json", "yaml") + + p = append(p, tdir+"/bad.json") + writeTestFile(t, p[len(p)-1], badData) + if _, e = LoadDataFiles("modified-desc", p...); e == nil { + t.Error("bad.json passed") + } +} + +func TestGenerateSuperData(t *testing.T) { + var data Data + var e error + var gd Data + var d []Data + var sd Data + + if data, e = LoadData("json", strings.NewReader(good["json"])); e == nil { + gd = data + } else { + t.Skip("setup failure:", e) + } + if data, e = LoadData("json", strings.NewReader(good["json"])); e == nil { + d = append(d, data) + } else { + t.Skip("setup failure:", e) + } + if data, e = LoadData("yaml", strings.NewReader(good["yaml"])); e == nil { + d = append(d, data) + } else { + t.Skip("setup failure:", e) + } + + sd, e = GenerateSuperData("testdata", gd, d) + if e != nil { + t.Error(e) + } + if sd["testdata"] == nil { + t.Log(sd) + t.Error("datakey is empty") + } + if v, ok := sd["testdata"].([]Data); ok == false { + t.Log(sd) + t.Error("unable to infer datakey 'testdata'") + } else if len(v) < len(data) { + t.Log(sd) + t.Error("datakey is missing data") + } +} + +func TestMergeData(t *testing.T) { + var e error + var d []Data + var m Data + var c []string + + if m, e = LoadData("json", strings.NewReader(good["json"])); e == nil { + d = append(d, m) + } else { + t.Skip("setup failure:", e) + } + if m, e = LoadData("json", strings.NewReader(good["json"])); e == nil { + d = append(d, m) + } else { + t.Skip("setup failure:", e) + } + if m, e = LoadData("yaml", strings.NewReader(good["yaml"])); e == nil { + d = append(d, m) + } else { + t.Skip("setup failure:", e) + } + + m, c = MergeData(d...) + if m["eg"] == nil { + t.Error("missing global keys") + } + if len(c) == 0 { + t.Errorf("conflicting keys were not reported") + } +} diff --git a/doc/suti.txt b/docs/suti.txt diff --git a/go.mod b/go.mod @@ -4,10 +4,6 @@ go 1.16 require ( github.com/cbroglie/mustache v1.2.0 - github.com/coreos/go-etcd v2.0.0+incompatible // indirect - github.com/cpuguy83/go-md2man v1.0.10 // indirect github.com/pelletier/go-toml v1.8.1 - github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect - golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum @@ -11,6 +11,7 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRmbhAH8HLxhiiG6nYNwaBZjrFps1oWEk= @@ -40,12 +41,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -86,6 +85,7 @@ github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur9 github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0 h1:4DFWWMXVfbcN5So1sBNW9+yeiMqLFGl1wFLTL5R0Tgg= github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= @@ -153,6 +153,7 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -183,6 +184,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -195,6 +197,7 @@ github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xl github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -209,6 +212,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kyoh86/exportloopref v0.1.4 h1:t8QP+vBUykOFp6Bks/ZVYm3+Rp3+aj+AKWpGXgK4anA= github.com/kyoh86/exportloopref v0.1.4/go.mod h1:h1rDl2Kdj97+Kwh4gdz3ujE7XHmH51Q0lUiZ1z4NLj8= @@ -253,13 +257,16 @@ github.com/nakabonne/nestif v0.3.0 h1:+yOViDGhg8ygGrmII72nV9B/zGxY188TYpfolntsaP github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nishanths/exhaustive v0.0.0-20200525081945-8e46705b6132 h1:NjznefjSrral0MiR4KlB41io/d3OklvhcgQUdfZTqJE= github.com/nishanths/exhaustive v0.0.0-20200525081945-8e46705b6132/go.mod h1:wBEpHwM2OdmeNpdCvRPUlkEbBuaFmcK4Wv8Q7FuGW3c= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -292,7 +299,6 @@ github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:r github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.1.0 h1:DWbye9KyMgytn8uYpuHkwf0RHqAYO6Ay/D0TbCpPtVU= github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM= @@ -304,13 +310,17 @@ github.com/securego/gosec/v2 v2.3.0 h1:y/9mCF2WPDbSDpL3QDWZD3HHGrSYw0QSHnCqTfs4J github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME= github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY= @@ -353,7 +363,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLKKwb7p1cnoygsbKIgNlJqSYBeAFON3Ar8As= github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbdo= github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg= @@ -394,7 +403,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -420,6 +428,7 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -486,7 +495,6 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200117220505-0cba7a3a9ee9/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= @@ -526,16 +534,18 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/data.go b/src/data.go @@ -1,256 +0,0 @@ -package main - -/* - Copyright (C) 2021 gearsix <gearsix@tuta.io> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import ( - "encoding/json" - "fmt" - "github.com/pelletier/go-toml" - "gopkg.in/yaml.v3" - "io" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - "time" -) - -// Data is the data type used to represent parsed Data (in any format). -type Data map[string]interface{} - -func getDataType(path string) string { - return strings.TrimPrefix(filepath.Ext(path), ".") -} - -func loadGlobPaths(paths ...string) []string { - for p, path := range paths { - if strings.Contains(path, "*") { - if glob, e := filepath.Glob(path); e == nil { - paths = append(paths, glob...) - paths = append(paths[:p], paths[p+1:]...) - } else { - warn("error parsing glob '%s': %s", path, e) - } - } - } - return paths -} - -// LoadData reads all data from `in` and loads it in the format set in `lang`. -func LoadData(lang string, in io.Reader) (d Data, e error) { - var fbuf []byte - if fbuf, e = ioutil.ReadAll(in); e != nil { - return make(Data), e - } else if len(fbuf) == 0 { - return make(Data), nil - } - - if lang == "json" { - e = json.Unmarshal(fbuf, &d) - } else if lang == "yaml" { - e = yaml.Unmarshal(fbuf, &d) - } else if lang == "toml" { - e = toml.Unmarshal(fbuf, &d) - } else { - e = fmt.Errorf("'%s' is not a supported data language", lang) - } - - return -} - -// LoadDataFile loads all the data from the file found at `path` into the the -// format of that files file extension (e.g. "x.json" will be loaded as a json). -func LoadDataFile(path string) (d Data, e error) { - var f *os.File - if f, e = os.Open(path); e != nil { - warn("could not load data file '%s' (%s)", path, e) - } else { - defer f.Close() - d, e = LoadData(getDataType(path), f) - } - return -} - -// LoadDataFiles loads all files in `paths` recursively and sorted them in -// `order`. -func LoadDataFiles(order string, paths ...string) []Data { - var err error - var stat os.FileInfo - var d Data - - loaded := make(map[string]Data) - - paths = loadGlobPaths(paths...) - - for _, path := range paths { - stat, err = os.Stat(path) - if err == nil { - if stat.IsDir() { - err = filepath.Walk(path, - func(p string, fi os.FileInfo, e error) error { - if e == nil && !fi.IsDir() { - if d, e = LoadDataFile(p); e == nil { - loaded[p] = d - } else { - warn("skipping data file '%s' (%s)", p, e) - e = nil - } - } - return e - }) - if err != nil { - warn("error loading files in %s (%s)", path, err) - } - } else if d, err = LoadDataFile(path); err == nil { - loaded[path] = d - } else { - warn("skipping data file '%s' (%s)", path, err) - } - } - } - - return sortFileData(loaded, order) -} - -func sortFileData(data map[string]Data, order string) []Data { - sorted := make([]Data, 0, len(data)) - - if strings.HasPrefix(order, "filename") { - if order == "filename-desc" { - sorted = sortFileDataFilename("desc", data) - } else if order == "filename-asc" { - sorted = sortFileDataFilename("asc", data) - } else { - sorted = sortFileDataFilename("asc", data) - } - } else if strings.HasPrefix(order, "modified") { - if order == "modified-desc" { - sorted = sortFileDataModified("desc", data) - } else if order == "modified-asc" { - sorted = sortFileDataModified("asc", data) - } else { - sorted = sortFileDataModified("asc", data) - } - } else { - for _, d := range data { - sorted = append(sorted, d) - } - } - - return sorted -} - -func sortFileDataFilename(direction string, data map[string]Data) []Data { - sorted := make([]Data, 0, len(data)) - fnames := make([]string, 0, len(data)) - for fpath := range data { - fnames = append(fnames, filepath.Base(fpath)) - } - sort.Strings(fnames) - - if direction == "desc" { - for i := len(fnames) - 1; i >= 0; i-- { - for fpath, d := range data { - if fnames[i] == filepath.Base(fpath) { - sorted = append(sorted, d) - } - } - } - } else { - for _, fname := range fnames { - for fpath, d := range data { - if fname == filepath.Base(fpath) { - sorted = append(sorted, d) - } - } - } - } - return sorted -} - -func sortFileDataModified(direction string, data map[string]Data) []Data { - sorted := make([]Data, 0, len(data)) - stats := make(map[string]os.FileInfo) - for fpath := range data { - if stat, err := os.Stat(fpath); err != nil { - warn("failed to stat %s (%s)", fpath, err) - } else { - stats[fpath] = stat - } - } - - modtimes := make([]time.Time, 0, len(data)) - for _, stat := range stats { - modtimes = append(modtimes, stat.ModTime()) - } - if direction == "desc" { - sort.Slice(modtimes, func(i, j int) bool { - return modtimes[i].After(modtimes[j]) - }) - } else { - sort.Slice(modtimes, func(i, j int) bool { - return modtimes[i].Before(modtimes[j]) - }) - } - - for _, t := range modtimes { - for fpath, stat := range stats { - if stat.ModTime() == t { - sorted = append(sorted, data[fpath]) - delete(stats, fpath) - break - } - } - } - - return sorted -} - -// GenerateSuperData merges all `global` Data and then adds `d` to the merged -// structure under the key provided in `datakey`. -func GenerateSuperData(datakey string, d []Data, global ...Data) (superd Data) { - if len(datakey) == 0 { - datakey = "data" - } - superd = MergeData(global...) - - if superd[datakey] != nil { - warn("global data has a key matching the datakey ('%s')\n", - "this value of this key will be overwritten") - } - superd[datakey] = d - return -} - -// MergeData combines all keys in `data` into a single Data object. If there's -// a conflict (duplicate key), the first found value is kept and the conflicting -// values are ignored. -func MergeData(data ...Data) Data { - merged := make(Data) - for _, d := range data { - for k, v := range d { - if merged[k] == nil { - merged[k] = v - } else { - warn("merge conflict for data key '%s'", k) - } - } - } - return merged -} diff --git a/src/data_test.go b/src/data_test.go @@ -1,212 +0,0 @@ -package main - -/* - Copyright (C) 2021 gearsix <gearsix@tuta.io> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - - -import ( - "encoding/json" - "github.com/pelletier/go-toml" - "gopkg.in/yaml.v3" - "os" - "strings" - "testing" - "time" -) - -var good = map[string]string{ - "json": `{"eg":0}`, - "yaml": `eg: 0 -`, - "toml": `eg = 0 -`, -} - -const badData = `{"json"!:2:]}}` - -func writeTestFile(t *testing.T, path string, Data string) { - f, e := os.Create(path) - defer f.Close() - if e != nil { - t.Skipf("setup failure: %s", e) - } - _, e = f.WriteString(Data) - if e != nil { - t.Skipf("setup failure: %s", e) - } - - return -} - -func validateData(t *testing.T, d Data, e error, lang string) { - var b []byte - - if e != nil { - t.Error(e) - } - if len(d) == 0 { - t.Error("no data loaded") - } - - switch lang { - case "json": - b, e = json.Marshal(d) - case "yaml": - b, e = yaml.Marshal(d) - case "toml": - b, e = toml.Marshal(d) - } - - if e != nil { - t.Error(e) - } - if string(b) != good[lang] { - t.Errorf("incorrect %s: %s does not match %s", lang, b, good[lang]) - } -} - -func TestLoadData(t *testing.T) { - var d Data - var e error - - for lang, data := range good { - d, e = LoadData(lang, strings.NewReader(data)) - validateData(t, d, e, lang) - - if d, e = LoadData(lang, strings.NewReader(badData)); e == nil || len(d) > 0 { - t.Errorf("bad %s passed", lang) - } - - if d, e = LoadData(lang, strings.NewReader("")); e != nil { - t.Errorf("empty file failed for json: %s, %s", d, e) - } - } - - if d, e = LoadData("invalid", strings.NewReader("shouldn't pass")); e == nil || len(d) > 0 { - t.Errorf("invalid data language passed: %s, %s", d, e) - } - - return -} - -func validateFileData(t *testing.T, d []Data, dlen int, orderedLangs ...string) { - if dlen != len(orderedLangs) { - t.Errorf("invalid orderedLangs length (%d should be %d)", len(orderedLangs), dlen) - } - - if len(d) != dlen { - t.Errorf("invalid data length (%d should be %d)", len(d), dlen) - } - - for i, lang := range orderedLangs { - validateData(t, d[i], nil, lang) - } -} - -func TestLoadDataFiles(t *testing.T) { - var p []string - var d []Data - tdir := t.TempDir() - - p = append(p, tdir+"/1.yaml") - writeTestFile(t, p[len(p)-1], good["yaml"]) - time.Sleep(100 * time.Millisecond) - p = append(p, tdir+"/good.json") - writeTestFile(t, p[len(p)-1], good["json"]) - time.Sleep(100 * time.Millisecond) - p = append(p, tdir+"/good.toml") - writeTestFile(t, p[len(p)-1], good["toml"]) - time.Sleep(100 * time.Millisecond) - p = append(p, tdir+"/bad.json") - writeTestFile(t, p[len(p)-1], badData) - - d = LoadDataFiles("filename", tdir) - validateFileData(t, d, len(p)-1, "yaml", "json", "toml") - - d = LoadDataFiles("filename-desc", tdir+"/*") - validateFileData(t, d, len(p)-1, "toml", "json", "yaml") - - d = LoadDataFiles("modified", p...) - validateFileData(t, d, len(p)-1, "yaml", "json", "toml") - - d = LoadDataFiles("modified-desc", p...) - validateFileData(t, d, len(p)-1, "toml", "json", "yaml") -} - -func TestGenerateSuperData(t *testing.T) { - var data Data - var e error - var gd []Data - var d []Data - var sd Data - - if data, e = LoadData("json", strings.NewReader(good["json"])); e == nil { - gd = append(gd, data) - } else { - t.Skip("setup failure:", e) - } - if data, e = LoadData("json", strings.NewReader(good["json"])); e == nil { // test duplicate - gd = append(gd, data) - } else { - t.Skip("setup failure:", e) - } - if data, e = LoadData("yaml", strings.NewReader(good["yaml"])); e == nil { - d = append(d, data) - } else { - t.Skip("setup failure:", e) - } - - sd = GenerateSuperData("testdata", d, gd...) - if sd["testdata"] == nil { - t.Log(sd) - t.Error("datakey is empty") - } - if v, ok := sd["testdata"].([]Data); ok == false { - t.Log(sd) - t.Error("unable to infer datakey 'testdata'") - } else if len(v) < len(data) { - t.Log(sd) - t.Error("datakey is missing data") - } -} - -func TestMergeData(t *testing.T) { - var e error - var d []Data - var m Data - - if m, e = LoadData("json", strings.NewReader(good["json"])); e == nil { - d = append(d, m) - } else { - t.Skip("setup failure:", e) - } - if m, e = LoadData("json", strings.NewReader(good["json"])); e == nil { - d = append(d, m) - } else { - t.Skip("setup failure:", e) - } - if m, e = LoadData("yaml", strings.NewReader(good["yaml"])); e == nil { - d = append(d, m) - } else { - t.Skip("setup failure:", e) - } - - m = MergeData(d...) - if m["eg"] == nil { - t.Error("missing global keys") - } -} diff --git a/src/suti.go b/src/suti.go @@ -1,163 +0,0 @@ -package main - -/* - Copyright (C) 2021 gearsix <gearsix@tuta.io> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import ( - "bufio" - "fmt" - "io" - "path/filepath" - "os" - "strings" -) - -type options struct { - RootPath string - PartialPaths []string - GlobalDataPaths []string - DataPaths []string - DataKey string - SortData string - ConfigFile string -} - -var opts options -var cwd string - -func warn(msg string, args ...interface{}) { - fmt.Println("WARNING", strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n")) -} - -func basedir(path string) string { - var err error - if !filepath.IsAbs(path) { - if path, err = filepath.Rel(cwd, path); err != nil { - warn("failed to parse path '%s': %s", path, err) - } - } - return path -} - -func init() { - if len(os.Args) <= 1 { - print("nothing to do") - os.Exit(0) - } - - cwd = "." - opts = parseArgs(os.Args[1:], options{}) - if len(opts.ConfigFile) != 0 { - cwd = filepath.Dir(opts.ConfigFile) - opts = parseConfig(opts.ConfigFile, opts) - } - opts = setDefaultOptions(opts) -} - -func main() { - gd := LoadDataFiles("", opts.GlobalDataPaths...) - d := LoadDataFiles(opts.SortData, opts.DataPaths...) - sd := GenerateSuperData(opts.DataKey, d, gd...) - - if t, e := LoadTemplateFile(opts.RootPath, opts.PartialPaths...); e != nil { - warn("unable to load templates (%s)", e) - } else if out, err := ExecuteTemplate(t, sd); err != nil { - warn("failed to execute template '%s' (%s)", opts.RootPath, err) - } else { - fmt.Println(out.String()) - } - - return -} - -// custom arg parser because golang.org/pkg/flag doesn't support list args -func parseArgs(args []string, existing options) (o options) { - o = existing - var flag string - for a := 0; a < len(args); a++ { - arg := args[a] - if arg[0] == '-' && flag != "--" { - flag = arg - ndelims := 0 - for len(flag) > 0 && flag[0] == '-' { - flag = flag[1:] - ndelims++ - } - - if ndelims > 2 { - warn("bad flag syntax: '%s'", arg) - flag = "" - } - - // set valid any flags that don't take arguments here - } else if (flag == "r" || flag == "root") && len(o.RootPath) == 0 { - o.RootPath = basedir(arg) - } else if flag == "p" || flag == "partial" { - o.PartialPaths = append(o.PartialPaths, basedir(arg)) - } else if flag == "gd" || flag == "globaldata" { - o.GlobalDataPaths = append(o.GlobalDataPaths, basedir(arg)) - } else if flag == "d" || flag == "data" { - o.DataPaths = append(o.DataPaths, arg) - } else if flag == "dk" || flag == "datakey" && len(o.DataKey) == 0 { - o.DataKey = arg - } else if flag == "sd" || flag == "sortdata" && len(o.SortData) == 0 { - o.SortData = arg - } else if flag == "cfg" || flag == "config" && len(o.ConfigFile) == 0 { - o.ConfigFile = basedir(arg) - } else if len(flag) == 0 { - // skip unknown flag arguments - } else { - warn("ignoring flag: '%s'", flag) - flag = "" - } - } - - return -} - -func parseConfig(fpath string, existing options) options { - var err error - var cfgf *os.File - if cfgf, err = os.Open(fpath); err != nil { - warn("error loading config file '%s': %s", fpath, err) - err = io.EOF - } - defer cfgf.Close() - - var args []string - scanf := bufio.NewScanner(cfgf) - for scanf.Scan() { - for i, arg := range strings.Split(scanf.Text(), "=") { - arg = strings.TrimSpace(arg) - if i == 0 { - arg = "-" + arg - } - args = append(args, arg) - } - } - return parseArgs(args, existing) -} - -func setDefaultOptions(o options) options { - if len(o.SortData) == 0 { - o.SortData = "filename" - } - if len(o.DataKey) == 0 { - o.DataKey = "data" - } - return o -} diff --git a/src/template.go b/src/template.go @@ -1,175 +0,0 @@ -package main - -/* - Copyright (C) 2021 gearsix <gearsix@tuta.io> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import ( - "bytes" - "github.com/cbroglie/mustache" - "fmt" - hmpl "html/template" - "os" - "path/filepath" - "reflect" - "strings" - tmpl "text/template" -) - -// Template is a generic interface container for any template type -type Template interface{} - -func getTemplateType(path string) string { - return strings.TrimPrefix(filepath.Ext(path), ".") -} - -func loadTemplateFileTmpl(root string, partials ...string) (t *tmpl.Template, e error) { - var stat os.FileInfo - if t, e = tmpl.ParseFiles(root); e == nil { - for _, p := range partials { - ptype := getTemplateType(p) - stat, e = os.Stat(p) - - if e == nil { - if ptype == "tmpl" || ptype == "gotmpl" { - t, e = t.ParseFiles(p) - } else if strings.Contains(p, "*") { - t, e = t.ParseGlob(p) - } else if stat.IsDir() { - t, e = t.ParseGlob(p+"/*.tmpl") - t, e = t.ParseGlob(p+"/*.gotmpl") - } else { - e = fmt.Errorf("non-matching filetype") - } - } - - if e != nil { - warn("skipping partial '%s': %s", p, e) - } - } - } - return -} - -func loadTemplateFileHmpl(root string, partials ...string) (t *hmpl.Template, e error) { - var stat os.FileInfo - if t, e = hmpl.ParseFiles(root); e == nil { - for _, p := range partials { - ptype := getTemplateType(p) - stat, e = os.Stat(p) - - if e == nil { - if ptype == "hmpl" || ptype == "gohmpl" { - t, e = t.ParseFiles(p) - } else if strings.Contains(p, "*") { - t, e = t.ParseGlob(p) - } else if stat.IsDir() { - t, e = t.ParseGlob(p+"/*.hmpl") - t, e = t.ParseGlob(p+"/*.gohmpl") - } else { - e = fmt.Errorf("non-matching filetype") - } - } - - if e != nil { - warn("skipping partial '%s': %s", p, e) - e = nil - } - } - } - return -} - -func loadTemplateFileMst(root string, partials ...string) (t *mustache.Template, e error) { - for p, partial := range partials { - if stat, err := os.Stat(partial); err != nil { - partials = append(partials[:p], partials[p+1:]...) - warn("skipping partial '%s': %s", partial, e) - } else if stat.IsDir() == false { - partials[p] = filepath.Dir(partial) - } else if strings.Contains(partial, "*") { - if paths, err := filepath.Glob(partial); err != nil { - partials = append(partials[:p], partials[p+1:]...) - warn("skipping partial '%s': %s", partial, e) - } else { - partials = append(partials[:p], partials[p+1:]...) - partials = append(partials, paths...) - } - } - } - - mstfp := &mustache.FileProvider{ - Paths: partials, - Extensions: []string{".mst", ".mustache"}, - } - t, e = mustache.ParseFilePartials(root, mstfp) - - return -} - -// LoadTemplateFile loads a Template from file `root`. All files in `partials` -// that have the same template type (identified by file extension) are also -// parsed and associated with the parsed root template. -func LoadTemplateFile(root string, partials ...string) (t Template, e error) { - if len(root) == 0 { - return nil, fmt.Errorf("no root tempslate specified") - } - - if stat, err := os.Stat(root); err != nil { - return nil, err - } else if stat.IsDir() { - return nil, fmt.Errorf("root path must be a file, not a directory: %s", root) - } - - - ttype := getTemplateType(root) - if ttype == "tmpl" || ttype == "gotmpl" { - t, e = loadTemplateFileTmpl(root, partials...) - } else if ttype == "hmpl" || ttype == "gohmpl" { - t, e = loadTemplateFileHmpl(root, partials...) - } else if ttype == "mst" || ttype == "mustache" { - t, e = loadTemplateFileMst(root, partials...) - } else { - e = fmt.Errorf("'%s' is not a supported template language", ttype) - } - return -} - -// ExecuteTemplate executes `t` against `d`. Reflection is used to determine -// the template type and call it's execution fuction. -func ExecuteTemplate(t Template, d Data) (result bytes.Buffer, err error) { - tv := reflect.ValueOf(t) - tt := reflect.TypeOf(t) - - var rval []reflect.Value - if tt.String() == "*template.Template" { // tmpl or hmpl - rval = tv.MethodByName("Execute").Call([]reflect.Value{ - reflect.ValueOf(&result), reflect.ValueOf(&d), - }) - } else if tt.String() == "*mustache.Template" { // mustache - rval = tv.MethodByName("FRender").Call([]reflect.Value{ - reflect.ValueOf(&result), reflect.ValueOf(&d), - }) - } else { - err = fmt.Errorf("unable to infer template type '%s'", tt.String()) - } - - if rval[0].IsNil() == false { // rval[0] = err - err = rval[0].Interface().(error) - } - - return -} diff --git a/src/template_test.go b/src/template_test.go @@ -1,213 +0,0 @@ -package main - -/* - Copyright (C) 2021 gearsix <gearsix@tuta.io> - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import ( - "bytes" - "path/filepath" - "reflect" - "strings" - "testing" -) - -const tmplRootGood = "{{.eg}} {{ template \"tmplPartialGood.tmpl\" . }}" -const tmplPartialGood = "{{range .data}}{{.eg}}{{end}}" -const tmplResult = "0 00" -const tmplRootBad = "{{ example }}} {{{ template \"tmplPartialBad.tmpl\" . }}" -const tmplPartialBad = "{{{ .example }}" - -const hmplRootGood = "<!DOCTYPE html><html><p>{{.eg}} {{ template \"hmplPartialGood.hmpl\" . }}</p></html>" -const hmplPartialGood = "<b>{{range .data}}{{.eg}}{{end}}</b>" -const hmplResult = "<!DOCTYPE html><html><p>0 <b>00</b></p></html>" -const hmplRootBad = "{{ example }} {{{ template \"hmplPartialBad.hmpl\" . }}" -const hmplPartialBad = "<b>{{{ .example2 }}</b>" - -const mstRootGood = "{{eg}} {{> mstPartialGood}}" -const mstPartialGood = "{{#data}}{{eg}}{{/data}}" -const mstResult = "0 00" -const mstRootBad = "{{> badPartial.mst}}{{#doesnt-exist}}{{/exit}}" -const mstPartialBad = "p{{$}{{ > noexist}}" - -func validateTemplateFile(t *testing.T, template Template, root string, partials ...string) { - types := map[string]string{ - "tmpl": "*template.Template", - "gotmpl": "*template.Template", - "hmpl": "*template.Template", - "gohmpl": "*template.Template", - "mst": "*mustache.Template", - "mustache": "*mustache.Template", - } - - ttype := getTemplateType(root) - if reflect.TypeOf(template).String() != types[ttype] { - t.Error("invalid template loaded") - } - - if types[ttype] == "*template.Template" { - var rv []reflect.Value - for _, p := range partials { - p = filepath.Base(p) - rv := reflect.ValueOf(template).MethodByName("Lookup").Call([]reflect.Value{ - reflect.ValueOf(p), - }) - if rv[0].IsNil() { - t.Errorf("missing defined template '%s'", p) - rv = reflect.ValueOf(template).MethodByName("DefinedTemplates").Call([]reflect.Value{}) - t.Log(rv) - } - } - rv = reflect.ValueOf(template).MethodByName("Name").Call([]reflect.Value{}) - if rv[0].String() != filepath.Base(root) { - t.Errorf("invalid template name: %s does not match %s", - rv[0].String(), filepath.Base(root)) - } - } -} - -func TestLoadTemplateFile(t *testing.T) { - var gr, gp, br, bp []string - tdir := t.TempDir() - i := 0 - - gr = append(gr, tdir+"/goodRoot.tmpl") - writeTestFile(t, gr[i], tmplRootGood) - gp = append(gp, tdir+"/goodPartial.gotmpl") - writeTestFile(t, gp[i], tmplPartialGood) - br = append(br, tdir+"/badRoot.tmpl") - writeTestFile(t, br[i], tmplRootBad) - bp = append(bp, tdir+"/badPartial.gotmpl") - writeTestFile(t, bp[i], tmplPartialBad) - i++ - - gr = append(gr, tdir+"/goodRoot.hmpl") - writeTestFile(t, gr[i], hmplRootGood) - gp = append(gp, tdir+"/goodPartial.gohmpl") - writeTestFile(t, gp[i], hmplPartialGood) - br = append(br, tdir+"/badRoot.hmpl") - writeTestFile(t, br[i], hmplRootBad) - bp = append(bp, tdir+"/badPartial.gohmpl") - writeTestFile(t, bp[i], hmplPartialBad) - i++ - - gr = append(gr, tdir+"/goodRoot.mustache") - writeTestFile(t, gr[i], mstRootGood) - gp = append(gp, tdir+"/goodPartial.mst") - writeTestFile(t, gp[i], mstPartialGood) - br = append(br, tdir+"/badRoot.mst") - writeTestFile(t, br[i], mstRootBad) - bp = append(bp, tdir+"/badPartial.mst") - writeTestFile(t, bp[i], mstPartialBad) - - for g, root := range gr { // good root, good partials - if template, e := LoadTemplateFile(root, gp[g]); e != nil { - t.Error(e) - } else { - validateTemplateFile(t, template, root, gp[g]) - } - } - for _, root := range br { // bad root, good partials - if _, e := LoadTemplateFile(root, gp...); e == nil { - t.Errorf("no error for bad template with good partials\n") - } - } - for _, root := range br { // bad root, bad partials - if _, e := LoadTemplateFile(root, bp...); e == nil { - t.Errorf("no error for bad template with bad partials\n") - } - } -} - -func validateExecuteTemplate(t *testing.T, results string, expect string, e error) { - if e != nil { - t.Error(e) - } - if results != expect { - t.Errorf("invalid results: '%s' should match '%s'", results, expect) - } -} - -func TestExecuteTemplate(t *testing.T) { - var e error - var sd, data Data - var gd, d []Data - var tmplr, tmplp, hmplr, hmplp, mstr, mstp string - var tmpl1, tmpl2, hmpl1, hmpl2, mst1, mst2 Template - var results bytes.Buffer - tdir := t.TempDir() - - tmplr = tdir + "/tmplRootGood.gotmpl" - writeTestFile(t, tmplr, tmplRootGood) - tmplp = tdir + "/tmplPartialGood.tmpl" - writeTestFile(t, tmplp, tmplPartialGood) - hmplr = tdir + "/hmplRootGood.gohmpl" - writeTestFile(t, hmplr, hmplRootGood) - hmplp = tdir + "/hmplPartialGood.hmpl" - writeTestFile(t, hmplp, hmplPartialGood) - mstr = tdir + "/mstRootGood.mustache" - writeTestFile(t, mstr, mstRootGood) - mstp = tdir + "/mstPartialGood.mst" - writeTestFile(t, mstp, mstPartialGood) - - if data, e = LoadData("json", strings.NewReader(good["json"])); e != nil { - t.Skip("setup failure:", e) - } - gd = append(gd, data) - if data, e = LoadData("yaml", strings.NewReader(good["yaml"])); e != nil { - t.Skip("setup failure:", e) - } - d = append(d, data) - if data, e = LoadData("toml", strings.NewReader(good["toml"])); e != nil { - t.Skip("setup failure:", e) - } - d = append(d, data) - - sd = GenerateSuperData("", d, gd...) - if tmpl1, e = LoadTemplateFile(tmplr, tmplp); e != nil { - t.Skip("setup failure:", e) - } - if tmpl2, e = LoadTemplateFile(tmplr, tdir); e != nil { - t.Skip("setup failure:", e) - } - if hmpl1, e = LoadTemplateFile(hmplr, hmplp); e != nil { - t.Skip("setup failure:", e) - } - if hmpl2, e = LoadTemplateFile(tmplr, tdir); e != nil { - t.Skip("setup failure:", e) - } - if mst1, e = LoadTemplateFile(mstr, mstp); e != nil { - t.Skip("setup failure:", e) - } - if mst2, e = LoadTemplateFile(tmplr, tdir); e != nil { - t.Skip("setup failure:", e) - } - - results, e = ExecuteTemplate(tmpl1, sd) - validateExecuteTemplate(t, results.String(), tmplResult, e) - results, e = ExecuteTemplate(tmpl2, sd) - validateExecuteTemplate(t, results.String(), tmplResult, e) - - results, e = ExecuteTemplate(hmpl1, sd) - validateExecuteTemplate(t, results.String(), hmplResult, e) - results, e = ExecuteTemplate(hmpl2, sd) - validateExecuteTemplate(t, results.String(), tmplResult, e) - - results, e = ExecuteTemplate(mst1, sd) - validateExecuteTemplate(t, results.String(), mstResult, e) - results, e = ExecuteTemplate(mst2, sd) - validateExecuteTemplate(t, results.String(), mstResult, e) -} diff --git a/template.go b/template.go @@ -0,0 +1,176 @@ +package suti + +/* + Copyright (C) 2021 gearsix <gearsix@tuta.io> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import ( + "bytes" + mst "github.com/cbroglie/mustache" + "fmt" + hmpl "html/template" + "os" + "path/filepath" + "reflect" + "strings" + tmpl "text/template" +) + +// Template is a generic interface container for any template type +type Template interface{} + +func getTemplateType(path string) string { + return strings.TrimPrefix(filepath.Ext(path), ".") +} + +func loadTemplateFileTmpl(root string, partials ...string) (*tmpl.Template, error) { + var stat os.FileInfo + t, e := tmpl.ParseFiles(root) + + for i := 0; i < len(partials) && e == nil; i++ { + p := partials[i] + ptype := getTemplateType(p) + + stat, e = os.Stat(p) + if e == nil { + if ptype == "tmpl" || ptype == "gotmpl" { + t, e = t.ParseFiles(p) + } else if strings.Contains(p, "*") { + t, e = t.ParseGlob(p) + } else if stat.IsDir() { + t, e = t.ParseGlob(p+"/*.tmpl") + t, e = t.ParseGlob(p+"/*.gotmpl") + } else { + return nil, fmt.Errorf("non-matching filetype") + } + } + } + + return t, e +} + +func loadTemplateFileHmpl(root string, partials ...string) (*hmpl.Template, error) { + var stat os.FileInfo + t, e := hmpl.ParseFiles(root) + + for i := 0; i < len(partials) && e == nil; i++ { + p := partials[i] + ptype := getTemplateType(p) + + stat, e = os.Stat(p) + if e == nil { + if ptype == "hmpl" || ptype == "gohmpl" { + t, e = t.ParseFiles(p) + } else if strings.Contains(p, "*") { + t, e = t.ParseGlob(p) + } else if stat.IsDir() { + t, e = t.ParseGlob(p+"/*.hmpl") + t, e = t.ParseGlob(p+"/*.gohmpl") + } else { + return nil, fmt.Errorf("non-matching filetype") + } + } + } + + return t, e +} + +func loadTemplateFileMst(root string, partials ...string) (*mst.Template, error) { + var err error + for p, partial := range partials { + if err != nil { + break + } + + if stat, e := os.Stat(partial); e != nil { + partials = append(partials[:p], partials[p+1:]...) + err = e + } else if stat.IsDir() == false { + partials[p] = filepath.Dir(partial) + } else if strings.Contains(partial, "*") { + if paths, e := filepath.Glob(partial); e != nil { + partials = append(partials[:p], partials[p+1:]...) + err = e + } else { + partials = append(partials[:p], partials[p+1:]...) + partials = append(partials, paths...) + } + } + } + + if err != nil { + return nil, err + } + + mstfp := &mst.FileProvider{ + Paths: partials, + Extensions: []string{".mst", ".mst"}, + } + return mst.ParseFilePartials(root, mstfp) +} + +// LoadTemplateFile loads a Template from file `root`. All files in `partials` +// that have the same template type (identified by file extension) are also +// parsed and associated with the parsed root template. +func LoadTemplateFile(root string, partials ...string) (t Template, e error) { + if len(root) == 0 { + return nil, fmt.Errorf("no root tempslate specified") + } + + if stat, err := os.Stat(root); err != nil { + return nil, err + } else if stat.IsDir() { + return nil, fmt.Errorf("root path must be a file, not a directory: %s", root) + } + + ttype := getTemplateType(root) + if ttype == "tmpl" || ttype == "gotmpl" { + t, e = loadTemplateFileTmpl(root, partials...) + } else if ttype == "hmpl" || ttype == "gohmpl" { + t, e = loadTemplateFileHmpl(root, partials...) + } else if ttype == "mst" || ttype == "mustache" { + t, e = loadTemplateFileMst(root, partials...) + } else { + e = fmt.Errorf("'%s' is not a supported template language", ttype) + } + return +} + +// ExecuteTemplate executes `t` against `d`. Reflection is used to determine +// the template type and call it's execution fuction. +func ExecuteTemplate(t Template, d Data) (result bytes.Buffer, err error) { + tv := reflect.ValueOf(t) + tt := reflect.TypeOf(t) + + var rval []reflect.Value + if tt.String() == "*template.Template" { // tmpl or hmpl + rval = tv.MethodByName("Execute").Call([]reflect.Value{ + reflect.ValueOf(&result), reflect.ValueOf(&d), + }) + } else if tt.String() == "*mustache.Template" { // mustache + rval = tv.MethodByName("FRender").Call([]reflect.Value{ + reflect.ValueOf(&result), reflect.ValueOf(&d), + }) + } else { + err = fmt.Errorf("unable to infer template type '%s'", tt.String()) + } + + if rval[0].IsNil() == false { // rval[0] = err + err = rval[0].Interface().(error) + } + + return +} diff --git a/template_test.go b/template_test.go @@ -0,0 +1,215 @@ +package suti + +/* + Copyright (C) 2021 gearsix <gearsix@tuta.io> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import ( + "bytes" + "path/filepath" + "reflect" + "strings" + "testing" +) + +const tmplRootGood = "{{.eg}} {{ template \"tmplPartialGood.tmpl\" . }}" +const tmplPartialGood = "{{range .data}}{{.eg}}{{end}}" +const tmplResult = "0 00" +const tmplRootBad = "{{ example }}} {{{ template \"tmplPartialBad.tmpl\" . }}" +const tmplPartialBad = "{{{ .example }}" + +const hmplRootGood = "<!DOCTYPE html><html><p>{{.eg}} {{ template \"hmplPartialGood.hmpl\" . }}</p></html>" +const hmplPartialGood = "<b>{{range .data}}{{.eg}}{{end}}</b>" +const hmplResult = "<!DOCTYPE html><html><p>0 <b>00</b></p></html>" +const hmplRootBad = "{{ example }} {{{ template \"hmplPartialBad.hmpl\" . }}" +const hmplPartialBad = "<b>{{{ .example2 }}</b>" + +const mstRootGood = "{{eg}} {{> mstPartialGood}}" +const mstPartialGood = "{{#data}}{{eg}}{{/data}}" +const mstResult = "0 00" +const mstRootBad = "{{> badPartial.mst}}{{#doesnt-exist}}{{/exit}}" +const mstPartialBad = "p{{$}{{ > noexist}}" + +func validateTemplateFile(t *testing.T, template Template, root string, partials ...string) { + types := map[string]string{ + "tmpl": "*template.Template", + "gotmpl": "*template.Template", + "hmpl": "*template.Template", + "gohmpl": "*template.Template", + "mst": "*mustache.Template", + "mustache": "*mustache.Template", + } + + ttype := getTemplateType(root) + if reflect.TypeOf(template).String() != types[ttype] { + t.Error("invalid template loaded") + } + + if types[ttype] == "*template.Template" { + var rv []reflect.Value + for _, p := range partials { + p = filepath.Base(p) + rv := reflect.ValueOf(template).MethodByName("Lookup").Call([]reflect.Value{ + reflect.ValueOf(p), + }) + if rv[0].IsNil() { + t.Errorf("missing defined template '%s'", p) + rv = reflect.ValueOf(template).MethodByName("DefinedTemplates").Call([]reflect.Value{}) + t.Log(rv) + } + } + rv = reflect.ValueOf(template).MethodByName("Name").Call([]reflect.Value{}) + if rv[0].String() != filepath.Base(root) { + t.Errorf("invalid template name: %s does not match %s", + rv[0].String(), filepath.Base(root)) + } + } +} + +func TestLoadTemplateFile(t *testing.T) { + var gr, gp, br, bp []string + tdir := t.TempDir() + i := 0 + + gr = append(gr, tdir+"/goodRoot.tmpl") + writeTestFile(t, gr[i], tmplRootGood) + gp = append(gp, tdir+"/goodPartial.gotmpl") + writeTestFile(t, gp[i], tmplPartialGood) + br = append(br, tdir+"/badRoot.tmpl") + writeTestFile(t, br[i], tmplRootBad) + bp = append(bp, tdir+"/badPartial.gotmpl") + writeTestFile(t, bp[i], tmplPartialBad) + i++ + + gr = append(gr, tdir+"/goodRoot.hmpl") + writeTestFile(t, gr[i], hmplRootGood) + gp = append(gp, tdir+"/goodPartial.gohmpl") + writeTestFile(t, gp[i], hmplPartialGood) + br = append(br, tdir+"/badRoot.hmpl") + writeTestFile(t, br[i], hmplRootBad) + bp = append(bp, tdir+"/badPartial.gohmpl") + writeTestFile(t, bp[i], hmplPartialBad) + i++ + + gr = append(gr, tdir+"/goodRoot.mustache") + writeTestFile(t, gr[i], mstRootGood) + gp = append(gp, tdir+"/goodPartial.mst") + writeTestFile(t, gp[i], mstPartialGood) + br = append(br, tdir+"/badRoot.mst") + writeTestFile(t, br[i], mstRootBad) + bp = append(bp, tdir+"/badPartial.mst") + writeTestFile(t, bp[i], mstPartialBad) + + for g, root := range gr { // good root, good partials + if template, e := LoadTemplateFile(root, gp[g]); e != nil { + t.Error(e) + } else { + validateTemplateFile(t, template, root, gp[g]) + } + } + for _, root := range br { // bad root, good partials + if _, e := LoadTemplateFile(root, gp...); e == nil { + t.Errorf("no error for bad template with good partials\n") + } + } + for _, root := range br { // bad root, bad partials + if _, e := LoadTemplateFile(root, bp...); e == nil { + t.Errorf("no error for bad template with bad partials\n") + } + } +} + +func validateExecuteTemplate(t *testing.T, results string, expect string, e error) { + if e != nil { + t.Error(e) + } + if results != expect { + t.Errorf("invalid results: '%s' should match '%s'", results, expect) + } +} + +func TestExecuteTemplate(t *testing.T) { + var e error + var sd, gd, data Data + var d []Data + var tmplr, tmplp, hmplr, hmplp, mstr, mstp string + var tmpl1, tmpl2, hmpl1, hmpl2, mst1, mst2 Template + var results bytes.Buffer + tdir := t.TempDir() + + tmplr = tdir + "/tmplRootGood.gotmpl" + writeTestFile(t, tmplr, tmplRootGood) + tmplp = tdir + "/tmplPartialGood.tmpl" + writeTestFile(t, tmplp, tmplPartialGood) + hmplr = tdir + "/hmplRootGood.gohmpl" + writeTestFile(t, hmplr, hmplRootGood) + hmplp = tdir + "/hmplPartialGood.hmpl" + writeTestFile(t, hmplp, hmplPartialGood) + mstr = tdir + "/mstRootGood.mustache" + writeTestFile(t, mstr, mstRootGood) + mstp = tdir + "/mstPartialGood.mst" + writeTestFile(t, mstp, mstPartialGood) + + if data, e = LoadData("json", strings.NewReader(good["json"])); e != nil { + t.Skip("setup failure:", e) + } + gd = data + if data, e = LoadData("yaml", strings.NewReader(good["yaml"])); e != nil { + t.Skip("setup failure:", e) + } + d = append(d, data) + if data, e = LoadData("toml", strings.NewReader(good["toml"])); e != nil { + t.Skip("setup failure:", e) + } + d = append(d, data) + + if sd, e = GenerateSuperData("", gd, d); e != nil { + t.Skip("setup failure:", e) + } + if tmpl1, e = LoadTemplateFile(tmplr, tmplp); e != nil { + t.Skip("setup failure:", e) + } + if tmpl2, e = LoadTemplateFile(tmplr, tdir); e != nil { + t.Skip("setup failure:", e) + } + if hmpl1, e = LoadTemplateFile(hmplr, hmplp); e != nil { + t.Skip("setup failure:", e) + } + if hmpl2, e = LoadTemplateFile(tmplr, tdir); e != nil { + t.Skip("setup failure:", e) + } + if mst1, e = LoadTemplateFile(mstr, mstp); e != nil { + t.Skip("setup failure:", e) + } + if mst2, e = LoadTemplateFile(tmplr, tdir); e != nil { + t.Skip("setup failure:", e) + } + + results, e = ExecuteTemplate(tmpl1, sd) + validateExecuteTemplate(t, results.String(), tmplResult, e) + results, e = ExecuteTemplate(tmpl2, sd) + validateExecuteTemplate(t, results.String(), tmplResult, e) + + results, e = ExecuteTemplate(hmpl1, sd) + validateExecuteTemplate(t, results.String(), hmplResult, e) + results, e = ExecuteTemplate(hmpl2, sd) + validateExecuteTemplate(t, results.String(), tmplResult, e) + + results, e = ExecuteTemplate(mst1, sd) + validateExecuteTemplate(t, results.String(), mstResult, e) + results, e = ExecuteTemplate(mst2, sd) + validateExecuteTemplate(t, results.String(), mstResult, e) +}