Files
terraform-docs/internal/pkg/doc/doc.go
2018-09-21 08:49:52 +02:00

305 lines
6.1 KiB
Go

package doc
import (
"fmt"
"io/ioutil"
"log"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/segmentio/terraform-docs/internal/pkg/fs"
)
// Input represents a terraform input variable.
type Input struct {
Name string
Description string
Default *Value
Type string
}
// Value returns the default value as a string.
func (i *Input) Value() string {
if i.Default != nil {
switch i.Default.Type {
case "string":
return i.Default.Literal
case "map":
return "<map>"
case "list":
return "<list>"
}
}
return "required"
}
// Value represents a terraform value.
type Value struct {
Type string
Literal string
}
// Output represents a terraform output.
type Output struct {
Name string
Description string
}
// Doc represents a terraform module doc.
type Doc struct {
Comment string
Inputs []Input
Outputs []Output
}
type inputsByName []Input
func (a inputsByName) Len() int { return len(a) }
func (a inputsByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a inputsByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
type outputsByName []Output
func (a outputsByName) Len() int { return len(a) }
func (a outputsByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a outputsByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
// CreateFromPaths creates a *Doc from a list of paths.
func CreateFromPaths(paths []string) (*Doc, error) {
names := make([]string, 0)
for _, path := range paths {
if fs.DirectoryExists(path) {
matches, err := filepath.Glob(fmt.Sprintf("%s/*.tf", path))
if err != nil {
log.Fatal(err)
}
names = append(names, matches...)
} else if fs.FileExists(path) {
names = append(names, path)
}
}
files := make(map[string]*ast.File)
for _, name := range names {
bytes, err := ioutil.ReadFile(name)
if err != nil {
log.Fatal(err)
panic(err)
}
ast, err := hcl.ParseBytes(bytes)
if err != nil {
log.Fatal(err)
panic(err)
}
files[name] = ast
}
return Create(files), nil
}
// Create creates a new *Doc from the supplied map
// of filenames and *ast.File.
func Create(files map[string]*ast.File) *Doc {
doc := new(Doc)
for name, f := range files {
list := f.Node.(*ast.ObjectList)
doc.Inputs = append(doc.Inputs, inputs(list)...)
doc.Outputs = append(doc.Outputs, outputs(list)...)
filename := path.Base(name)
comments := f.Comments
if filename == "main.tf" && len(comments) > 0 {
doc.Comment = header(comments[0])
}
}
sort.Sort(inputsByName(doc.Inputs))
sort.Sort(outputsByName(doc.Outputs))
return doc
}
// Inputs returns all variables from `list`.
func inputs(list *ast.ObjectList) []Input {
var ret []Input
for _, item := range list.Items {
if is(item, "variable") {
name, _ := strconv.Unquote(item.Keys[1].Token.Text)
if name == "" {
name = item.Keys[1].Token.Text
}
items := item.Val.(*ast.ObjectType).List.Items
var desc string
switch {
case description(items) != "":
desc = description(items)
case item.LeadComment != nil:
desc = comment(item.LeadComment.List)
}
var itemsType = get(items, "type")
var itemType string
if itemsType == nil || itemsType.Literal == "" {
itemType = "string"
} else {
itemType = itemsType.Literal
}
def := get(items, "default")
ret = append(ret, Input{
Name: name,
Description: desc,
Default: def,
Type: itemType,
})
}
}
return ret
}
// Outputs returns all outputs from `list`.
func outputs(list *ast.ObjectList) []Output {
var ret []Output
for _, item := range list.Items {
if is(item, "output") {
name, _ := strconv.Unquote(item.Keys[1].Token.Text)
if name == "" {
name = item.Keys[1].Token.Text
}
items := item.Val.(*ast.ObjectType).List.Items
var desc string
switch {
case description(items) != "":
desc = description(items)
case item.LeadComment != nil:
desc = comment(item.LeadComment.List)
}
ret = append(ret, Output{
Name: name,
Description: strings.TrimSpace(desc),
})
}
}
return ret
}
// Get `key` from the list of object `items`.
func get(items []*ast.ObjectItem, key string) *Value {
for _, item := range items {
if is(item, key) {
v := new(Value)
if lit, ok := item.Val.(*ast.LiteralType); ok {
if value, ok := lit.Token.Value().(string); ok {
v.Literal = value
} else {
v.Literal = lit.Token.Text
}
v.Type = "string"
return v
}
if _, ok := item.Val.(*ast.ObjectType); ok {
v.Type = "map"
return v
}
if _, ok := item.Val.(*ast.ListType); ok {
v.Type = "list"
return v
}
return nil
}
}
return nil
}
// description returns a description from items or an empty string.
func description(items []*ast.ObjectItem) string {
if v := get(items, "description"); v != nil {
return v.Literal
}
return ""
}
// Is returns true if `item` is of `kind`.
func is(item *ast.ObjectItem, kind string) bool {
if len(item.Keys) > 0 {
return item.Keys[0].Token.Text == kind
}
return false
}
// Comment cleans and returns a comment.
func comment(l []*ast.Comment) string {
var line string
var ret string
for _, t := range l {
line = strings.TrimSpace(t.Text)
line = strings.TrimPrefix(line, "#")
line = strings.TrimPrefix(line, "//")
ret += strings.TrimSpace(line)
}
return ret
}
// Header returns the header comment from the list
// or an empty comment. The head comment must start
// at line 1 and start with `/**`.
func header(c *ast.CommentGroup) (comment string) {
if len(c.List) == 0 {
return comment
}
if c.Pos().Line != 1 {
return comment
}
cm := strings.TrimSpace(c.List[0].Text)
if strings.HasPrefix(cm, "/**") {
lines := strings.Split(cm, "\n")
if len(lines) < 2 {
return comment
}
lines = lines[1 : len(lines)-1]
for _, l := range lines {
l = strings.TrimSpace(l)
switch {
case strings.TrimPrefix(l, "* ") != l:
l = strings.TrimPrefix(l, "* ")
default:
l = strings.TrimPrefix(l, "*")
}
comment += l + "\n"
}
}
return comment
}