Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- JSON formatting functionality with `--check-format` flag
- CLI flag `--check-format` to enable formatting check of valid config files, only JSON is supported
- Checks JSON format with consistent 2-space indentation
- Added a new interface method `ValidateFormat` for format validation
- CHANGELOG.md file
- Github action to verify that changelog was changed for each PR
- CODEOWNERS file
- Support for TOON validation

### Changed

- Interface method name change
- `Validate` interface renamed to `ValidateSyntax`
- Test examples for good JSON config files were updated to have consistent 2-space indentation
- Build instructions for MacOS were updated to default to arm64

### Fixed
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ optional flags:
Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports
-quiet
If quiet flag is set. It doesn't print any output to stdout.
-check-format string
If check format flag is set, it will attempt to check the format of the provided file types. Currenly supported file types are json. Provide a comma separated list of file types or "all" to format all supported file types.
-reporter value
A string representing report format and optional output file path separated by colon if present.
Usage: --reporter <format>:<optional_file_path>
Expand All @@ -158,6 +160,7 @@ The config-file-validator supports setting options via environment variables. If
| `CFV_REPORTER` | `-reporter` |
| `CFV_GROUPBY` | `-groupby` |
| `CFV_QUIET` | `-quiet` |
| `CFV_FORMAT` | `-format` |
| `CFV_GLOBBING` | `-globbing` |

### Examples
Expand Down Expand Up @@ -259,6 +262,14 @@ Passing the `--quiet` flag suppresses all output to stdout. If there are invalid
validator --quiet /path/to/search
```

### Check format of valid files
Use the `-check-format` flag to check the format of valid files.

```shell
validator -check-format /path/to/search
```
> Only JSON files are formatted currently.

### Search files using a glob pattern

Use the `-globbing` flag to validate files matching a specified pattern. Include the pattern as a positional argument in double quotes. Multiple glob patterns and direct file paths are supported. If invalid config files are detected, the validator tool exits with code 1, and errors (e.g., invalid patterns) are displayed.
Expand Down
41 changes: 41 additions & 0 deletions cmd/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type validatorConfig struct {
groupOutput *string
quiet *bool
globbing *bool
format *string
}

type reporterFlags []string
Expand Down Expand Up @@ -129,6 +130,7 @@ func getFlags() (validatorConfig, error) {
groupOutputPtr = flag.String("groupby", "", "Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports")
quietPtr = flag.Bool("quiet", false, "If quiet flag is set. It doesn't print any output to stdout.")
globbingPrt = flag.Bool("globbing", false, "If globbing flag is set, check for glob patterns in the arguments.")
formatPtr = flag.String("check-format", "", "A comma separated list of file types for which to check formattingt. A value of 'all' will check all supported file types. For example, -check-format=json,yaml,ini or -check-format=all. Only json is supported currently.")
)
flag.Var(
&reporterConfigFlags,
Expand Down Expand Up @@ -161,6 +163,13 @@ Supported formats: standard, json, junit, and sarif (default: "standard")`,
return validatorConfig{}, err
}

if *formatPtr != "" {
formatFileTypes := strings.Split(strings.ToLower(*formatPtr), ",")
if !slices.Contains(formatFileTypes, "all") && !validateFileTypeList(formatFileTypes) {
return validatorConfig{}, errors.New("invalid check format file type")
}
}

