Skip to content

Commit

Permalink
feat: support multipart/multidoc JSON/YAML data (#1)
Browse files Browse the repository at this point in the history
* wip: multipart YAML/JSON support

* wip: tests for multipart

* wip: move multipart logic to timplit package

* wip: use a writer for multipart output

* wip: remove unused multipart.json test file

* wip: clarify flag logic in command

* wip: reader-based exported methods

* wip: use yaml decoder, improve tests

* wip: use yaml v3 library

* wip: Timplit struct, refine public api

* fix: normalize multidoc YAML execution func

* feat: version bump v0.2.0
  • Loading branch information
tnyeanderson committed Aug 20, 2023
1 parent 7d4de99 commit 3557ca8
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 95 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ timplit -y data.yaml <template.tmpl

# Read YAML from stdin and apply it to a template file
timplit -Y template.tmpl <data.yaml

# Apply template for each line of JSON
tail -f /some/log.json | timplit -m template.tmpl

# Apply template for each YAML document (separated by "---")
# Multidoc is always supported for YAML (no need for "-m")
tail -f deployment.yaml | timplit -Y template.tmpl
```

Example JSON/YAML/template files are available in `cmd/testdata`.
Expand Down
7 changes: 7 additions & 0 deletions cmd/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ Examples:
# Read YAML from stdin and apply it to a template file
timplit -Y template.tmpl <data.yaml
# Apply template for each line of JSON
tail -f /some/log.json | timplit -m template.tmpl
# Apply template for each YAML document (separated by "---")
# Multidoc is always supported for YAML (no need for "-m")
tail -f deployment.yaml | timplit -Y template.tmpl
`
69 changes: 35 additions & 34 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cmd

import (
"fmt"
"io"
"os"

"github.com/spf13/cobra"
Expand All @@ -13,21 +12,27 @@ var jsonFile string
var templateFile string
var yamlFile string
var yamlStdin bool
var multipart bool
var ignoreErrors bool

func NewRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "timplit",
Short: "A minimal CLI for go templates",
Long: help,
Version: "v0.1.1",
RunE: rootCmdRunE,
Use: "timplit",
Short: "A minimal CLI for go templates",
Long: help,
Version: "v0.2.0",
RunE: rootCmdRunE,
SilenceUsage: true,
}
cmd.PersistentFlags().StringVarP(&templateFile, "template", "t", "", "path to template file")
cmd.PersistentFlags().StringVarP(&jsonFile, "json", "j", "", "path to JSON file")
cmd.PersistentFlags().StringVarP(&yamlFile, "yaml", "y", "", "path to YAML file")
cmd.PersistentFlags().BoolVarP(&yamlStdin, "yaml-stdin", "Y", false, "parse stdin as YAML")
cmd.PersistentFlags().BoolVarP(&multipart, "multipart", "m", false, "render a template for each line of JSON from stdin")
cmd.PersistentFlags().BoolVarP(&ignoreErrors, "ignore", "i", false, "don't exit if one part of a multipart document is invalid")
cmd.MarkFlagsMutuallyExclusive("json", "yaml", "yaml-stdin")
cmd.MarkFlagsMutuallyExclusive("json", "yaml", "template")
cmd.MarkFlagsMutuallyExclusive("json", "yaml", "yaml-stdin", "multipart")
return cmd
}

