2022-04-03 04:07:16 +00:00
|
|
|
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 {
|
2022-12-18 17:14:23 +00:00
|
|
|
// 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) {
|
2022-04-03 04:07:16 +00:00
|
|
|
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 {
|
2022-12-18 17:14:23 +00:00
|
|
|
path := make([]string, len(parents), len(parents)+1)
|
|
|
|
copy(path, parents)
|
|
|
|
path = append(path, name)
|
2022-04-03 04:07:16 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2022-12-18 17:14:23 +00:00
|
|
|
groups := map[int][]string{}
|
2022-04-03 04:07:16 +00:00
|
|
|
for _, name := range sortedNames {
|
|
|
|
att := nestedAttributes.Attributes[name]
|
|
|
|
|
2022-12-18 17:14:23 +00:00
|
|
|
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")
|
2022-04-03 04:07:16 +00:00
|
|
|
if err != nil {
|
2022-12-18 17:14:23 +00:00
|
|
|
return err
|
2022-04-03 04:07:16 +00:00
|
|
|
}
|
|
|
|
|
2022-12-18 17:14:23 +00:00
|
|
|
for _, name := range names {
|
|
|
|
att := nestedAttributes.Attributes[name]
|
|
|
|
path := append(parents, name)
|
2022-04-03 04:07:16 +00:00
|
|
|
|
2022-12-18 17:14:23 +00:00
|
|
|
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
|
|
|
|
}
|
2022-04-03 04:07:16 +00:00
|
|
|
}
|
|
|
|
|
2022-12-18 17:14:23 +00:00
|
|
|
err := writeNestedTypes(w, nestedTypes)
|
2022-04-03 04:07:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|