Skip to content

Commit

Permalink
Merge pull request #13 from losisin/move-flags-out-of-main
Browse files Browse the repository at this point in the history
move flags out of main function
  • Loading branch information
losisin authored Nov 2, 2023
2 parents 8ad471b + 77b4016 commit 240e586
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 34 deletions.
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:

0 comments on commit 240e586

Please sign in to comment.