505 lines
12 KiB
Go
505 lines
12 KiB
Go
|
package schemamd
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
|
||
|
tfjson "github.com/hashicorp/terraform-json"
|
||
|
"github.com/zclconf/go-cty/cty"
|
||
|
)
|
||
|
|
||
|
// Render writes a Markdown formatted Schema definition to the specified writer.
|
||
|
// A Schema contains a Version and the root Block, for example:
|
||
|
// "aws_accessanalyzer_analyzer": {
|
||
|
// "block": {
|
||
|
// },
|
||
|
// "version": 0
|
||
|
// },
|
||
|
func Render(schema *tfjson.Schema, w io.Writer) error {
|
||
|
_, err := io.WriteString(w, "## Schema\n\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = writeRootBlock(w, schema.Block)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("unable to render schema: %w", err)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Group by Attribute/Block characteristics.
|
||
|
type groupFilter struct {
|
||
|
topLevelTitle string
|
||
|
nestedTitle string
|
||
|
|
||
|
filterAttribute func(att *tfjson.SchemaAttribute) bool
|
||
|
filterBlock func(block *tfjson.SchemaBlockType) bool
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
// Attributes and Blocks are in one of 3 characteristic groups:
|
||
|
// * Required
|
||
|
// * Optional
|
||
|
// * Read-Only
|
||
|
groupFilters = []groupFilter{
|
||
|
{"### Required", "Required:", childAttributeIsRequired, childBlockIsRequired},
|
||
|
{"### Optional", "Optional:", childAttributeIsOptional, childBlockIsOptional},
|
||
|
{"### Read-Only", "Read-Only:", childAttributeIsReadOnly, childBlockIsReadOnly},
|
||
|
}
|
||
|
)
|
||
|
|
||
|
type nestedType struct {
|
||
|
anchorID string
|
||
|
path []string
|
||
|
block *tfjson.SchemaBlock
|
||
|
object *cty.Type
|
||
|
attrs *tfjson.SchemaNestedAttributeType
|
||
|
|
||
|
group groupFilter
|
||
|
}
|
||
|
|
||
|
func writeAttribute(w io.Writer, path []string, att *tfjson.SchemaAttribute, group groupFilter) ([]nestedType, error) {
|
||
|
name := path[len(path)-1]
|
||
|
|
||
|
_, err := io.WriteString(w, "- `"+name+"` ")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if name == "id" && att.Description == "" {
|
||
|
att.Description = "The ID of this resource."
|
||
|
}
|
||
|
|
||
|
if att.AttributeNestedType == nil {
|
||
|
err = WriteAttributeDescription(w, att, false)
|
||
|
} else {
|
||
|
err = WriteNestedAttributeTypeDescription(w, att, false)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if att.AttributeType.IsTupleType() {
|
||
|
return nil, fmt.Errorf("TODO: tuples are not yet supported")
|
||
|
}
|
||
|
|
||
|
anchorID := "nestedatt--" + strings.Join(path, "--")
|
||
|
nestedTypes := []nestedType{}
|
||
|
switch {
|
||
|
case att.AttributeNestedType != nil:
|
||
|
_, err = io.WriteString(w, " (see [below for nested schema](#"+anchorID+"))")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nestedType{
|
||
|
anchorID: anchorID,
|
||
|
path: path,
|
||
|
attrs: att.AttributeNestedType,
|
||
|
|
||
|
group: group,
|
||
|
})
|
||
|
case att.AttributeType.IsObjectType():
|
||
|
_, err = io.WriteString(w, " (see [below for nested schema](#"+anchorID+"))")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nestedType{
|
||
|
anchorID: anchorID,
|
||
|
path: path,
|
||
|
object: &att.AttributeType,
|
||
|
|
||
|
group: group,
|
||
|
})
|
||
|
case att.AttributeType.IsCollectionType() && att.AttributeType.ElementType().IsObjectType():
|
||
|
_, err = io.WriteString(w, " (see [below for nested schema](#"+anchorID+"))")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
nt := att.AttributeType.ElementType()
|
||
|
nestedTypes = append(nestedTypes, nestedType{
|
||
|
anchorID: anchorID,
|
||
|
path: path,
|
||
|
object: &nt,
|
||
|
|
||
|
group: group,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return nestedTypes, nil
|
||
|
}
|
||
|
|
||
|
func writeBlockType(w io.Writer, path []string, block *tfjson.SchemaBlockType) ([]nestedType, error) {
|
||
|
name := path[len(path)-1]
|
||
|
|
||
|
_, err := io.WriteString(w, "- `"+name+"` ")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
err = WriteBlockTypeDescription(w, block)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("unable to write block description for %q: %w", name, err)
|
||
|
}
|
||
|
|
||
|
anchorID := "nestedblock--" + strings.Join(path, "--")
|
||
|
nt := nestedType{
|
||
|
anchorID: anchorID,
|
||
|
path: path,
|
||
|
block: block.Block,
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, " (see [below for nested schema](#"+anchorID+"))")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return []nestedType{nt}, nil
|
||
|
}
|
||
|
|
||
|
func writeRootBlock(w io.Writer, block *tfjson.SchemaBlock) error {
|
||
|
return writeBlockChildren(w, nil, block, true)
|
||
|
}
|
||
|
|
||
|
// A Block contains:
|
||
|
// * Attributes (arbitrarily nested)
|
||
|
// * Nested Blocks (with nesting mode, max and min items)
|
||
|
// * Description(Kind)
|
||
|
// * Deprecated flag
|
||
|
// For example:
|
||
|
// "block": {
|
||
|
// "attributes": {
|
||
|
// "certificate_arn": {
|
||
|
// "description_kind": "plain",
|
||
|
// "required": true,
|
||
|
// "type": "string"
|
||
|
// }
|
||
|
// },
|
||
|
// "block_types": {
|
||
|
// "timeouts": {
|
||
|
// "block": {
|
||
|
// "attributes": {
|
||
|
// },
|
||
|
// "description_kind": "plain"
|
||
|
// },
|
||
|
// "nesting_mode": "single"
|
||
|
// }
|
||
|
// },
|
||
|
// "description_kind": "plain"
|
||
|
// },
|
||
|
func writeBlockChildren(w io.Writer, parents []string, block *tfjson.SchemaBlock, root bool) error {
|
||
|
names := []string{}
|
||
|
for n := range block.Attributes {
|
||
|
names = append(names, n)
|
||
|
}
|
||
|
for n := range block.NestedBlocks {
|
||
|
names = append(names, n)
|
||
|
}
|
||
|
|
||
|
groups := map[int][]string{}
|
||
|
|
||
|
// Group Attributes/Blocks by characteristics.
|
||
|
nameLoop:
|
||
|
for _, n := range names {
|
||
|
if childBlock, ok := block.NestedBlocks[n]; ok {
|
||
|
for i, gf := range groupFilters {
|
||
|
if gf.filterBlock(childBlock) {
|
||
|
groups[i] = append(groups[i], n)
|
||
|
continue nameLoop
|
||
|
}
|
||
|
}
|
||
|
} else if childAtt, ok := block.Attributes[n]; ok {
|
||
|
for i, gf := range groupFilters {
|
||
|
if gf.filterAttribute(childAtt) {
|
||
|
groups[i] = append(groups[i], n)
|
||
|
continue nameLoop
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("no match for %q, this can happen if you have incompatible schema defined, for example an "+
|
||
|
"optional block where all the child attributes are computed, in which case the block itself should also "+
|
||
|
"be marked computed", n)
|
||
|
}
|
||
|
|
||
|
nestedTypes := []nestedType{}
|
||
|
|
||
|
// For each characteristic group
|
||
|
// If Attribute
|
||
|
// Write out summary including characteristic and type (if primitive type or collection of primitives)
|
||
|
// If NestedAttribute type, Object type or collection of Objects, add to list of nested types
|
||
|
// ElseIf Block
|
||
|
// Write out summary including characteristic
|
||
|
// Add block to list of nested types
|
||
|
// End
|
||
|
// End
|
||
|
// For each nested type:
|
||
|
// Write out heading
|
||
|
// If Block
|
||
|
// Recursively call this function (writeBlockChildren)
|
||
|
// ElseIf Object
|
||
|
// Call writeObjectChildren, which
|
||
|
// For each Object Attribute
|
||
|
// Write out summary including characteristic and type (if primitive type or collection of primitives)
|
||
|
// If Object type or collection of Objects, add to list of nested types
|
||
|
// End
|
||
|
// Recursively do nested type functionality
|
||
|
// ElseIf NestedAttribute
|
||
|
// Call writeNestedAttributeChildren, which
|
||
|
// For each nested Attribute
|
||
|
// Write out summary including characteristic and type (if primitive type or collection of primitives)
|
||
|
// If NestedAttribute type, Object type or collection of Objects, add to list of nested types
|
||
|
// End
|
||
|
// Recursively do nested type functionality
|
||
|
// End
|
||
|
// End
|
||
|
for i, gf := range groupFilters {
|
||
|
sortedNames := groups[i]
|
||
|
if len(sortedNames) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
sort.Strings(sortedNames)
|
||
|
|
||
|
groupTitle := gf.topLevelTitle
|
||
|
if !root {
|
||
|
groupTitle = gf.nestedTitle
|
||
|
}
|
||
|
|
||
|
_, err := io.WriteString(w, groupTitle+"\n\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, name := range sortedNames {
|
||
|
path := append(parents, name)
|
||
|
|
||
|
if childBlock, ok := block.NestedBlocks[name]; ok {
|
||
|
nt, err := writeBlockType(w, path, childBlock)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("unable to render block %q: %w", name, err)
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nt...)
|
||
|
continue
|
||
|
} else if childAtt, ok := block.Attributes[name]; ok {
|
||
|
nt, err := writeAttribute(w, path, childAtt, gf)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("unable to render attribute %q: %w", name, err)
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nt...)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("unexpected name in schema render %q", name)
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
err := writeNestedTypes(w, nestedTypes)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func writeNestedTypes(w io.Writer, nestedTypes []nestedType) error {
|
||
|
for _, nt := range nestedTypes {
|
||
|
_, err := io.WriteString(w, "<a id=\""+nt.anchorID+"\"></a>\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "### Nested Schema for `"+strings.Join(nt.path, ".")+"`\n\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
switch {
|
||
|
case nt.block != nil:
|
||
|
err = writeBlockChildren(w, nt.path, nt.block, false)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
case nt.object != nil:
|
||
|
err = writeObjectChildren(w, nt.path, *nt.object, nt.group)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
case nt.attrs != nil:
|
||
|
err = writeNestedAttributeChildren(w, nt.path, nt.attrs, nt.group)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
default:
|
||
|
return fmt.Errorf("missing information on nested block: %s", strings.Join(nt.path, "."))
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func writeObjectAttribute(w io.Writer, path []string, att cty.Type, group groupFilter) ([]nestedType, error) {
|
||
|
name := path[len(path)-1]
|
||
|
|
||
|
_, err := io.WriteString(w, "- `"+name+"` (")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
err = WriteType(w, att)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, ")")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if att.IsTupleType() {
|
||
|
return nil, fmt.Errorf("TODO: tuples are not yet supported")
|
||
|
}
|
||
|
|
||
|
anchorID := "nestedobjatt--" + strings.Join(path, "--")
|
||
|
nestedTypes := []nestedType{}
|
||
|
switch {
|
||
|
case att.IsObjectType():
|
||
|
_, err = io.WriteString(w, " (see [below for nested schema](#"+anchorID+"))")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nestedType{
|
||
|
anchorID: anchorID,
|
||
|
path: path,
|
||
|
object: &att,
|
||
|
|
||
|
group: group,
|
||
|
})
|
||
|
case att.IsCollectionType() && att.ElementType().IsObjectType():
|
||
|
_, err = io.WriteString(w, " (see [below for nested schema](#"+anchorID+"))")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
nt := att.ElementType()
|
||
|
nestedTypes = append(nestedTypes, nestedType{
|
||
|
anchorID: anchorID,
|
||
|
path: path,
|
||
|
object: &nt,
|
||
|
|
||
|
group: group,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return nestedTypes, nil
|
||
|
}
|
||
|
|
||
|
func writeObjectChildren(w io.Writer, parents []string, ty cty.Type, group groupFilter) error {
|
||
|
_, err := io.WriteString(w, group.nestedTitle+"\n\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
atts := ty.AttributeTypes()
|
||
|
sortedNames := []string{}
|
||
|
for n := range atts {
|
||
|
sortedNames = append(sortedNames, n)
|
||
|
}
|
||
|
sort.Strings(sortedNames)
|
||
|
nestedTypes := []nestedType{}
|
||
|
|
||
|
for _, name := range sortedNames {
|
||
|
att := atts[name]
|
||
|
path := append(parents, name)
|
||
|
|
||
|
nt, err := writeObjectAttribute(w, path, att, group)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("unable to render attribute %q: %w", name, err)
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nt...)
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = writeNestedTypes(w, nestedTypes)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func writeNestedAttributeChildren(w io.Writer, parents []string, nestedAttributes *tfjson.SchemaNestedAttributeType, group groupFilter) error {
|
||
|
_, err := io.WriteString(w, group.nestedTitle+"\n\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
sortedNames := []string{}
|
||
|
for n := range nestedAttributes.Attributes {
|
||
|
sortedNames = append(sortedNames, n)
|
||
|
}
|
||
|
sort.Strings(sortedNames)
|
||
|
nestedTypes := []nestedType{}
|
||
|
|
||
|
for _, name := range sortedNames {
|
||
|
att := nestedAttributes.Attributes[name]
|
||
|
path := append(parents, name)
|
||
|
|
||
|
nt, err := writeAttribute(w, path, att, group)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("unable to render attribute %q: %w", name, err)
|
||
|
}
|
||
|
|
||
|
nestedTypes = append(nestedTypes, nt...)
|
||
|
}
|
||
|
|
||
|
_, err = io.WriteString(w, "\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = writeNestedTypes(w, nestedTypes)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|