err = validateReporterConf(reporterConf, groupOutputPtr)
if err != nil {
return validatorConfig{}, err
Expand Down Expand Up @@ -192,6 +201,7 @@ Supported formats: standard, json, junit, and sarif (default: "standard")`,
groupOutputPtr,
quietPtr,
globbingPrt,
formatPtr,
}

return config, nil
Expand Down Expand Up @@ -319,6 +329,7 @@ func applyDefaultFlagsFromEnv() error {
"reporter": "CFV_REPORTER",
"groupby": "CFV_GROUPBY",
"quiet": "CFV_QUIET",
"format": "CFV_FORMAT",
"globbing": "CFV_GLOBBING",
}

Expand Down Expand Up @@ -411,6 +422,8 @@ func mainInit() int {
fsOpts = append(fsOpts, finder.WithDepth(*validatorConfig.depth))
}

formatFileTypes := getFormatFileTypes(*validatorConfig.format)

// Initialize a file system finder
fileSystemFinder := finder.FileSystemFinderInit(fsOpts...)

Expand All @@ -420,6 +433,7 @@ func mainInit() int {
cli.WithFinder(fileSystemFinder),
cli.WithGroupOutput(groupOutput),
cli.WithQuiet(quiet),
cli.WithFormatCheckTypes(formatFileTypes),
)

// Run the config file validation
Expand Down Expand Up @@ -456,6 +470,33 @@ func getExcludeFileTypes(configExcludeFileTypes string) []string {
return excludeFileTypes
}

func getFormatFileTypes(formatFlag string) []string {
if formatFlag == "" {
return nil
}

typesToFormat := strings.Split(strings.ToLower(formatFlag), ",")
typesToFormatSet := tools.ArrToMap(typesToFormat...)
fileTypesToFormat := make(map[string]struct{})

formatAll := false
if _, ok := typesToFormatSet["all"]; ok {
formatAll = true
}
for _, ft := range filetype.FileTypes {
for ext := range ft.Extensions {
if _, ok := typesToFormatSet[ext]; formatAll || ok {
fileTypesToFormat[ft.Name] = struct{}{}
}
}
}
types := make([]string, 0, len(fileTypesToFormat))
for ft := range fileTypesToFormat {
types = append(types, ft)
}
return types
}

func main() {
os.Exit(mainInit())
}
87 changes: 77 additions & 10 deletions cmd/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,27 @@ func Test_flags(t *testing.T) {
{"globbing flag not set", []string{"test/**/*.json", "."}, 1},
{"globbing flag with exclude-dirs", []string{"-globbing", "--exclude-dirs=subdir", "test/**/*.json", "."}, 1},
{"globbing flag with exclude-file-types", []string{"-globbing", "--exclude-file-types=hcl", "test/**/*.json", "."}, 1},
{"format flag all", []string{"--check-format=all", "../../test/fixtures/good.json"}, 0},
{"format flag invalid", []string{"--check-format", "../../test/fixtures/good.json"}, 1},
{"format flag with types", []string{"--check-format=json,yaml,ini", "../../test/fixtures/good.json"}, 0},
{"format flag with invalid file", []string{"--check-format=all", "/path/does/not/exist"}, 1},
{"format flag with multiple files", []string{"--check-format=json", "../../test/fixtures/good.json", "../../test/fixtures/good.toml"}, 0},
{"format flag with exclude-dirs", []string{"--check-format=all", "--exclude-dirs=subdir", "."}, 0},
{"format flag with json reporter", []string{"--check-format=all", "--reporter=json", "../../test/fixtures/good.json"}, 0},
}
for _, tc := range cases {
// this call is required because otherwise flags panics,
// if args are set between flag.Parse call
fmt.Printf("Testing args: %v = %v\n", tc.Name, tc.Args)
flag.CommandLine = flag.NewFlagSet(tc.Name, flag.ExitOnError)
// we need a value to set Args[0] to cause flag begins parsing at Args[1]
os.Args = append([]string{tc.Name}, tc.Args...)
actualExit := mainInit()
if tc.ExpectedExit != actualExit {
t.Errorf("Test Case %v: Wrong exit code, expected: %v, got: %v", tc.Name, tc.ExpectedExit, actualExit)
}
t.Run(tc.Name, func(t *testing.T) {
// this call is required because otherwise flags panics,
// if args are set between flag.Parse call
fmt.Printf("Testing args: %v = %v\n", tc.Name, tc.Args)
flag.CommandLine = flag.NewFlagSet(tc.Name, flag.ExitOnError)
// we need a value to set Args[0] to cause flag begins parsing at Args[1]
os.Args = append([]string{tc.Name}, tc.Args...)
actualExit := mainInit()
if tc.ExpectedExit != actualExit {
t.Errorf("Test Case %v: Wrong exit code, expected: %v, got: %v", tc.Name, tc.ExpectedExit, actualExit)
}
})
}
}

Expand Down Expand Up @@ -104,3 +113,61 @@ func Test_getExcludeFileTypes(t *testing.T) {
})
}
}

func Test_getFormatFileTypes(t *testing.T) {
type testCase struct {
name string
input string
expectedFormatters []string
}

tcases := []testCase{
{
name: "empty",
input: "",
expectedFormatters: []string{},
},
{
name: "format check json",
input: "json",
expectedFormatters: []string{"json"},
},
{
name: "double input",
input: "json,json",
expectedFormatters: []string{"json"},
},

{
name: "format check json and yaml",
input: "json,yaml",
expectedFormatters: []string{"json", "yaml"},
},
{
name: "format check all",
input: "all,json",
expectedFormatters: []string{
"json",
"yaml",
"xml",
"toml",
"ini",
"properties",
"hcl",
"plist",
"csv",
"hocon",
"env",
"editorconfig",
"toon",
},
},
}

for _, tcase := range tcases {
t.Run(tcase.name, func(t *testing.T) {
actual := getFormatFileTypes(tcase.input)
require.ElementsMatch(t, tcase.expectedFormatters, actual)
})
}
}
29 changes: 25 additions & 4 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package cli

import (
"errors"
"fmt"
"os"
"slices"

"github.com/Boeing/config-file-validator/pkg/finder"
"github.com/Boeing/config-file-validator/pkg/reporter"
"github.com/Boeing/config-file-validator/pkg/validator"
)

// GroupOutput is a global variable that is used to
// store the group by options that the user specifies
var (
GroupOutput []string
Quiet bool
errorFound bool
GroupOutput []string
Quiet bool
errorFound bool
FormatCheckFileTypes []string
)

type CLI struct {
Expand Down Expand Up @@ -56,6 +60,12 @@ func WithQuiet(quiet bool) Option {
}
}

func WithFormatCheckTypes(types []string) Option {
return func(_ *CLI) {
FormatCheckFileTypes = types
}
}

// Initialize the CLI object
func Init(opts ...Option) *CLI {
defaultFsFinder := finder.FileSystemFinderInit()
Expand Down Expand Up @@ -88,15 +98,26 @@ func (c CLI) Run() (int, error) {
}

for _, fileToValidate := range foundFiles {
checkFormat := false
if slices.Contains(FormatCheckFileTypes, fileToValidate.FileType.Name) {
checkFormat = true
}
// read it
fileContent, err := os.ReadFile(fileToValidate.Path)
if err != nil {
return 1, fmt.Errorf("unable to read file: %w", err)
}

isValid, err := fileToValidate.FileType.Validator.Validate(fileContent)
isValid, err := fileToValidate.FileType.Validator.ValidateSyntax(fileContent)
if !isValid {
errorFound = true
} else if checkFormat {
isValid, err = fileToValidate.FileType.Validator.ValidateFormat(fileContent, nil)
if errors.Is(err, validator.ErrMethodUnimplemented) {
// Format validation not implemented for this type
isValid = true
err = nil
}
}
report := reporter.Report{
FileName: fileToValidate.Name,
Expand Down
Loading