diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2016-01-24 20:07:36 -0500 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2016-05-19 16:38:04 -0400 |
| commit | cb5774a31d2ac883db87f27fe2942b8ea7975b70 (patch) | |
| tree | 29e5a83b603d9f9b0756a048deef1d8af8ea7e5d | |
| parent | cc70da900865809abd2affdd0bbb426bedbbcfde (diff) | |
New datasource function - works for JSON files
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
| -rw-r--r-- | README.md | 41 | ||||
| -rw-r--r-- | data.go | 180 | ||||
| -rw-r--r-- | data_test.go | 96 | ||||
| -rw-r--r-- | main.go | 14 |
4 files changed, 329 insertions, 2 deletions
@@ -24,6 +24,22 @@ $ echo "Hello, {{.Env.USER}}" | gomplate Hello, hairyhenderson ``` +### Commandline Arguments + +#### `--datasource`/`-d` + +Add a data source in `name=URL` form. Specify multiple times to add multiple sources. The data can then be used by the [`datasource`](#datasource) function. + +A few different forms are valid: +- `mydata=file:///tmp/my/file.json` + - Create a data source named `mydata` which is read from `/tmp/my/file.json`. This form is valid for any file in any path. +- `mydata=file.json` + - Create a data source named `mydata` which is read from `file.json` (in the current working directory). This form is only valid for files in the current directory. +- `mydata.json` + - This form infers the name from the file name (without extension). Only valid for files in the current directory. + +## Syntax + #### About `.Env` You can easily access environment variables with `.Env`, but there's a catch: @@ -130,6 +146,31 @@ $ gomplate < input.tmpl Hello world ``` +#### `datasource` + +Parses a given datasource (provided by the [`--datasource/-d`](#--datasource-d) argument). + +Currently, only `file://` URLs are supported, and only the JSON format can be parsed. More support is coming. + +##### Example + +_`person.json`:_ +```json +{ + "name": "Dave" +} +``` + +_`input.tmpl`:_ +``` +Hello {{ (datasource "person").name }} +``` + +```console +$ gomplate -d person.json < input.tmpl +Hello Dave +``` + #### `ec2meta` Queries AWS [EC2 Instance Metadata](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) for information. This only retrieves data in the `meta-data` path -- for data in the `dynamic` path use `ec2dynamic`. diff --git a/data.go b/data.go new file mode 100644 index 00000000..b053faa8 --- /dev/null +++ b/data.go @@ -0,0 +1,180 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "mime" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/blang/vfs" +) + +func init() { + // Add some types we want to be able to handle which can be missing by default + mime.AddExtensionType(".json", "application/json") + mime.AddExtensionType(".yml", "application/yaml") + mime.AddExtensionType(".yaml", "application/yaml") +} + +// Data - +type Data struct { + Sources map[string]*Source + cache map[string][]byte +} + +// NewData - constructor for Data +func NewData(datasourceArgs []string) *Data { + sources := make(map[string]*Source) + for _, v := range datasourceArgs { + s, err := ParseSource(v) + if err != nil { + log.Fatalf("error parsing datasource %v", err) + return nil + } + sources[s.Alias] = s + } + return &Data{ + Sources: sources, + } +} + +// Source - a data source +type Source struct { + Alias string + URL *url.URL + Ext string + Type string + FS vfs.Filesystem +} + +// NewSource - builds a &Source +func NewSource(alias string, URL *url.URL) (s *Source) { + ext := filepath.Ext(URL.Path) + + var t string + if ext != "" { + t = mime.TypeByExtension(ext) + } + + s = &Source{ + Alias: alias, + URL: URL, + Ext: ext, + Type: t, + } + return +} + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (s *Source) String() string { + return fmt.Sprintf("%s=%s (%s)", s.Alias, s.URL.String(), s.Type) +} + +// ParseSource - +func ParseSource(value string) (*Source, error) { + var ( + alias string + srcURL *url.URL + ) + parts := strings.SplitN(value, "=", 2) + if len(parts) == 1 { + f := parts[0] + alias = strings.SplitN(value, ".", 2)[0] + if path.Base(f) != f { + err := fmt.Errorf("Invalid datasource (%s). Must provide an alias with files not in working directory.", value) + return nil, err + } + srcURL = absURL(f) + } else if len(parts) == 2 { + alias = parts[0] + var err error + srcURL, err = url.Parse(parts[1]) + if err != nil { + return nil, err + } + + if !srcURL.IsAbs() { + srcURL = absURL(parts[1]) + } + } + + s := NewSource(alias, srcURL) + return s, nil +} + +func absURL(value string) *url.URL { + cwd, err := os.Getwd() + if err != nil { + log.Fatalf("Can't get working directory: %s", err) + } + baseURL := &url.URL{ + Scheme: "file", + Path: cwd + "/", + } + relURL := &url.URL{ + Path: value, + } + return baseURL.ResolveReference(relURL) +} + +// Datasource - +func (d *Data) Datasource(alias string) map[string]interface{} { + source := d.Sources[alias] + b, err := d.ReadSource(source.FS, source) + if err != nil { + log.Fatalf("Couldn't read datasource '%s': %#v", alias, err) + } + if source.Type == "application/json" { + ty := &TypeConv{} + return ty.JSON(string(b)) + } + log.Fatalf("Datasources of type %s not yet supported", source.Type) + return nil +} + +// ReadSource - +func (d *Data) ReadSource(fs vfs.Filesystem, source *Source) ([]byte, error) { + if d.cache == nil { + d.cache = make(map[string][]byte) + } + cached, ok := d.cache[source.Alias] + if ok { + return cached, nil + } + if source.URL.Scheme == "file" { + if fs == nil { + fs = vfs.OS() + source.FS = fs + } + + // make sure we can access the file + _, err := fs.Stat(source.URL.Path) + if err != nil { + log.Fatalf("Can't stat %s: %#v", source.URL.Path, err) + return nil, err + } + + f, err := fs.OpenFile(source.URL.Path, os.O_RDWR, 0) + if err != nil { + log.Fatalf("Can't open %s: %#v", source.URL.Path, err) + return nil, err + } + + b, err := ioutil.ReadAll(f) + if err != nil { + log.Fatalf("Can't read %s: %#v", source.URL.Path, err) + return nil, err + } + d.cache[source.Alias] = b + return b, nil + } + + log.Fatalf("Datasources with scheme %s not yet supported", source.URL.Scheme) + return nil, nil +} diff --git a/data_test.go b/data_test.go new file mode 100644 index 00000000..be60430c --- /dev/null +++ b/data_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "net/url" + "testing" + + "github.com/blang/vfs" + "github.com/blang/vfs/memfs" + "github.com/stretchr/testify/assert" +) + +func TestNewSource(t *testing.T) { + s := NewSource("foo", &url.URL{ + Scheme: "file", + Path: "/foo.json", + }) + assert.Equal(t, "application/json", s.Type) + assert.Equal(t, ".json", s.Ext) + + s = NewSource("foo", &url.URL{ + Scheme: "http", + Host: "example.com", + Path: "/foo.json", + }) + assert.Equal(t, "application/json", s.Type) + assert.Equal(t, ".json", s.Ext) + + s = NewSource("foo", &url.URL{ + Scheme: "ftp", + Host: "example.com", + Path: "/foo.json", + }) + assert.Equal(t, "application/json", s.Type) + assert.Equal(t, ".json", s.Ext) +} + +func TestParseSourceNoAlias(t *testing.T) { + s, err := ParseSource("foo.json") + assert.NoError(t, err) + assert.Equal(t, "foo", s.Alias) + + _, err = ParseSource("../foo.json") + assert.Error(t, err) + + _, err = ParseSource("ftp://example.com/foo.yml") + assert.Error(t, err) +} + +func TestParseSourceWithAlias(t *testing.T) { + s, err := ParseSource("data=foo.json") + assert.NoError(t, err) + assert.Equal(t, "data", s.Alias) + assert.Equal(t, "file", s.URL.Scheme) + assert.Equal(t, "application/json", s.Type) + assert.True(t, s.URL.IsAbs()) + + s, err = ParseSource("data=/otherdir/foo.json") + assert.NoError(t, err) + assert.Equal(t, "data", s.Alias) + assert.Equal(t, "file", s.URL.Scheme) + assert.True(t, s.URL.IsAbs()) + assert.Equal(t, "/otherdir/foo.json", s.URL.Path) + + s, err = ParseSource("data=sftp://example.com/blahblah/foo.json") + assert.NoError(t, err) + assert.Equal(t, "data", s.Alias) + assert.Equal(t, "sftp", s.URL.Scheme) + assert.True(t, s.URL.IsAbs()) + assert.Equal(t, "/blahblah/foo.json", s.URL.Path) +} + +func TestDatasource(t *testing.T) { + fs := memfs.Create() + fs.Mkdir("/tmp", 0777) + f, _ := vfs.Create(fs, "/tmp/foo.json") + f.Write([]byte(`{"hello":"world"}`)) + + sources := make(map[string]*Source) + sources["foo"] = &Source{ + Alias: "foo", + URL: &url.URL{ + Scheme: "file", + Path: "/tmp/foo.json", + }, + Ext: "json", + Type: "application/json", + FS: fs, + } + data := &Data{ + Sources: sources, + } + expected := make(map[string]interface{}) + expected["hello"] = "world" + actual := data.Datasource("foo") + assert.Equal(t, expected["hello"], actual["hello"]) +} @@ -42,7 +42,7 @@ func (g *Gomplate) RunTemplate(in io.Reader, out io.Writer) { } // NewGomplate - -func NewGomplate() *Gomplate { +func NewGomplate(data *Data) *Gomplate { env := &Env{} typeconv := &TypeConv{} ec2meta := &aws.Ec2Meta{} @@ -62,12 +62,15 @@ func NewGomplate() *Gomplate { "title": strings.Title, "toUpper": strings.ToUpper, "toLower": strings.ToLower, + "datasource": data.Datasource, }, } } func runTemplate(c *cli.Context) error { - g := NewGomplate() + data := NewData(c.StringSlice("datasource")) + + g := NewGomplate(data) g.RunTemplate(os.Stdin, os.Stdout) return nil } @@ -79,5 +82,12 @@ func main() { app.Version = version.Version app.Action = runTemplate + app.Flags = []cli.Flag{ + cli.StringSliceFlag{ + Name: "datasource, d", + Usage: "Data source in alias=URL form. Specify multiple times to add multiple sources.", + }, + } + app.Run(os.Args) } |