Expand All @@ -43,49 +48,45 @@ func rootCmdRunE(cmd *cobra.Command, args []string) error {

// Don't wait forever if there is no path, like if the command was run with no args
if path == "" {
cmd.Usage()
return fmt.Errorf("a path must be provided")
}

// Read the file, which could contain JSON, YAML, or a template
raw, err := os.ReadFile(path)
file, err := os.Open(path)
if err != nil {
return err
}

// Read stdin
stdin, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return err
stdin := cmd.InOrStdin()

// Assume that data is coming from stdin
t := timplit.Timplit{
Template: file,
Data: stdin,
Out: cmd.OutOrStdout(),
Err: cmd.ErrOrStderr(),
IgnoreParseErrors: ignoreErrors,
}

// Set these here so they can be shared by the below if/else block
var out string
var e error
// If data is coming from a file, switch Template and Data
if yamlFile != "" || jsonFile != "" {
t.Template = stdin
t.Data = file
}

// Depending on the flags, execute the template with the data. Done this
// way to prevent repetitive error checking.
if jsonFile != "" {
// Use JSON file
out, e = timplit.ExecuteJson(stdin, raw)
} else if yamlFile != "" {
// Use YAML file
out, e = timplit.ExecuteYaml(stdin, raw)
} else if yamlStdin {
// Use stdin as YAML
out, e = timplit.ExecuteYaml(raw, stdin)
} else {
// Use stdin as JSON
out, e = timplit.ExecuteJson(raw, stdin)
// YAML
if yamlStdin || yamlFile != "" {
return t.YAML()
}

// Handle errors when executing the template
if e != nil {
return e
// Multipart
if multipart {
return t.MultipartJSON()
}

// Print the output
fmt.Print(out)
return nil
// JSON
return t.JSON()
}

func Execute() {
Expand Down
165 changes: 159 additions & 6 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,171 @@ import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
)

func mockUsage(cmd *cobra.Command) error {
fmt.Println("<mock usage>")
return nil
}

func ExampleTimplitNoPath() {
// Run the command with no args
// Run the command with no args.
// This should print the usage and an error.
r := NewRootCmd()
r.SetUsageFunc(mockUsage)
r.SetErr(r.OutOrStdout())
r.Execute()

// Output:
// <mock usage>
// Error: a path must be provided
}

func ExampleTimplitJsonMultipart() {
// Empty lines should be ignored
data := `{"name": "Finn", "role": "hero"}
{"name": "Jake", "role": "dog"}
{"name": "Bubblegum", "role": "princess"}`

templateFile := "testdata/multipart.tmpl"
stdin := strings.NewReader(data)
r := NewRootCmd()
r.SetArgs([]string{"-m", templateFile})
r.SetIn(stdin)
r.Execute()

// Output:
// Finn is a hero
// Jake is a dog
// Bubblegum is a princess
}

func ExampleTimplitJsonMultipartInvalid() {
// Empty lines should be ignored
data := `{"name": "Finn", "role": "hero"}
{"name": "Jake", "role": "dog"}
invalid
{"name": "Bubblegum", "role": "princess"}`

templateFile := "testdata/multipart.tmpl"
stdin := strings.NewReader(data)
r := NewRootCmd()
r.SetArgs([]string{"-m", templateFile})
r.SetIn(stdin)
r.SetErr(r.OutOrStdout())
r.Execute()

// Output:
// Finn is a hero
// Jake is a dog
// Error: invalid character 'i' looking for beginning of value
}

func ExampleTimplitJsonMultipartInvalidIgnoreErrors() {
// Empty lines should be ignored
data := `{"name": "Finn", "role": "hero"}
{"name": "Jake", "role": "dog"}
invalid
{"name": "Bubblegum", "role": "princess"}`

templateFile := "testdata/multipart.tmpl"
stdin := strings.NewReader(data)
r := NewRootCmd()
r.SetArgs([]string{"-i", "-m", templateFile})
r.SetIn(stdin)
r.SetErr(r.OutOrStdout())
r.Execute()

// Output:
// Finn is a hero
// Jake is a dog
// Error: invalid character 'i' looking for beginning of value
// Bubblegum is a princess
}

func ExampleTimplitYamlMultipart() {
data := `
---
name: Finn
role: hero
---
name: "Jake"
role: dog
---
name: Bubblegum
role: princess
`

templateFile := "testdata/multipart.tmpl"
stdin := strings.NewReader(data)
r := NewRootCmd()
r.SetOut(&strings.Builder{})
r.SetErr(&strings.Builder{})
err := r.Execute()
fmt.Println(err)
r.SetArgs([]string{"-Y", templateFile})
r.SetIn(stdin)
r.Execute()

// Output:
// Finn is a hero
// Jake is a dog
// Bubblegum is a princess
}

func ExampleTimplitYamlMultipartInvalid() {
data := `
---
name: Finn
role: hero
---
name: {invalid}value
role: dog
---
name: Bubblegum
role: princess
`

templateFile := "testdata/multipart.tmpl"
stdin := strings.NewReader(data)
r := NewRootCmd()
r.SetArgs([]string{"-Y", templateFile})
r.SetIn(stdin)
r.SetErr(r.OutOrStdout())
r.Execute()

// Output:
// Finn is a hero
// Error: yaml: did not find expected key
}

func ExampleTimplitYamlMultipartInvalidIgnoreErrors() {
data := `
---
name: Finn
role: hero
---
name: {invalid}value
role: dog
---
name: Bubblegum
role: princess
`

templateFile := "testdata/multipart.tmpl"
stdin := strings.NewReader(data)
r := NewRootCmd()
r.SetArgs([]string{"-i", "-Y", templateFile})
r.SetIn(stdin)
r.SetErr(r.OutOrStdout())
r.Execute()

// Output:
// a path must be provided
// Finn is a hero
// Error: yaml: did not find expected key
// Bubblegum is a princess
}

func ExampleTimplitJson() {
Expand Down
1 change: 1 addition & 0 deletions cmd/testdata/multipart.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{.name}} is a {{.role}}
10 changes: 4 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@ go 1.18

require (
github.com/Masterminds/sprig v2.22.0+incompatible
gopkg.in/yaml.v2 v2.3.0
github.com/spf13/cobra v1.6.1
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.5.1 // indirect
golang.org/x/crypto v0.3.0 // indirect
)
Loading

0 comments on commit 3557ca8

Please sign in to comment.