From 7a9ba4f0ba759261620acaafb4b5101d8b8ba4bd Mon Sep 17 00:00:00 2001 From: Junxing Zhu Date: Mon, 11 Sep 2023 16:17:32 +0800 Subject: [PATCH] feat: support nested object when generating from terraform schema (#152) --- pkg/tools/gen/genkcl_terraform.go | 126 +++++++++++++----- pkg/tools/gen/templates/kcl/schema.gotmpl | 6 +- .../gen/testdata/jsonschema/items/expect.k | 1 + .../testdata/jsonschema/multipleof/expect.k | 1 + .../gen/testdata/jsonschema/pattern/expect.k | 1 + .../testdata/jsonschema/unsupport/expect.k | 1 + .../testdata/jsonschema/validation/expect.k | 1 + pkg/tools/gen/testdata/terraform/expect.k | 18 ++- 8 files changed, 118 insertions(+), 37 deletions(-) diff --git a/pkg/tools/gen/genkcl_terraform.go b/pkg/tools/gen/genkcl_terraform.go index afd50850..af9611a0 100644 --- a/pkg/tools/gen/genkcl_terraform.go +++ b/pkg/tools/gen/genkcl_terraform.go @@ -5,7 +5,9 @@ import ( "github.com/iancoleman/strcase" "io" "kcl-lang.io/kcl-go/pkg/logger" + "reflect" "sort" + "strconv" ) type tfSchema struct { @@ -40,6 +42,11 @@ type tfAttribute struct { Computed bool `json:"computed"` } +type tfConvertContext struct { + resultMap map[string]schema + attrKeyNow string +} + func (k *kclGenerator) genSchemaFromTerraformSchema(w io.Writer, filename string, src interface{}) error { code, err := readSource(filename, src) if err != nil { @@ -51,35 +58,15 @@ func (k *kclGenerator) genSchemaFromTerraformSchema(w io.Writer, filename string } // convert terraform schema to kcl schema - var result []schema + ctx := &tfConvertContext{ + resultMap: make(map[string]schema), + } for _, providerSchema := range tfSch.ProviderSchemas { - for resKey, resourceSchema := range providerSchema.ResourceSchemas { - sch := schema{ - Name: strcase.ToCamel(resKey), - Description: resourceSchema.Block.Description, - } - for attrKey, attr := range resourceSchema.Block.Attributes { - sch.Properties = append(sch.Properties, property{ - Name: attrKey, - Description: attr.Description, - Type: tfTypeToKclType(attr.Type), - Required: attr.Required, - }) - if t, ok := attr.Type.([]interface{}); ok && t[0] == "set" { - sch.Validations = append(sch.Validations, validation{ - Name: attrKey, - Unique: true, - }) - } - } - sort.Slice(sch.Properties, func(i, j int) bool { - return sch.Properties[i].Name < sch.Properties[j].Name - }) - sort.Slice(sch.Validations, func(i, j int) bool { - return sch.Validations[i].Name < sch.Validations[j].Name - }) - result = append(result, sch) - } + convertSchemaFromTFSchema(ctx, providerSchema) + } + result := make([]schema, 0, len(ctx.resultMap)) + for _, sch := range ctx.resultMap { + result = append(result, sch) } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name @@ -93,19 +80,92 @@ func (k *kclGenerator) genSchemaFromTerraformSchema(w io.Writer, filename string return k.genKcl(w, kclSch) } -func tfTypeToKclType(t interface{}) typeInterface { +// convertSchemaFromTFSchema converts terraform provider schema to kcl schema and save to ctx.resultMap +func convertSchemaFromTFSchema(ctx *tfConvertContext, tfSch tfProviderSchema) { + for resKey, resourceSchema := range tfSch.ResourceSchemas { + sch := schema{ + Name: strcase.ToCamel(resKey), + Description: resourceSchema.Block.Description, + } + for attrKey, attr := range resourceSchema.Block.Attributes { + ctx.attrKeyNow = attrKey + sch.Properties = append(sch.Properties, property{ + Name: attrKey, + Description: attr.Description, + Type: tfTypeToKclType(ctx, attr.Type), + Required: attr.Required, + }) + if t, ok := attr.Type.([]interface{}); ok && t[0] == "set" { + sch.Validations = append(sch.Validations, validation{ + Name: attrKey, + Unique: true, + }) + } + } + sort.Slice(sch.Properties, func(i, j int) bool { + return sch.Properties[i].Name < sch.Properties[j].Name + }) + sort.Slice(sch.Validations, func(i, j int) bool { + return sch.Validations[i].Name < sch.Validations[j].Name + }) + ctx.resultMap[sch.Name] = sch + } +} + +// convertTFNestedSchema converts nested object schema to kcl schema, save to ctx.resultMap and return schema name +func convertTFNestedSchema(ctx *tfConvertContext, tfSchema map[string]interface{}) string { + resultSchemaName := strcase.ToCamel(ctx.attrKeyNow + "Item") + sch := schema{} + for key, typ := range tfSchema { + ctx.attrKeyNow = key + sch.Properties = append(sch.Properties, property{ + Name: key, + Type: tfTypeToKclType(ctx, typ), + }) + if t, ok := typ.([]interface{}); ok && t[0] == "set" { + sch.Validations = append(sch.Validations, validation{ + Name: key, + Unique: true, + }) + } + } + sort.Slice(sch.Properties, func(i, j int) bool { + return sch.Properties[i].Name < sch.Properties[j].Name + }) + sort.Slice(sch.Validations, func(i, j int) bool { + return sch.Validations[i].Name < sch.Validations[j].Name + }) + + // for the name of the schema, we will try xxxItem first + // if it is already used and not equal to the schema, we will try xxxItem1, xxxItem2, ... + for i := 0; true; i++ { + sch.Name = resultSchemaName + if i != 0 { + sch.Name += strconv.Itoa(i) + } + if _, ok := ctx.resultMap[sch.Name]; !ok || reflect.DeepEqual(ctx.resultMap[sch.Name], sch) { + break + } + } + ctx.resultMap[sch.Name] = sch + return sch.Name +} + +func tfTypeToKclType(ctx *tfConvertContext, t interface{}) typeInterface { switch t := t.(type) { case string: return jsonTypeToKclType(t) case []interface{}: switch t[0] { case "list": - return typeArray{Items: tfTypeToKclType(t[1])} + return typeArray{Items: tfTypeToKclType(ctx, t[1])} case "map": - return typeDict{Key: typePrimitive(typStr), Value: tfTypeToKclType(t[1])} + return typeDict{Key: typePrimitive(typStr), Value: tfTypeToKclType(ctx, t[1])} case "set": - return typeArray{Items: tfTypeToKclType(t[1])} - case "object", "tuple": + return typeArray{Items: tfTypeToKclType(ctx, t[1])} + case "object": + return typeCustom{Name: convertTFNestedSchema(ctx, t[1].(map[string]interface{}))} + case "tuple": // todo return typePrimitive(typAny) default: diff --git a/pkg/tools/gen/templates/kcl/schema.gotmpl b/pkg/tools/gen/templates/kcl/schema.gotmpl index bd92cde8..fe826ec2 100644 --- a/pkg/tools/gen/templates/kcl/schema.gotmpl +++ b/pkg/tools/gen/templates/kcl/schema.gotmpl @@ -10,7 +10,9 @@ schema {{ formatName .Name }}: {{- if .IndexSignature.Alias }}{{ formatName .IndexSignature.Alias }}:{{ end }} {{- "...str]: " }}{{ formatType .IndexSignature.Type }} {{- end }} -{{ if .Validations }} + +{{- if .Validations }} + check: {{- template "validator" .Validations }} -{{- end -}} +{{- end }} diff --git a/pkg/tools/gen/testdata/jsonschema/items/expect.k b/pkg/tools/gen/testdata/jsonschema/items/expect.k index 2d500e56..eb8b1579 100644 --- a/pkg/tools/gen/testdata/jsonschema/items/expect.k +++ b/pkg/tools/gen/testdata/jsonschema/items/expect.k @@ -20,3 +20,4 @@ schema Book: len(authors) <= 5 len(authors) >= 1 isunique(authors) + diff --git a/pkg/tools/gen/testdata/jsonschema/multipleof/expect.k b/pkg/tools/gen/testdata/jsonschema/multipleof/expect.k index 6b839293..1bcc4979 100644 --- a/pkg/tools/gen/testdata/jsonschema/multipleof/expect.k +++ b/pkg/tools/gen/testdata/jsonschema/multipleof/expect.k @@ -18,3 +18,4 @@ schema Book: check: multiplyof(price, 10) + diff --git a/pkg/tools/gen/testdata/jsonschema/pattern/expect.k b/pkg/tools/gen/testdata/jsonschema/pattern/expect.k index bbff438c..6656416a 100644 --- a/pkg/tools/gen/testdata/jsonschema/pattern/expect.k +++ b/pkg/tools/gen/testdata/jsonschema/pattern/expect.k @@ -21,3 +21,4 @@ schema Cronjob: check: regex.match(schedule, r"^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$") + diff --git a/pkg/tools/gen/testdata/jsonschema/unsupport/expect.k b/pkg/tools/gen/testdata/jsonschema/unsupport/expect.k index 3a20af2d..c3c03ee0 100644 --- a/pkg/tools/gen/testdata/jsonschema/unsupport/expect.k +++ b/pkg/tools/gen/testdata/jsonschema/unsupport/expect.k @@ -37,3 +37,4 @@ schema Product: check: price >= 0 + diff --git a/pkg/tools/gen/testdata/jsonschema/validation/expect.k b/pkg/tools/gen/testdata/jsonschema/validation/expect.k index 5ab6316e..c55ff4ac 100644 --- a/pkg/tools/gen/testdata/jsonschema/validation/expect.k +++ b/pkg/tools/gen/testdata/jsonschema/validation/expect.k @@ -25,3 +25,4 @@ schema Book: price >= 0 quantity < 100 quantity > 0 + diff --git a/pkg/tools/gen/testdata/terraform/expect.k b/pkg/tools/gen/testdata/terraform/expect.k index 616b988a..fd5a2bfb 100644 --- a/pkg/tools/gen/testdata/terraform/expect.k +++ b/pkg/tools/gen/testdata/terraform/expect.k @@ -9,11 +9,11 @@ schema AlicloudConfigRule: Attributes ---------- - compliance : [any], optional + compliance : [ComplianceItem], optional resource_types_scope : [str], optional """ - compliance?: [any] + compliance?: [ComplianceItem] resource_types_scope?: [str] schema AlicloudDbInstance: @@ -38,3 +38,17 @@ schema AlicloudDbInstance: check: isunique(security_group_ids) isunique(security_ips) + +schema ComplianceItem: + """ + ComplianceItem + + Attributes + ---------- + compliance_type : str, optional + count : float, optional + """ + + compliance_type?: str + count?: float +