Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move flags out of main function #13

Merged
merged 12 commits into from
Nov 2, 2023
Merged
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
4 changes: 4 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
coverage:
precision: 2
round: nearest
range: 50...70
comment:
require_changes: true
79 changes: 53 additions & 26 deletions schema.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"flag"
Expand All @@ -12,6 +13,15 @@ import (
"gopkg.in/yaml.v3"
)

// Save values of parsed flags in Config
type Config struct {
input multiStringFlag
outputPath string
draft int

args []string
}

// Define a custom flag type to accept multiple yamlFiles
type multiStringFlag []string

Expand All @@ -27,6 +37,7 @@ func (m *multiStringFlag) Set(value string) error {
return nil
}

// Read and unmarshal YAML file
func readAndUnmarshalYAML(filePath string, target interface{}) error {
data, err := os.ReadFile(filePath)
if err != nil {
Expand All @@ -35,6 +46,7 @@ func readAndUnmarshalYAML(filePath string, target interface{}) error {
return yaml.Unmarshal(data, target)
}

// Merge all YAML files into a single map
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
Expand All @@ -54,6 +66,7 @@ func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
return out
}

// Print the merged map to a file as JSON schema
func printMap(data *jsonschema.Document, outputPath string) error {
if data == nil {
return errors.New("data is nil")
Expand Down Expand Up @@ -82,35 +95,37 @@ func printMap(data *jsonschema.Document, outputPath string) error {
return nil
}

func usage() {
fmt.Fprintln(os.Stderr, "usage: helm schema [-input STR] [-draft INT] [-output STR]")
flag.PrintDefaults()
}

func main() {
// Define the custom flag for yamlFiles and set its default value
var yamlFiles multiStringFlag
flag.Var(&yamlFiles, "input", "Multiple yamlFiles as inputs (comma-separated)")
// Parse flags
func parseFlags(progname string, args []string) (config *Config, output string, err error) {
flags := flag.NewFlagSet(progname, flag.ContinueOnError)
var buf bytes.Buffer
flags.SetOutput(&buf)

// Define the flag to specify the schema url
draft := flag.Int("draft", 2020, "Draft version (4, 6, 7, 2019, or 2020)")
var conf Config
flags.Var(&conf.input, "input", "Multiple yaml files as inputs (comma-separated)")
flags.StringVar(&conf.outputPath, "output", "values.schema.json", "Output file path")
flags.IntVar(&conf.draft, "draft", 2020, "Draft version (4, 6, 7, 2019, or 2020)")

// Define the flag to specify the output file
var outputPath string
flag.StringVar(&outputPath, "output", "values.schema.json", "Output file path")
err = flags.Parse(args)
if err != nil {
fmt.Println("usage: helm schema [-input STR] [-draft INT] [-output STR]")
return nil, buf.String(), err
}

flag.Usage = usage
flag.Parse()
conf.args = flags.Args()
return &conf, buf.String(), nil
}

// Generate JSON schema
func generateJsonSchema(config *Config) {
// Check if the input flag is set
if len(yamlFiles) == 0 {
fmt.Println("Input flag is required. Please provide input yaml files using the -input flag.")
usage()
return
if len(config.input) == 0 {
fmt.Fprintln(os.Stderr, "Input flag is required. Please provide input yaml files using the -input flag.")
os.Exit(2)
}

var schemaUrl string
switch *draft {
switch config.draft {
case 4:
schemaUrl = "http://json-schema.org/draft-04/schema#"
case 6:
Expand All @@ -130,8 +145,7 @@ func main() {
mergedMap := make(map[string]interface{})

// Iterate over the input YAML files
for _, filePath := range yamlFiles {
// Read and unmarshal each YAML file
for _, filePath := range config.input {
var currentMap map[string]interface{}
if err := readAndUnmarshalYAML(filePath, &currentMap); err != nil {
fmt.Printf("Error reading %s: %v\n", filePath, err)
Expand All @@ -140,15 +154,28 @@ func main() {

// Merge the current YAML data with the mergedMap
mergedMap = mergeMaps(mergedMap, currentMap)
// fmt.Println(mergedMap)
}

// Print or save the merged map
// Print the merged map
d := jsonschema.NewDocument(schemaUrl)
d.ReadDeep(&mergedMap)

err := printMap(d, outputPath)
err := printMap(d, config.outputPath)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}

func main() {
conf, output, err := parseFlags(os.Args[0], os.Args[1:])
if err == flag.ErrHelp {
fmt.Println(output)
os.Exit(0)
} else if err != nil {
fmt.Println("got error:", err)
fmt.Println("output:\n", output)
os.Exit(1)
}

generateJsonSchema(conf)
}
146 changes: 138 additions & 8 deletions schema_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package main

import (
"flag"
"fmt"
"os"
"reflect"
"strings"
"testing"

"github.com/losisin/go-jsonschema-generator"
Expand Down Expand Up @@ -105,7 +108,6 @@ func TestReadAndUnmarshalYAML(t *testing.T) {
})

t.Run("File Missing", func(t *testing.T) {
// YAML file is assumed to be missing
missingFilePath := "missing.yaml"

var target map[string]interface{}
Expand Down Expand Up @@ -176,15 +178,14 @@ func TestPrintMap(t *testing.T) {

var yamlData map[string]interface{}

// Test successful data read and schema creation
err := readAndUnmarshalYAML("testdata/values.yaml", &yamlData)
err := readAndUnmarshalYAML("testdata/values_1.yaml", &yamlData)
if err != nil {
t.Fatalf("Failed to mock YAML data: %v", err)
}
data := jsonschema.NewDocument("")
data.ReadDeep(&yamlData)

cases := []struct {
tests := []struct {
data *jsonschema.Document
tmpFile string
expectError bool
Expand All @@ -194,15 +195,144 @@ func TestPrintMap(t *testing.T) {
{nil, tmpFile, true},
}

for _, c := range cases {
for _, tt := range tests {
t.Run("PrintMap", func(t *testing.T) {
err := printMap(c.data, c.tmpFile)
err := printMap(tt.data, tt.tmpFile)
switch {
case err == nil && c.expectError:
case err == nil && tt.expectError:
t.Fatalf("Expected an error, but printMap succeeded")
case err != nil && !c.expectError:
case err != nil && !tt.expectError:
t.Fatalf("Unexpected error: %v", err)
}
})
}
}

func TestParseFlagsPass(t *testing.T) {
var tests = []struct {
args []string
conf Config
}{
{[]string{"-input", "testdata/values_1.yaml"},
Config{input: multiStringFlag{"testdata/values_1.yaml"}, outputPath: "values.schema.json", draft: 2020, args: []string{}}},

{[]string{"-input", "values1.yaml testdata/values_1.yaml"},
Config{input: multiStringFlag{"values1.yaml testdata/values_1.yaml"}, outputPath: "values.schema.json", draft: 2020, args: []string{}}},

{[]string{"-input", "testdata/values_1.yaml", "-output", "my.schema.json", "-draft", "2019"},
Config{input: multiStringFlag{"testdata/values_1.yaml"}, outputPath: "my.schema.json", draft: 2019, args: []string{}}},
}

for _, tt := range tests {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
conf, output, err := parseFlags("prog", tt.args)
if err != nil {
t.Errorf("err got %v, want nil", err)
}
if output != "" {
t.Errorf("output got %q, want empty", output)
}
if !reflect.DeepEqual(*conf, tt.conf) {
t.Errorf("conf got %+v, want %+v", *conf, tt.conf)
}
})
}
}

func TestParseFlagsUsage(t *testing.T) {
var usageArgs = []string{"-help", "-h", "--help"}

for _, arg := range usageArgs {
t.Run(arg, func(t *testing.T) {
conf, output, err := parseFlags("prog", []string{arg})
if err != flag.ErrHelp {
t.Errorf("err got %v, want ErrHelp", err)
}
if conf != nil {
t.Errorf("conf got %v, want nil", conf)
}
if !strings.Contains(output, "Usage of") {
t.Errorf("output can't find \"Usage of\": %q", output)
}
})
}
}

func TestParseFlagsFail(t *testing.T) {
var tests = []struct {
args []string
errStr string
}{
{[]string{"-input"}, "flag needs an argument"},
{[]string{"-draft", "foo"}, "invalid value"},
{[]string{"-foo"}, "flag provided but not defined"},
}

for _, tt := range tests {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
conf, output, err := parseFlags("prog", tt.args)
if conf != nil {
t.Errorf("conf got %v, want nil", conf)
}
if !strings.Contains(err.Error(), tt.errStr) {
t.Errorf("err got %q, want to find %q", err.Error(), tt.errStr)
}
if !strings.Contains(output, "Usage of") {
t.Errorf("output got %q", output)
}
})
}
}

func TestGenerateJsonSchemaPass(t *testing.T) {
var tests = []struct {
conf Config
expectedUrl string
}{
{Config{input: multiStringFlag{"testdata/values_1.yaml", "testdata/values_2.yaml"}, draft: 2020, outputPath: "2020.schema.json", args: []string{}}, "https://json-schema.org/draft/2020-12/schema"},
{Config{input: multiStringFlag{"testdata/values_1.yaml"}, draft: 2020, outputPath: "2020.schema.json", args: []string{}}, "https://json-schema.org/draft/2020-12/schema"},
{Config{input: multiStringFlag{"testdata/values_1.yaml"}, draft: 2019, outputPath: "2019.schema.json", args: []string{}}, "https://json-schema.org/draft/2019-09/schema"},
{Config{input: multiStringFlag{"testdata/values_1.yaml"}, draft: 7, outputPath: "7.schema.json", args: []string{}}, "http://json-schema.org/draft-07/schema#"},
{Config{input: multiStringFlag{"testdata/values_1.yaml"}, draft: 6, outputPath: "6.schema.json", args: []string{}}, "http://json-schema.org/draft-06/schema#"},
{Config{input: multiStringFlag{"testdata/values_1.yaml"}, draft: 4, outputPath: "4.schema.json", args: []string{}}, "http://json-schema.org/draft-04/schema#"},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("%v", tt.conf), func(t *testing.T) {
conf := &tt.conf
generateJsonSchema(conf)

_, err := os.Stat(conf.outputPath)
if os.IsNotExist(err) {
t.Errorf("Expected file '%q' to be created, but it doesn't exist", conf.outputPath)
}

outputJson, err := os.ReadFile(conf.outputPath)
if err != nil {
t.Errorf("Error reading file '%q': %v", conf.outputPath, err)
}

actualURL := string(outputJson)
if !strings.Contains(actualURL, tt.expectedUrl) {
t.Errorf("Schema URL does not match. Got: %s, Expected: %s", actualURL, tt.expectedUrl)
}

os.Remove(conf.outputPath)
})
t.Run(fmt.Sprintf("%v", tt.conf), func(t *testing.T) {
conf := &tt.conf
generateJsonSchema(conf)

outputJson, err := os.ReadFile(conf.outputPath)
if err != nil {
t.Errorf("Error reading file '%q': %v", conf.outputPath, err)
}

actualURL := string(outputJson)
if !strings.Contains(actualURL, tt.expectedUrl) {
t.Errorf("Schema URL does not match. Got: %s, Expected: %s", actualURL, tt.expectedUrl)
}
os.Remove(conf.outputPath)
})
}
}
File renamed without changes.
16 changes: 16 additions & 0 deletions testdata/values_2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
nodeSelector:
kubernetes.io/hostname: ""
deep:
deep1:
deep2:
deep3:
deep4: "qwerty"
list:
- "a"
- "b"
- "c"

key1: "qwerty"
key2: 42
key3: {}
key4: []
6 changes: 6 additions & 0 deletions testdata/values_3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
this:
is:
not valid:
this:
is:
not valid: