diff --git a/codecov.yml b/codecov.yml index ba6f0b2..9f57ac0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,6 @@ +coverage: + precision: 2 + round: nearest + range: 50...70 comment: require_changes: true diff --git a/schema.go b/schema.go index 736eaf4..fd151b8 100644 --- a/schema.go +++ b/schema.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "errors" "flag" @@ -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 @@ -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 { @@ -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 { @@ -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") @@ -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: @@ -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, ¤tMap); err != nil { fmt.Printf("Error reading %s: %v\n", filePath, err) @@ -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) +} diff --git a/schema_test.go b/schema_test.go index 46b69b5..4b3c9f0 100644 --- a/schema_test.go +++ b/schema_test.go @@ -1,8 +1,11 @@ package main import ( + "flag" + "fmt" "os" "reflect" + "strings" "testing" "github.com/losisin/go-jsonschema-generator" @@ -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{} @@ -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 @@ -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) + }) + } +} diff --git a/testdata/values.yaml b/testdata/values_1.yaml similarity index 100% rename from testdata/values.yaml rename to testdata/values_1.yaml diff --git a/testdata/values_2.yaml b/testdata/values_2.yaml new file mode 100644 index 0000000..5803757 --- /dev/null +++ b/testdata/values_2.yaml @@ -0,0 +1,16 @@ +nodeSelector: + kubernetes.io/hostname: "" +deep: + deep1: + deep2: + deep3: + deep4: "qwerty" +list: + - "a" + - "b" + - "c" + +key1: "qwerty" +key2: 42 +key3: {} +key4: [] diff --git a/testdata/values_3.yaml b/testdata/values_3.yaml new file mode 100644 index 0000000..e01ec5e --- /dev/null +++ b/testdata/values_3.yaml @@ -0,0 +1,6 @@ +this: +is: +not valid: +this: +is: +not valid: \ No newline at end of file