Skip to content

Commit

Permalink
feat: support nested object when generating from terraform schema (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakezhu9 authored Sep 11, 2023
1 parent e6e349f commit 7a9ba4f
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 37 deletions.
126 changes: 93 additions & 33 deletions pkg/tools/gen/genkcl_terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"github.com/iancoleman/strcase"
"io"
"kcl-lang.io/kcl-go/pkg/logger"
"reflect"
"sort"
"strconv"
)

type tfSchema struct {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions pkg/tools/gen/templates/kcl/schema.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
1 change: 1 addition & 0 deletions pkg/tools/gen/testdata/jsonschema/items/expect.k
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ schema Book:
len(authors) <= 5
len(authors) >= 1
isunique(authors)

1 change: 1 addition & 0 deletions pkg/tools/gen/testdata/jsonschema/multipleof/expect.k
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ schema Book:

check:
multiplyof(price, 10)

1 change: 1 addition & 0 deletions pkg/tools/gen/testdata/jsonschema/pattern/expect.k
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ schema Cronjob:

check:
regex.match(schedule, r"^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$")

1 change: 1 addition & 0 deletions pkg/tools/gen/testdata/jsonschema/unsupport/expect.k
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ schema Product:

check:
price >= 0

1 change: 1 addition & 0 deletions pkg/tools/gen/testdata/jsonschema/validation/expect.k
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ schema Book:
price >= 0
quantity < 100
quantity > 0

18 changes: 16 additions & 2 deletions pkg/tools/gen/testdata/terraform/expect.k
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

0 comments on commit 7a9ba4f

Please sign in to comment.