pulumi/pkg/codegen/schema/docs_test.go
Pat Gavlin 3d4c6523d2
[schema/docs] Use a Markdown parser. (#4838)
In particular, use the parser to filter and extract examples. This also
sets up support for entity references in documentation that can be used
in order to render language-specific names for resources, functions,
types, and properties.

Related to #4632 and #4159.
2020-06-17 14:02:45 -07:00

203 lines
5.5 KiB
Go

package schema
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"path"
"path/filepath"
"strings"
"testing"
"github.com/pgavlin/goldmark/ast"
"github.com/pgavlin/goldmark/testutil"
"github.com/stretchr/testify/assert"
)
var testdataPath = filepath.Join("..", "internal", "test", "testdata")
var nodeAssertions = testutil.DefaultNodeAssertions().Union(testutil.NodeAssertions{
KindShortcode: func(t *testing.T, sourceExpected, sourceActual []byte, expected, actual ast.Node) bool {
shortcodeExpected, shortcodeActual := expected.(*Shortcode), actual.(*Shortcode)
return testutil.AssertEqualBytes(t, shortcodeExpected.Name, shortcodeActual.Name)
},
})
type doc struct {
entity string
content string
}
func getDocsForProperty(parent string, p *Property) []doc {
entity := path.Join(parent, p.Name)
return []doc{
{entity: entity + "/description", content: p.Comment},
{entity: entity + "/deprecationMessage", content: p.DeprecationMessage},
}
}
func getDocsForObjectType(path string, t *ObjectType) []doc {
if t == nil {
return nil
}
docs := []doc{{entity: path + "/description", content: t.Comment}}
for _, p := range t.Properties {
docs = append(docs, getDocsForProperty(path+"/properties", p)...)
}
return docs
}
func getDocsForFunction(f *Function) []doc {
entity := "#/functions/" + url.PathEscape(f.Token)
docs := []doc{
{entity: entity + "/description", content: f.Comment},
{entity: entity + "/deprecationMessage", content: f.DeprecationMessage},
}
docs = append(docs, getDocsForObjectType(entity+"/inputs/properties", f.Inputs)...)
docs = append(docs, getDocsForObjectType(entity+"/outputs/properties", f.Outputs)...)
return docs
}
func getDocsForResource(r *Resource, isProvider bool) []doc {
var entity string
if isProvider {
entity = "#/provider"
} else {
entity = "#/resources/" + url.PathEscape(r.Token)
}
docs := []doc{
{entity: entity + "/description", content: r.Comment},
{entity: entity + "/deprecationMessage", content: r.DeprecationMessage},
}
for _, p := range r.InputProperties {
docs = append(docs, getDocsForProperty(entity+"/inputProperties", p)...)
}
for _, p := range r.Properties {
docs = append(docs, getDocsForProperty(entity+"/properties", p)...)
}
docs = append(docs, getDocsForObjectType(entity+"/stateInputs", r.StateInputs)...)
return docs
}
func getDocsForPackage(pkg *Package) []doc {
var allDocs []doc
for _, p := range pkg.Config {
allDocs = append(allDocs, getDocsForProperty("#/config/variables", p)...)
}
for _, f := range pkg.Functions {
allDocs = append(allDocs, getDocsForFunction(f)...)
}
allDocs = append(allDocs, getDocsForResource(pkg.Provider, true)...)
for _, r := range pkg.Resources {
allDocs = append(allDocs, getDocsForResource(r, false)...)
}
for _, t := range pkg.Types {
if obj, ok := t.(*ObjectType); ok {
allDocs = append(allDocs, getDocsForObjectType("#/types", obj)...)
}
}
return allDocs
}
func TestParseAndRenderDocs(t *testing.T) {
files, err := ioutil.ReadDir(testdataPath)
if err != nil {
t.Fatalf("could not read test data: %v", err)
}
for _, f := range files {
if filepath.Ext(f.Name()) != ".json" {
continue
}
t.Run(f.Name(), func(t *testing.T) {
path := filepath.Join(testdataPath, f.Name())
contents, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read %v: %v", path, err)
}
var spec PackageSpec
if err = json.Unmarshal(contents, &spec); err != nil {
t.Fatalf("could not unmarshal package spec: %v", err)
}
pkg, err := ImportSpec(spec, nil)
if err != nil {
t.Fatalf("could not import package: %v", err)
}
for _, doc := range getDocsForPackage(pkg) {
t.Run(doc.entity, func(t *testing.T) {
original := []byte(doc.content)
expected := ParseDocs(original)
rendered := []byte(RenderDocsToString(original, expected))
actual := ParseDocs(rendered)
if !testutil.AssertSameStructure(t, original, rendered, expected, actual, nodeAssertions) {
t.Logf("original: %v", doc.content)
t.Logf("rendered: %v", string(rendered))
}
})
}
})
}
}
func TestReferenceRenderer(t *testing.T) {
files, err := ioutil.ReadDir(testdataPath)
if err != nil {
t.Fatalf("could not read test data: %v", err)
}
for _, f := range files {
if filepath.Ext(f.Name()) != ".json" {
continue
}
t.Run(f.Name(), func(t *testing.T) {
path := filepath.Join(testdataPath, f.Name())
contents, err := ioutil.ReadFile(path)
if err != nil {
t.Fatalf("could not read %v: %v", path, err)
}
var spec PackageSpec
if err = json.Unmarshal(contents, &spec); err != nil {
t.Fatalf("could not unmarshal package spec: %v", err)
}
pkg, err := ImportSpec(spec, nil)
if err != nil {
t.Fatalf("could not import package: %v", err)
}
for _, doc := range getDocsForPackage(pkg) {
t.Run(doc.entity, func(t *testing.T) {
text := []byte(fmt.Sprintf("[entity](%s)", doc.entity))
expected := strings.Replace(doc.entity, "/", "_", -1) + "\n"
parsed := ParseDocs(text)
actual := []byte(RenderDocsToString(text, parsed, WithReferenceRenderer(
func(r *Renderer, w io.Writer, src []byte, l *ast.Link, enter bool) (ast.WalkStatus, error) {
if !enter {
return ast.WalkContinue, nil
}
replaced := bytes.Replace(l.Destination, []byte{'/'}, []byte{'_'}, -1)
if _, err := r.MarkdownRenderer().Write(w, replaced); err != nil {
return ast.WalkStop, err
}
return ast.WalkSkipChildren, nil
})))
assert.Equal(t, expected, string(actual))
})
}
})
}
}