You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraformDummyRepo2/vendor/github.com/hashicorp/terraform-plugin-docs/schemamd/render.go

538 lines
13 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
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 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 {
// By default, the attribute `id` is place in the "Read-Only" group
// if the provider schema contained no `.Description` for it.
//
// If a `.Description` is provided instead, the behaviour will be the
// same as for every other attribute.
if strings.ToLower(n) == "id" && childAtt.Description == "" {
if strings.Contains(gf.topLevelTitle, "Read-Only") {
childAtt.Description = "The ID of this resource."
groups[i] = append(groups[i], n)
continue nameLoop
}
} else 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 := make([]string, len(parents), len(parents)+1)
copy(path, parents)
path = append(path, 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 {
sortedNames := []string{}
for n := range nestedAttributes.Attributes {
sortedNames = append(sortedNames, n)
}
sort.Strings(sortedNames)
groups := map[int][]string{}
for _, name := range sortedNames {
att := nestedAttributes.Attributes[name]
for i, gf := range groupFilters {
if gf.filterAttribute(att) {
groups[i] = append(groups[i], name)
}
}
}
nestedTypes := []nestedType{}
for i, gf := range groupFilters {
names, ok := groups[i]
if !ok || len(names) == 0 {
continue
}
_, err := io.WriteString(w, gf.nestedTitle+"\n\n")
if err != nil {
return err
}
for _, name := range names {
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
}