Repository: TomWright/dasel
Pull request #231 (2022-06-26 13:04:29)

Add validate command


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 725ead1..817004f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Support for struct type usage in go package.
+- Validate command.
 
 ## [v1.24.3] - 2022-04-23
 
diff --git a/internal/command/root.go b/internal/command/root.go
index 109b1a3..fafabe6 100644
--- a/internal/command/root.go
+++ b/internal/command/root.go
@@ -21,6 +21,7 @@ func NewRootCMD() *cobra.Command {
 		putCommand(),
 		deleteCommand(),
 		updateCommand(selfupdate.NewUpdater(internal.Version)),
+		validateCommand(),
 	)
 	return cmd
 }
diff --git a/internal/command/validate.go b/internal/command/validate.go
new file mode 100644
index 0000000..9c2ec42
--- /dev/null
+++ b/internal/command/validate.go
@@ -0,0 +1,147 @@
+package command
+
+import (
+	"fmt"
+	"github.com/spf13/cobra"
+	"io"
+	"path/filepath"
+	"sync"
+)
+
+type validationFile struct {
+	File   string
+	Parser string
+}
+
+type validationFileResult struct {
+	File  validationFile
+	Pass  bool
+	Error error
+}
+
+type validateOptions struct {
+	Files        []validationFile
+	Reader       io.Reader
+	Writer       io.Writer
+	IncludeError bool
+}
+
+func runValidateCommand(opts validateOptions, cmd *cobra.Command) error {
+	fileCount := len(opts.Files)
+
+	wg := &sync.WaitGroup{}
+	wg.Add(fileCount)
+
+	results := make([]validationFileResult, fileCount)
+
+	for i, f := range opts.Files {
+		index := i
+		file := f
+		go func() {
+			defer wg.Done()
+
+			pass, err := validateFile(file)
+			results[index] = validationFileResult{
+				File:  file,
+				Pass:  pass,
+				Error: err,
+			}
+		}()
+	}
+
+	wg.Wait()
+
+	failureCount := 0
+	for _, result := range results {
+		if !result.Pass {
+			failureCount++
+		}
+	}
+
+	// Set up our output writer if one wasn't provided.
+	if opts.Writer == nil {
+		if failureCount > 0 {
+			opts.Writer = cmd.OutOrStderr()
+		} else {
+			opts.Writer = cmd.OutOrStdout()
+		}
+	}
+
+	for _, result := range results {
+		outputString := ""
+
+		if result.Pass {
+			outputString += "pass"
+		} else {
+			outputString += "fail"
+		}
+
+		outputString += " " + result.File.File
+
+		if opts.IncludeError && result.Error != nil {
+			outputString += " " + result.Error.Error()
+		}
+
+		if _, err := fmt.Fprintln(opts.Writer, outputString); err != nil {
+			return fmt.Errorf("could not write output: %w", err)
+		}
+	}
+
+	if failureCount > 0 {
+		return fmt.Errorf("%d files failed validation", failureCount)
+	}
+	return nil
+}
+
+func validateFile(file validationFile) (bool, error) {
+	readParser, err := getReadParser(file.File, file.Parser, "")
+	if err != nil {
+		return false, err
+	}
+	_, err = getRootNode(getRootNodeOpts{
+		File:   file.File,
+		Parser: readParser,
+		Reader: nil,
+	}, nil)
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+func validateCommand() *cobra.Command {
+	var includeErrorFlag bool
+
+	cmd := &cobra.Command{
+		Use:   "validate <file> <file> <file>",
+		Short: "Validate a list of files.",
+		RunE: func(cmd *cobra.Command, args []string) error {
+			files := make([]validationFile, 0)
+			for _, a := range args {
+				matches, err := filepath.Glob(a)
+				if err != nil {
+					return err
+				}
+
+				for _, m := range matches {
+					files = append(files, validationFile{
+						File:   m,
+						Parser: "",
+					})
+				}
+			}
+
+			return runValidateCommand(validateOptions{
+				Files:        files,
+				IncludeError: includeErrorFlag,
+			}, cmd)
+		},
+	}
+
+	cmd.Flags().BoolVar(&includeErrorFlag, "include-error", true, "Show error/validation information")
+
+	_ = cmd.MarkFlagFilename("file")
+
+	return cmd
+}


Test patch:
diff --git a/internal/command/root_test.go b/internal/command/root_test.go
index ed5dd6b..ffad2d9 100644
--- a/internal/command/root_test.go
+++ b/internal/command/root_test.go
@@ -114,3 +114,29 @@ func expectOutput(in string, args []string, exp string) func(t *testing.T) {
 		}
 	}
 }
