packer-cn/cmd/hcl2-schema/hcl2-schema.go

314 lines
7.8 KiB
Go

package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"regexp"
"strings"
"text/template"
"github.com/fatih/structtag"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
var (
typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
output = flag.String("output", "", "output file name; default srcdir/<type>_hcl2.go")
trimprefix = flag.String("trimprefix", "", "trim the `prefix` from the generated constant names")
)
// Usage is a replacement usage function for the flags package.
func Usage() {
fmt.Fprintf(os.Stderr, "Usage of hcl2-schema:\n")
fmt.Fprintf(os.Stderr, "\thcl2-schema [flags] -type T [directory]\n")
fmt.Fprintf(os.Stderr, "\thcl2-schema [flags] -type T files... # Must be a single package\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
}
func main() {
log.SetFlags(0)
log.SetPrefix("hcl2-schema: ")
flag.Usage = Usage
flag.Parse()
if len(*typeNames) == 0 {
flag.Usage()
os.Exit(2)
}
types := strings.Split(*typeNames, ",")
// We accept either one directory or a list of files. Which do we have?
args := flag.Args()
if len(args) == 0 {
// Default: process whole package in current directory.
args = []string{os.Getenv("GOFILE")}
}
fname := args[0]
outputPath := fname[:len(fname)-2] + "hcl2spec.go"
b, err := ioutil.ReadFile(fname)
if err != nil {
fmt.Printf("ReadFile: %+v", err)
os.Exit(1)
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, fname, b, parser.ParseComments)
if err != nil {
fmt.Printf("ParseFile: %+v", err)
os.Exit(1)
}
output := Output{
Args: strings.Join(os.Args[1:], " "),
Package: f.Name.String(),
}
res := []StructDef{}
for _, t := range types {
for _, decl := range f.Decls {
typeDecl, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
typeSpec, ok := typeDecl.Specs[0].(*ast.TypeSpec)
if !ok {
continue
}
structDecl, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
if typeSpec.Name.String() != t {
continue
}
sd := StructDef{StructName: t}
fields := structDecl.Fields.List
for _, field := range fields {
fieldType := string(b[field.Type.Pos()-1 : field.Type.End()-1])
fieldName := fieldType[strings.Index(fieldType, ".")+1:]
if len(field.Names) > 0 {
fieldName = field.Names[0].Name
}
if ast.IsExported(fieldName) {
continue
}
if strings.Contains(fieldType, "func") {
continue
}
fd := FieldDef{Name: fieldName}
squash := false
accessor := ToSnakeCase(fieldName)
if field.Tag != nil {
tag := field.Tag.Value[1:]
tag = tag[:len(tag)-1]
tags, err := structtag.Parse(tag)
if err != nil {
log.Fatalf("structtag.Parse(%s): err: %v", field.Tag.Value, err)
}
if hsg, err := tags.Get("hcl2-schema-generator"); err == nil {
if len(hsg.Options) > 0 && hsg.Options[0] == "direct" {
fieldType = "direct"
}
if hsg.Name != "" {
accessor = hsg.Name
}
} else if mstr, err := tags.Get("mapstructure"); err == nil {
if len(mstr.Options) > 0 && mstr.Options[0] == "squash" {
squash = true
}
if mstr.Name != "" {
accessor = mstr.Name
}
}
}
switch fieldType {
case "direct":
fd.Spec = `(&` + sd.StructName + `{}).` + fieldName + `.HCL2Spec()`
case "[]string", "[]*string":
fd.Spec = fmt.Sprintf("%#v", &hcldec.AttrSpec{
Name: accessor,
Type: cty.List(cty.String),
Required: false,
})
case "[]int", "[]uint", "[]int32", "[]int64":
fd.Spec = fmt.Sprintf("%#v", &hcldec.AttrSpec{
Name: accessor,
Type: cty.List(cty.Number),
Required: false,
})
case "[]byte", "string", "*string", "time.Duration", "*url.URL":
fd.Spec = fmt.Sprintf("%#v", &hcldec.AttrSpec{
Name: accessor,
Type: cty.String,
Required: false,
})
case "uint", "*int", "int", "int32", "int64", "float", "float32", "float64":
fd.Spec = fmt.Sprintf("%#v", &hcldec.AttrSpec{
Name: accessor,
Type: cty.Number,
Required: false,
})
case "bool", "config.Trilean":
fd.Spec = fmt.Sprintf("%#v", &hcldec.AttrSpec{
Name: accessor,
Type: cty.Bool,
Required: false,
})
case "osccommon.TagMap", "awscommon.TagMap", "TagMap",
"map[string]*string", "map[*string]*string", "map[string]string":
fd.Spec = fmt.Sprintf("%#v", &hcldec.BlockAttrsSpec{
TypeName: accessor,
ElementType: cty.String,
Required: false,
})
case "[][]string":
// TODO(azr): implement those
continue
case "communicator.Config":
// this one is manually set
continue
case "map[string]interface{}", "map[string]map[string]interface{}":
// probably never going to be supported
continue
case "common.PackerConfig":
// this one is deprecated ?
continue
default: // nested structures
if squash {
sd.Squashed = append(sd.Squashed, fieldName)
} else if strings.HasPrefix(fieldType, "[]") {
sd.NestedList = append(sd.NestedList, NestedFieldDef{
FieldName: fieldName,
TypeName: fieldType,
Accessor: accessor,
})
} else {
sd.Nested = append(sd.Nested, NestedFieldDef{
FieldName: fieldName,
TypeName: fieldType,
Accessor: accessor,
})
}
continue
}
output.ImportCty = true
sd.Fields = append(sd.Fields, fd)
}
res = append(res, sd)
}
}
output.StructDefs = res
result := bytes.NewBuffer(nil)
err = structDocsTemplate.Execute(result, output)
if err != nil {
log.Fatalf("err templating: %v", err)
}
formattedBytes, err := format.Source(result.Bytes())
if err != nil {
log.Printf("formatting err: %v", err)
formattedBytes = result.Bytes()
}
outputFile, err := os.Create(outputPath)
if err != nil {
log.Fatalf("err: %v", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, bytes.NewBuffer(formattedBytes))
if err != nil {
log.Fatalf("err: %v", err)
}
}
type Output struct {
Args string
Package string
StructDefs []StructDef
ImportCty bool
}
type FieldDef struct {
Name string
Spec string
}
type NestedFieldDef struct {
TypeName string
FieldName string
Accessor string
}
type StructDef struct {
StructName string
Fields []FieldDef
Nested []NestedFieldDef
NestedList []NestedFieldDef
Squashed []string
}
var structDocsTemplate = template.Must(template.New("structDocsTemplate").
Funcs(template.FuncMap{
// "indent": indent,
}).
Parse(`// Code generated by "hcl2-schema {{ .Args }}"; DO NOT EDIT.\n
package {{ .Package }}
import (
"github.com/hashicorp/hcl/v2/hcldec"
{{- if .ImportCty }}
"github.com/zclconf/go-cty/cty"
{{end -}}
)
{{ range .StructDefs }}
{{ $StructName := .StructName}}
func (*{{ .StructName }}) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
{{- range .Fields}}
"{{ .Name }}": {{ .Spec }},
{{- end }}
{{- range .Nested}}
"{{ .Accessor }}": &hcldec.BlockObjectSpec{TypeName: "{{ .TypeName }}", LabelNames: []string(nil), Nested: hcldec.ObjectSpec((&{{ $StructName }}{}).{{ .FieldName }}.HCL2Spec())},
{{- end }}
{{- range .NestedList }}
"{{ .Accessor }}": &hcldec.BlockListSpec{TypeName: "{{ .TypeName }}", Nested: hcldec.ObjectSpec((&{{ $StructName }}{}).{{ .FieldName }}[0].HCL2Spec()) },
{{- end}}
}
{{- range .Squashed }}
for k,v := range (&{{ $StructName }}{}).{{ . }}.HCL2Spec() {
s[k] = v
}
{{- end}}
return s
}
{{end}}`))
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
func ToSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}