commit f4477f9edeec387c38041fbf9d27bd4f29309bd9
parent 02fbd1aa507bd89f037f130bce7ae5d45053ed47
Author: gearsix <gearsix@tuta.io>
Date: Sun, 21 Mar 2021 17:22:11 +0000
standardized pkg layout; bugfixes & refactors
Diffstat:
A | cmd/suti.go | | | 185 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | data.go | | | 255 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | data_test.go | | | 227 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
R | doc/suti.txt -> docs/suti.txt | | | 0 | |
M | go.mod | | | 4 | ---- |
M | go.sum | | | 24 | +++++++++++++++++------- |
D | src/data.go | | | 256 | ------------------------------------------------------------------------------- |
D | src/data_test.go | | | 212 | ------------------------------------------------------------------------------- |
D | src/suti.go | | | 163 | ------------------------------------------------------------------------------- |
D | src/template.go | | | 175 | ------------------------------------------------------------------------------- |
D | src/template_test.go | | | 213 | ------------------------------------------------------------------------------- |
A | template.go | | | 176 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | template_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)
+}