Skip to content

Commit

Permalink
Keep the unmodified JSON schema for use in YAML files
Browse files Browse the repository at this point in the history
  • Loading branch information
MacroPower committed Jan 7, 2025
1 parent aa538aa commit 6a17f6c
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 21 deletions.
14 changes: 4 additions & 10 deletions pkg/helmutil/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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")
Expand Down
12 changes: 3 additions & 9 deletions pkg/jsonschema/gen_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
61 changes: 61 additions & 0 deletions pkg/jsonschema/jsonschema_kcl.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 6 additions & 1 deletion pkg/jsonschema/jsonschema_refs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions pkg/jsonschema/jsonschema_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package jsonschema_test

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -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,
)
})
}
}
3 changes: 2 additions & 1 deletion pkg/jsonschema/testdata/input/deep.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,6 @@
},
"required": []
}
}
},
"required": []
}
74 changes: 74 additions & 0 deletions pkg/jsonschema/testdata/output/deep-kcl.schema.json
Original file line number Diff line number Diff line change
@@ -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": []
}
12 changes: 12 additions & 0 deletions pkg/jsonschema/testdata/output/deep.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
"configMaps": {
"additionalProperties": {
"additionalProperties": false,
"oneOf": [
{
"required": [
"data"
]
},
{
"required": [
"binaryData"
]
}
],
"properties": {
"annotations": {
"additionalProperties": {
Expand Down

0 comments on commit 6a17f6c

Please sign in to comment.