dati

data and template interface
git clone git://git.gearsix.net/dati
Log | Files | Refs | Atom | README | LICENSE

commit 912752f46b6075653e1e356845eee1aeeb5e52df
parent c92784b306e68ebb5ff5722e9f52e4cf65a39035
Author: gearsix <gearsix@tuta.io>
Date:   Fri, 18 Mar 2022 15:00:03 +0000

REBRAND to 'dati - data and template interface'

Diffstat:
MCHANGELOG | 4++++
MREADME | 33+++++++++++++++++----------------
Acmd/dati.go | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/dati_test.sh | 21+++++++++++++++++++++
Dcmd/suti.go | 295-------------------------------------------------------------------------------
Dcmd/suti_test.sh | 21---------------------
Mdata.go | 2+-
Mdata_test.go | 2+-
Mexamples/README | 2+-
Mfile.go | 2+-
Mfile_test.go | 2+-
Mgo.mod | 2+-
Mtemplate.go | 4++--
Mtemplate_test.go | 2+-
14 files changed, 346 insertions(+), 341 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,5 +1,9 @@ # CHANGELOG +## v0.8.0 + +- rebranded to dati - data and template interface + ## v0.7.0 - added LoadTemplate, loads templates from io.Reader params. diff --git a/README b/README @@ -1,29 +1,30 @@ -suti + +dati ==== -simple unified templating interface +data and template interface USAGE ----- - suti [OPTIONS] + dati [OPTIONS] DESCRIPTION ----------- - suti aims to provide a universal interface for executing data files, + dati aims to provide a universal interface for executing data files, written in any data-serialization language, against template files, written in any templating languages. - Ideally suti will support any language you want to use. + Ideally dati will support any language you want to use. - suti works by using various libraries that do all the hard work to + dati works by using various libraries that do all the hard work to parse data and template files passed to it. It generates a data structure of all the passed data files combined (a super-data structure) and executes that structure against a set of root template files. The used libraries are listed below for credit/reference. - suti can also be imported as a golang package to be used as a library. + dati can also be imported as a golang package to be used as a library. OPTIONS ------- @@ -54,7 +55,7 @@ OPTIONS CONFIG ------ - It's possible you'll want to set the same options if you run suti multiple + It's possible you'll want to set the same options if you run dati multiple times for the same project. This can be done by creating a file (written as a data file) and passing the filepath to the -cfg argument. @@ -71,10 +72,10 @@ CONFIG DATA ---- - suti generates a single super-structure of all the data files passed to it. + dati generates a single super-structure of all the data files passed to it. This super-structure is executed against each "root" template. - The super-structure generated by suti will only have 1 definite key: "data" + The super-structure generated by dati will only have 1 definite key: "data" (or the value of the "data-key" option). This key will overwrite any "global data" keys in the root of the super-structure. Its value will be an array, where each element is the resulting data structure of each parsed "data" @@ -88,9 +89,9 @@ DATA TEMPLATES --------- - All "root" template files passed to suti that have a file extension matching + All "root" template files passed to dati that have a file extension matching one of the supported templating languages will be parsed and executed - against the super-structure generated by suti. + against the super-structure generated by dati. All "parital" templates will be parsed into any "root" templates that have a file extension that match the same templating language. @@ -119,16 +120,16 @@ SUPPORTED LANGUAGES EXAMPLES -------- - suti -cfg ./suti.cfg -r templates/textfile.mst + dati -cfg ./dati.cfg -r templates/textfile.mst - suti -r homepage.hmpl -p head.hmpl -p body.hmpl -gd meta.json -d posts/* + dati -r homepage.hmpl -p head.hmpl -p body.hmpl -gd meta.json -d posts/* - see the examples/ directory in the suti repository for a cool example. + see the examples/ directory in the dati repository for a cool example. LIBRARIES --------- - As stated above, all of these libraries do the hard work, suti just combines + As stated above, all of these libraries do the hard work, dati just combines it all together - so thanks to the authors. Also here for reference. - The Go standard library is used for parsing JSON, .tmpl/.gotmpl, .hmpl/.gohmpl diff --git a/cmd/dati.go b/cmd/dati.go @@ -0,0 +1,295 @@ +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" + "bytes" + "fmt" + "notabug.org/gearsix/dati" + "os" + "path/filepath" + "strings" +) + +// Data is just a generic map for key/value data +type Data map[string]interface{} + +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() { + var err error + var global Data + var data []Data + var template dati.Template + var out bytes.Buffer + + opts.GlobalDataPaths = loadFilePaths(opts.GlobalDataPaths...) + for _, path := range opts.GlobalDataPaths { + var d Data + err = dati.LoadDataFilepath(path, &d) + assert(err, "failed to load global data '%s'", path) + data = append(data, d) + } + global = mergeData(data) + + opts.DataPaths = loadFilePaths(opts.DataPaths...) + opts.DataPaths, err = dati.SortFileList(opts.DataPaths, opts.SortData) + if err != nil { + warn(err, "failed to sort data files") + } + data = make([]Data, 0) + for _, path := range opts.DataPaths { + var d Data + err = dati.LoadDataFilepath(path, &d) + assert(err, "failed to load data '%s'", path) + data = append(data, d) + } + global[opts.DataKey] = data + + template, err = dati.LoadTemplateFilepath(opts.RootPath, opts.PartialPaths...) + assert(err, "unable to load templates") + + out, err = template.Execute(global) + assert(err, "failed to execute template '%s'", opts.RootPath) + fmt.Print(out.String()) + + return +} + +func help() { + fmt.Print("Usage: dati [OPTIONS]\n\n") + + fmt.Print("Options") + fmt.Print(` + -r path, -root path + path of template file to execute against. + + -p path..., -partial path... + path of (multiple) template files that are called upon by at least one + root template. If a directory is passed then all files within that + directory will (recursively) be loaded. + + -gd path..., -global-data path... + path of (multiple) data files to load as "global data". If a directory is + passed then all files within that directory will (recursively) be loaded. + + -d path..., -data path... + path of (multiple) data files to load as "data". If a directory is passed + then all files within that directory will (recursively) be loaded. + + -dk name, -data-key name + set the name of the key used for the generated array of data (default: + "data") + + -sd attribute, -sort-data attribute + The file attribute to order data files by. If no value is provided, the data + will be provided in the order it's loaded. + Accepted values: "filename", "modified". + A suffix can be appended to each value to set the sort order: "-asc" (for + ascending), "-desc" (for descending). If not specified, this defaults to + "-asc". + -cfg file, -config file + A data file to provide default values for the above options (see CONFIG). + +`) + + fmt.Println("See doc/dati.txt for further details") +} + +// 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 = "" + } + + if strings.Contains(flag, "=") { + split := strings.SplitN(flag, "=", 2) + flag = split[0] + args[a] = split[1] + a-- + } + + // set valid any flags that don't take arguments here + if flag == "h" || flag == "help" { + help() + os.Exit(0) + } + } 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) + } + 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 +} + +// load glob & dir filepaths as individual filepaths +func loadFilePaths(paths ...string) (filepaths []string) { + for _, path := range paths { + var err error + if strings.Contains(path, "*") { + var glob []string + glob, err = filepath.Glob(path) + assert(err, "failed to glob '%s'", path) + for _, p := range glob { + filepaths = append(filepaths, p) + } + } else { + err = filepath.Walk(path, + func(p string, info os.FileInfo, e error) error { + if e == nil && !info.IsDir() { + filepaths = append(filepaths, p) + } + return e + }) + } + if err != nil { + assert(err, "failed to load filepaths for '%s'", path) + } + } + return +} + +func mergeData(data []Data) (merged Data) { + merged = make(Data) + for _, d := range data { + for key, val := range d { + if merged[key] == nil { + merged[key] = val + } else { + warn(nil, "merge conflict for global data key: '%s'", key) + } + } + } + return +} diff --git a/cmd/dati_test.sh b/cmd/dati_test.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh +# if nothing prints, the test passed + +diff="diff -bs" +fail=0 + +go build -o dati dati.go + +if [ -e dati ]; then + ./dati -cfg ../examples/dati.cfg -r ../examples/template/html.hmpl > out.html + $diff out.html ../examples/out.html + if [ $? -ne 0 ]; then fail=1; else rm out.html; fi + + ./dati -cfg ../examples/dati.cfg -r ../examples/template/txt.mst > out.txt + $diff out.txt ../examples/out.txt + if [ $? -ne 0 ]; then fail=1; else rm out.txt; fi + + rm dati + + if [ $fail -eq 1 ]; then echo "TEST FAIL"; else echo "TEST PASS"; fi +fi diff --git a/cmd/suti.go b/cmd/suti.go @@ -1,295 +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" - "bytes" - "fmt" - "notabug.org/gearsix/suti" - "os" - "path/filepath" - "strings" -) - -// Data is just a generic map for key/value data -type Data map[string]interface{} - -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() { - var err error - var global Data - var data []Data - var template suti.Template - var out bytes.Buffer - - opts.GlobalDataPaths = loadFilePaths(opts.GlobalDataPaths...) - for _, path := range opts.GlobalDataPaths { - var d Data - err = suti.LoadDataFilepath(path, &d) - assert(err, "failed to load global data '%s'", path) - data = append(data, d) - } - global = mergeData(data) - - opts.DataPaths = loadFilePaths(opts.DataPaths...) - opts.DataPaths, err = suti.SortFileList(opts.DataPaths, opts.SortData) - if err != nil { - warn(err, "failed to sort data files") - } - data = make([]Data, 0) - for _, path := range opts.DataPaths { - var d Data - err = suti.LoadDataFilepath(path, &d) - assert(err, "failed to load data '%s'", path) - data = append(data, d) - } - global[opts.DataKey] = data - - template, err = suti.LoadTemplateFilepath(opts.RootPath, opts.PartialPaths...) - assert(err, "unable to load templates") - - out, err = template.Execute(global) - assert(err, "failed to execute template '%s'", opts.RootPath) - fmt.Print(out.String()) - - return -} - -func help() { - fmt.Print("Usage: suti [OPTIONS]\n\n") - - fmt.Print("Options") - fmt.Print(` - -r path, -root path - path of template file to execute against. - - -p path..., -partial path... - path of (multiple) template files that are called upon by at least one - root template. If a directory is passed then all files within that - directory will (recursively) be loaded. - - -gd path..., -global-data path... - path of (multiple) data files to load as "global data". If a directory is - passed then all files within that directory will (recursively) be loaded. - - -d path..., -data path... - path of (multiple) data files to load as "data". If a directory is passed - then all files within that directory will (recursively) be loaded. - - -dk name, -data-key name - set the name of the key used for the generated array of data (default: - "data") - - -sd attribute, -sort-data attribute - The file attribute to order data files by. If no value is provided, the data - will be provided in the order it's loaded. - Accepted values: "filename", "modified". - A suffix can be appended to each value to set the sort order: "-asc" (for - ascending), "-desc" (for descending). If not specified, this defaults to - "-asc". - -cfg file, -config file - A data file to provide default values for the above options (see CONFIG). - -`) - - fmt.Println("See doc/suti.txt for further details") -} - -// 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 = "" - } - - if strings.Contains(flag, "=") { - split := strings.SplitN(flag, "=", 2) - flag = split[0] - args[a] = split[1] - a-- - } - - // set valid any flags that don't take arguments here - if flag == "h" || flag == "help" { - help() - os.Exit(0) - } - } 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) - } - 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 -} - -// load glob & dir filepaths as individual filepaths -func loadFilePaths(paths ...string) (filepaths []string) { - for _, path := range paths { - var err error - if strings.Contains(path, "*") { - var glob []string - glob, err = filepath.Glob(path) - assert(err, "failed to glob '%s'", path) - for _, p := range glob { - filepaths = append(filepaths, p) - } - } else { - err = filepath.Walk(path, - func(p string, info os.FileInfo, e error) error { - if e == nil && !info.IsDir() { - filepaths = append(filepaths, p) - } - return e - }) - } - if err != nil { - assert(err, "failed to load filepaths for '%s'", path) - } - } - return -} - -func mergeData(data []Data) (merged Data) { - merged = make(Data) - for _, d := range data { - for key, val := range d { - if merged[key] == nil { - merged[key] = val - } else { - warn(nil, "merge conflict for global data key: '%s'", key) - } - } - } - return -} diff --git a/cmd/suti_test.sh b/cmd/suti_test.sh @@ -1,21 +0,0 @@ -#!/bin/sh -# if nothing prints, the test passed - -diff="diff -bs" -fail=0 - -go build -o suti suti.go - -if [ -e suti ]; then - ./suti -cfg ../examples/suti.cfg -r ../examples/template/html.hmpl > out.html - $diff out.html ../examples/out.html - if [ $? -ne 0 ]; then fail=1; else rm out.html; fi - - ./suti -cfg ../examples/suti.cfg -r ../examples/template/txt.mst > out.txt - $diff out.txt ../examples/out.txt - if [ $? -ne 0 ]; then fail=1; else rm out.txt; fi - - rm suti - - if [ $fail -eq 1 ]; then echo "TEST FAIL"; else echo "TEST PASS"; fi -fi diff --git a/data.go b/data.go @@ -1,4 +1,4 @@ -package suti +package dati /* Copyright (C) 2021 gearsix <gearsix@tuta.io> diff --git a/data_test.go b/data_test.go @@ -1,4 +1,4 @@ -package suti +package dati /* Copyright (C) 2021 gearsix <gearsix@tuta.io> diff --git a/examples/README b/examples/README @@ -6,7 +6,7 @@ denoted by the filename (e.g. html.tmpl produces a html file). An example of how you can run one of these examples: - suti -cfg suti.cfg -r templates/html.tmpl > out.html + dati -cfg dati.cfg -r templates/html.tmpl > out.html Then open out.html to see the results. diff --git a/file.go b/file.go @@ -1,4 +1,4 @@ -package suti +package dati /* Copyright (C) 2021 gearsix <gearsix@tuta.io> diff --git a/file_test.go b/file_test.go @@ -1,4 +1,4 @@ -package suti +package dati import ( "os" diff --git a/go.mod b/go.mod @@ -1,4 +1,4 @@ -module notabug.org/gearsix/suti +module notabug.org/gearsix/dati go 1.13 diff --git a/template.go b/template.go @@ -1,4 +1,4 @@ -package suti +package dati /* Copyright (C) 2021 gearsix <gearsix@tuta.io> @@ -54,7 +54,7 @@ func getTemplateType(path string) string { return strings.TrimPrefix(filepath.Ext(path), ".") } -// Template is a wrapper to interface with any template parsed by suti. +// Template is a wrapper to interface with any template parsed by dati. // Ideally it would have just been an interface{} that defines Execute but // the libaries being used aren't that uniform. type Template struct { diff --git a/template_test.go b/template_test.go @@ -1,4 +1,4 @@ -package suti +package dati /* Copyright (C) 2021 gearsix <gearsix@tuta.io>