+
+func expectOutputAndErr(args []string, expErr string, expOutput string) func(t *testing.T) {
+	return func(t *testing.T) {
+		cmd := command.NewRootCMD()
+		outputBuffer := bytes.NewBuffer([]byte{})
+
+		cmd.SetOut(outputBuffer)
+		cmd.SetArgs(args)
+
+		err := cmd.Execute()
+
+		gotErr := ""
+		if err != nil {
+			gotErr = err.Error()
+		}
+
+		if expErr != gotErr {
+			t.Errorf("expected err %s, got %s", expErr, gotErr)
+		}
+
+		gotOutput := outputBuffer.String()
+		if expOutput != gotOutput {
+			t.Errorf("expected:\n%s\ngot:\n%s", expOutput, gotOutput)
+		}
+	}
+}
diff --git a/internal/command/root_validate_test.go b/internal/command/root_validate_test.go
new file mode 100644
index 0000000..0ddc4b8
--- /dev/null
+++ b/internal/command/root_validate_test.go
@@ -0,0 +1,55 @@
+package command_test
+
+import (
+	"testing"
+)
+
+func TestRootCMD_Validate(t *testing.T) {
+	t.Run("AllPass", expectOutputAndErr(
+		[]string{"validate", "./../../tests/assets/example.json", "./../../tests/assets/example.yaml"},
+		"",
+		`pass ./../../tests/assets/example.json
+pass ./../../tests/assets/example.yaml
+`,
+	))
+
+	t.Run("PartialFail", expectOutputAndErr(
+		[]string{"validate", "./../../tests/assets/example.json", "./../../tests/assets/example.yaml", "./../../tests/assets/broken.json", "./../../tests/assets/broken.xml"},
+		"2 files failed validation",
+		`pass ./../../tests/assets/example.json
+pass ./../../tests/assets/example.yaml
+fail ./../../tests/assets/broken.json could not load input: could not unmarshal data: invalid character '}' after array element
+fail ./../../tests/assets/broken.xml could not load input: could not unmarshal data: xml.Decoder.Token() - XML syntax error on line 1: element <a> closed by </b>
+`,
+	))
+
+	t.Run("PartialGlob", expectOutputAndErr(
+		[]string{"validate", "./../../tests/assets/*.json"},
+		"1 files failed validation",
+		`fail ../../tests/assets/broken.json could not load input: could not unmarshal data: invalid character '}' after array element
+pass ../../tests/assets/example.json
+`,
+	))
+
+	t.Run("AllFail", expectOutputAndErr(
+		[]string{"validate", "./../../tests/assets/broken.json", "./../../tests/assets/broken.xml"},
+		"2 files failed validation",
+		`fail ./../../tests/assets/broken.json could not load input: could not unmarshal data: invalid character '}' after array element
+fail ./../../tests/assets/broken.xml could not load input: could not unmarshal data: xml.Decoder.Token() - XML syntax error on line 1: element <a> closed by </b>
+`,
+	))
+
+	t.Run("AllFailNoErrors", expectOutputAndErr(
+		[]string{"validate", "--include-error=false", "./../../tests/assets/broken.json", "./../../tests/assets/broken.xml"},
+		"2 files failed validation",
+		`fail ./../../tests/assets/broken.json
+fail ./../../tests/assets/broken.xml
+`,
+	))
+
+	t.Run("NoFilesPass", expectOutputAndErr(
+		[]string{"validate"},
+		"",
+		``,
+	))
+}
diff --git a/tests/assets/broken.json b/tests/assets/broken.json
new file mode 100644
index 0000000..0c6ec1f
--- /dev/null
+++ b/tests/assets/broken.json
@@ -0,0 +1,9 @@
+{
+  "name": "Tom",
+  "preferences": {
+    "favouriteColour": "red"
+  },
+  "colours": [
+    "red",
+    "green"
+}
\ No newline at end of file
diff --git a/tests/assets/broken.xml b/tests/assets/broken.xml
new file mode 100644
index 0000000..a0b0dba
--- /dev/null
+++ b/tests/assets/broken.xml
@@ -0,0 +1,1 @@
+<a></b>
\ No newline at end of file