diff --git a/pkg/helmutil/add.go b/pkg/helmutil/add.go index 03b1d14..85e0b1a 100644 --- a/pkg/helmutil/add.go +++ b/pkg/helmutil/add.go @@ -10,12 +10,10 @@ import ( "regexp" "kcl-lang.io/kcl-go" - "kcl-lang.io/kcl-go/pkg/tools/gen" "github.com/MacroPower/kclipper/pkg/helm" "github.com/MacroPower/kclipper/pkg/helmmodels" "github.com/MacroPower/kclipper/pkg/jsonschema" - "github.com/MacroPower/kclipper/pkg/kclutil" ) var ( @@ -144,17 +142,13 @@ func (c *ChartPkg) writeValuesSchemaFiles(jsonSchema []byte, chartDir string) er return fmt.Errorf("failed to write values.schema.json: %w", err) } - kclSchema := &bytes.Buffer{} - if err := kclutil.Gen.GenKcl(kclSchema, "values", jsonSchema, &gen.GenKclOptions{ - Mode: gen.ModeJsonSchema, - CastingOption: gen.OriginalName, - UseIntegersForNumbers: true, - }); err != nil { - return fmt.Errorf("failed to generate kcl schema: %w", err) + kclSchema, err := jsonschema.ConvertToKCLSchema(jsonSchema) + if err != nil { + return fmt.Errorf("failed to convert JSON Schema to KCL Schema: %w", err) } kclSchemaFixed := &bytes.Buffer{} - scanner := bufio.NewScanner(kclSchema) + scanner := bufio.NewScanner(bytes.NewReader(kclSchema)) for scanner.Scan() { line := scanner.Text() line = SchemaDefaultRegexp.ReplaceAllString(line, "$1") diff --git a/pkg/jsonschema/gen_reader.go b/pkg/jsonschema/gen_reader.go index 6530a73..e56ec05 100644 --- a/pkg/jsonschema/gen_reader.go +++ b/pkg/jsonschema/gen_reader.go @@ -124,24 +124,18 @@ func (g *ReaderGenerator) FromData(data []byte, refBasePath string) ([]byte, err return nil, fmt.Errorf("invalid schema: %w", err) } - // Remove the ID to keep downstream KCL schema generation consistent. - hs.Id = "" - if err := handleSchemaRefs(hs, refBasePath); err != nil { return nil, fmt.Errorf("failed to handle schema refs: %w", err) } - mhs := &helmschema.Schema{} - mhs = mergeHelmSchemas(mhs, hs, true) - - if err := mhs.Validate(); err != nil { + if err := hs.Validate(); err != nil { return nil, fmt.Errorf("invalid schema: %w", err) } - if len(mhs.Properties) == 0 { + if len(hs.Properties) == 0 { return nil, errors.New("empty schema") } - resolvedData, err := mhs.ToJson() + resolvedData, err := hs.ToJson() if err != nil { return nil, fmt.Errorf("failed to convert schema to JSON: %w", err) } diff --git a/pkg/jsonschema/jsonschema_kcl.go b/pkg/jsonschema/jsonschema_kcl.go new file mode 100644 index 0000000..8639d34 --- /dev/null +++ b/pkg/jsonschema/jsonschema_kcl.go @@ -0,0 +1,61 @@ +package jsonschema + +import ( + "bytes" + "fmt" + + "gopkg.in/yaml.v3" + "kcl-lang.io/kcl-go/pkg/tools/gen" + + "github.com/MacroPower/kclipper/pkg/kclutil" + helmschema "github.com/dadav/helm-schema/pkg/schema" +) + +// ConvertToKCLSchema converts a JSON schema to a KCL schema. +func ConvertToKCLSchema(jsonSchemaData []byte) ([]byte, error) { + fixedJSONSchema, err := ConvertToKCLCompatibleJSONSchema(jsonSchemaData) + if err != nil { + return nil, fmt.Errorf("failed to convert to KCL compatible JSON schema: %w", err) + } + + kclSchema := &bytes.Buffer{} + if err := kclutil.Gen.GenKcl(kclSchema, "values", fixedJSONSchema, &gen.GenKclOptions{ + Mode: gen.ModeJsonSchema, + CastingOption: gen.OriginalName, + UseIntegersForNumbers: true, + }); err != nil { + return nil, fmt.Errorf("failed to generate kcl schema: %w", err) + } + + return kclSchema.Bytes(), nil +} + +// ConvertToKCLCompatibleJSONSchema converts a JSON schema to a JSON schema that +// is compatible with KCL schema generation (i.e. removing unsupported fields). +func ConvertToKCLCompatibleJSONSchema(jsonSchemaData []byte) ([]byte, error) { + // YAML is a superset of JSON, so this works and is simpler than re-writing + // the Unmarshaler for JSON. + var jsonNode yaml.Node + if err := yaml.Unmarshal(jsonSchemaData, &jsonNode); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON Schema: %w", err) + } + hs := &helmschema.Schema{} + if err := hs.UnmarshalYAML(&jsonNode); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON Schema: %w", err) + } + + // Remove the ID to keep KCL schema naming consistent. + hs.Id = "" + + // For now, merge into an empty schema as that will result in a schema that + // is compatible with KCL schema generation. + mhs := &helmschema.Schema{} + mhs = mergeHelmSchemas(mhs, hs, true) + + fixedJSONSchema, err := mhs.ToJson() + if err != nil { + return nil, fmt.Errorf("failed to convert schema to JSON: %w", err) + } + + return fixedJSONSchema, nil +} diff --git a/pkg/jsonschema/jsonschema_refs.go b/pkg/jsonschema/jsonschema_refs.go index 4b2d1b4..8d417e6 100644 --- a/pkg/jsonschema/jsonschema_refs.go +++ b/pkg/jsonschema/jsonschema_refs.go @@ -234,7 +234,12 @@ func derefAdditionalProperties(schema *helmschema.Schema, basePath string) error return err //nolint:wrapcheck } - subSchema.Required = helmschema.BoolOrArrayOfString{} + // No idea why, but Required isn't marshaled correctly without recreating the struct. + subSchema.Required = helmschema.BoolOrArrayOfString{ + Bool: subSchema.Required.Bool, + Strings: subSchema.Required.Strings, + } + schema.AdditionalProperties = subSchema return nil diff --git a/pkg/jsonschema/jsonschema_test.go b/pkg/jsonschema/jsonschema_test.go index bfa0ae1..92c2fba 100644 --- a/pkg/jsonschema/jsonschema_test.go +++ b/pkg/jsonschema/jsonschema_test.go @@ -1,8 +1,10 @@ package jsonschema_test import ( + "os" "path/filepath" "runtime" + "strings" "testing" "github.com/stretchr/testify/require" @@ -25,3 +27,65 @@ func TestGetGenerator(t *testing.T) { require.IsType(t, jsonschema.DefaultAutoGenerator, jsonschema.GetGenerator(jsonschema.AutoGeneratorType)) require.IsType(t, jsonschema.DefaultNoGenerator, jsonschema.GetGenerator("UNKNOWN")) } + +func TestKCLConversion(t *testing.T) { + t.Parallel() + + generator := jsonschema.DefaultReaderGenerator + + testCases := map[string]struct { + filePaths []string + expectedPath string + }{ + "SingleFile": { + filePaths: []string{"input/schema.json"}, + expectedPath: "output/schema.json", + }, + "MultiFile": { + filePaths: []string{"input/nota.schema.json", "input/invalid.json", "input/schema.json"}, + expectedPath: "output/schema.json", + }, + "FileRefs": { + filePaths: []string{"input/refs.schema.json"}, + expectedPath: "output/schema.json", + }, + "DeepSchema": { + filePaths: []string{"input/deep.schema.json"}, + expectedPath: "output/deep-kcl.schema.json", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + var testFilePaths []string + for _, filePath := range tc.filePaths { + testFilePath := filepath.Join(testDataDir, filePath) + testFilePaths = append(testFilePaths, testFilePath) + + // Ensure test file exists + _, err := os.Stat(testFilePath) + require.NoError(t, err) + } + + // Test FromPaths + t.Logf("Test FromPaths: %s", strings.Join(testFilePaths, ", ")) + schemaBytes, err := generator.FromPaths(testFilePaths...) + require.NoError(t, err) + require.NotEmpty(t, schemaBytes) + + fixedSchemaBytes, err := jsonschema.ConvertToKCLCompatibleJSONSchema(schemaBytes) + require.NoError(t, err) + + // Verify the output schema + wantFilePath := filepath.Join(testDataDir, tc.expectedPath) + expectedSchema, err := os.ReadFile(wantFilePath) + require.NoError(t, err) + require.JSONEq(t, + string(expectedSchema), string(fixedSchemaBytes), + "Input: %s\nWant: %s", strings.Join(testFilePaths, ", "), wantFilePath, + ) + }) + } +} diff --git a/pkg/jsonschema/testdata/input/deep.schema.json b/pkg/jsonschema/testdata/input/deep.schema.json index db282cb..edb19a4 100644 --- a/pkg/jsonschema/testdata/input/deep.schema.json +++ b/pkg/jsonschema/testdata/input/deep.schema.json @@ -81,5 +81,6 @@ }, "required": [] } - } + }, + "required": [] } diff --git a/pkg/jsonschema/testdata/output/deep-kcl.schema.json b/pkg/jsonschema/testdata/output/deep-kcl.schema.json new file mode 100644 index 0000000..70876b9 --- /dev/null +++ b/pkg/jsonschema/testdata/output/deep-kcl.schema.json @@ -0,0 +1,74 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "properties": { + "configMaps": { + "additionalProperties": { + "additionalProperties": false, + "properties": { + "annotations": { + "additionalProperties": { + "required": [], + "type": [ + "string", + "null" + ] + }, + "required": [], + "type": [ + "object", + "null" + ] + }, + "binaryData": { + "additionalProperties": { + "required": [], + "type": "string" + }, + "required": [], + "type": "object" + }, + "data": { + "additionalProperties": { + "required": [], + "type": "string" + }, + "required": [], + "type": "object" + }, + "enabled": { + "default": true, + "required": [], + "type": "boolean" + }, + "includeInChecksum": { + "default": true, + "required": [], + "type": "boolean" + }, + "labels": { + "additionalProperties": { + "required": [], + "type": [ + "string", + "null" + ] + }, + "required": [], + "type": [ + "object", + "null" + ] + }, + "nameOverride": { + "required": [], + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "required": [] + } + }, + "required": [] +} diff --git a/pkg/jsonschema/testdata/output/deep.schema.json b/pkg/jsonschema/testdata/output/deep.schema.json index 70876b9..edb19a4 100644 --- a/pkg/jsonschema/testdata/output/deep.schema.json +++ b/pkg/jsonschema/testdata/output/deep.schema.json @@ -4,6 +4,18 @@ "configMaps": { "additionalProperties": { "additionalProperties": false, + "oneOf": [ + { + "required": [ + "data" + ] + }, + { + "required": [ + "binaryData" + ] + } + ], "properties": { "annotations": { "additionalProperties": {