pulumi/developer-docs/utils/jsonschema2md.go
Pat Gavlin 236ce54269
[schema] Add the Pulumi Package metaschema. (#7952)
The Pulumi Package metaschema is a JSON schema definition that describes
the format of a Pulumi Package schema. The metaschema can be used to
validate certain basic properties of a Pulumi Package schema, including
(but not limited to):

- data types (e.g. is this property a string?)
- data formats (e.g. is this string property a valid regex?)
- object shapes (e.g. is this object missing required properties?)

The schema binder has been updated to use the metaschema as its first
validation pass.

In addition to its use in the binder, the metaschema has its own page in
the developer documentation. This page is generated using a small tool,
jsonschema2md.go.
2021-09-20 12:00:42 -07:00

385 lines
8.9 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"regexp"
"sort"
"strings"
"github.com/santhosh-tekuri/jsonschema/v5"
)
var punctuationRegexp = regexp.MustCompile(`[^\w\- ]`)
// ref: https://github.com/gjtorikian/html-pipeline/blob/main/lib/html/pipeline/toc_filter.rb
func gfmHeaderAnchor(header string) string {
header = strings.ToLower(header)
header = punctuationRegexp.ReplaceAllString(header, "")
return "#" + strings.ReplaceAll(header, " ", "-")
}
func fprintf(w io.Writer, f string, args ...interface{}) {
_, err := fmt.Fprintf(w, f, args...)
if err != nil {
log.Fatal(err)
}
}
func toJSON(v interface{}) string {
bytes, err := json.Marshal(v)
if err != nil {
log.Fatal(err)
}
return string(bytes)
}
func schemaItems(schema *jsonschema.Schema) *jsonschema.Schema {
if schema.Items2020 != nil {
return schema.Items2020
}
if items, ok := schema.Items.(*jsonschema.Schema); ok {
return items
}
return nil
}
type converter struct {
w io.Writer
rootLocation string
defs map[string]*jsonschema.Schema
}
func (c *converter) printf(f string, args ...interface{}) {
fprintf(c.w, f, args...)
}
func (c *converter) inlineDef(schema *jsonschema.Schema) bool {
return schema.Description == "" &&
schema.Title == "" &&
schema.Format == "" &&
len(schema.Properties) == 0 &&
len(schema.AllOf) == 0 &&
len(schema.AnyOf) == 0 &&
len(schema.OneOf) == 0 &&
schema.If == nil &&
schema.PropertyNames == nil &&
len(schema.PatternProperties) == 0 &&
schema.Items == nil &&
schema.AdditionalItems == nil &&
len(schema.PrefixItems) == 0 &&
schema.Items2020 == nil &&
schema.Contains == nil &&
schema.Pattern == nil
}
func (c *converter) recordDef(schema *jsonschema.Schema) {
if schema != nil && strings.HasPrefix(schema.Location, c.rootLocation) {
if _, has := c.defs[schema.Location]; !has {
c.defs[schema.Location] = schema
c.collectDefs(schema)
}
}
}
func (c *converter) recordDefs(schemas []*jsonschema.Schema) {
for _, schema := range schemas {
c.recordDef(schema)
}
}
func (c *converter) collectDefs(schema *jsonschema.Schema) {
c.recordDef(schema.Ref)
c.recordDef(schema.RecursiveRef)
c.recordDef(schema.DynamicRef)
c.recordDef(schema.Not)
c.recordDefs(schema.AllOf)
c.recordDefs(schema.AnyOf)
c.recordDefs(schema.OneOf)
c.recordDef(schema.If)
c.recordDef(schema.Then)
c.recordDef(schema.Else)
for _, schema := range schema.Properties {
c.collectDefs(schema)
}
c.recordDef(schema.PropertyNames)
for _, schema := range schema.PatternProperties {
c.collectDefs(schema)
}
if child, ok := schema.AdditionalProperties.(*jsonschema.Schema); ok {
c.recordDef(child)
}
for _, dep := range schema.Dependencies {
if schema, ok := dep.(*jsonschema.Schema); ok {
c.recordDef(schema)
}
}
for _, schema := range schema.DependentSchemas {
c.recordDef(schema)
}
c.recordDef(schema.UnevaluatedProperties)
switch items := schema.Items.(type) {
case *jsonschema.Schema:
c.recordDef(items)
case []*jsonschema.Schema:
c.recordDefs(items)
}
if child, ok := schema.AdditionalItems.(*jsonschema.Schema); ok {
c.recordDef(child)
}
c.recordDefs(schema.PrefixItems)
c.recordDef(schema.Items2020)
c.recordDef(schema.Contains)
c.recordDef(schema.UnevaluatedItems)
}
func (c *converter) schemaTitle(schema *jsonschema.Schema) string {
if schema.Title != "" {
return schema.Title
}
return "`" + schema.Location + "`"
}
func (c *converter) refLink(ref *jsonschema.Schema) string {
dest := ref.Location
if strings.HasPrefix(ref.Location, c.rootLocation) {
dest = gfmHeaderAnchor(c.schemaTitle(ref))
}
return fmt.Sprintf("[%v](%v)", c.schemaTitle(ref), dest)
}
func (c *converter) ref(ref *jsonschema.Schema) string {
if !c.inlineDef(ref) {
return c.refLink(ref)
}
if ref.Ref != nil {
return c.ref(ref.Ref)
}
if len(ref.Constant) != 0 {
return c.schemaConstant(ref)
}
if len(ref.Enum) != 0 {
return c.schemaEnum(ref)
}
return c.schemaTypes(ref)
}
func (c *converter) schemaTypes(schema *jsonschema.Schema) string {
types := schema.Types
if len(types) == 1 {
return fmt.Sprintf("`%v`", types[0])
}
var sb strings.Builder
for i, t := range types {
if i != 0 {
fprintf(&sb, " | ")
}
fprintf(&sb, "`%v`", t)
}
return sb.String()
}
func (c *converter) convertSchemaTypes(schema *jsonschema.Schema) {
types := schema.Types
switch len(types) {
case 0:
// Nothing to do
case 1:
c.printf("\n%v\n", c.schemaTypes(schema))
default:
c.printf("\n%v\n", c.schemaTypes(schema))
}
}
func (c *converter) convertSchemaStringValidators(schema *jsonschema.Schema) {
if schema.Format != "" {
c.printf("\nFormat: `%v`\n", schema.Format)
}
if schema.Pattern != nil {
c.printf("\nPattern: `%v`\n", schema.Pattern)
}
}
func (c *converter) convertSchemaRef(schema *jsonschema.Schema) {
if schema.Ref != nil {
c.printf("\n%v\n", c.refLink(schema.Ref))
}
}
func (c *converter) schemaConstant(schema *jsonschema.Schema) string {
return fmt.Sprintf("`%s`", toJSON(schema.Constant[0]))
}
func (c *converter) convertSchemaConstant(schema *jsonschema.Schema) {
if len(schema.Constant) != 0 {
c.printf("\nConstant: %v\n", c.schemaConstant(schema))
}
}
func (c *converter) schemaEnum(schema *jsonschema.Schema) string {
var sb strings.Builder
for i, v := range schema.Enum {
if i != 0 {
sb.WriteString(" | ")
}
fprintf(&sb, "`%s`", toJSON(v))
}
return sb.String()
}
func (c *converter) convertSchemaEnum(schema *jsonschema.Schema) {
if len(schema.Enum) != 0 {
c.printf("\nEnum: %v\n", c.schemaEnum(schema))
}
}
func (c *converter) convertSchemaLogic(schema *jsonschema.Schema) {
if len(schema.AllOf) != 0 {
c.printf("\nAll of:\n")
for _, ref := range schema.AllOf {
c.printf("- %v\n", c.ref(ref))
}
}
if len(schema.AnyOf) != 0 {
c.printf("\nAny of:\n")
for _, ref := range schema.AllOf {
c.printf("- %v\n", c.ref(ref))
}
}
if len(schema.OneOf) != 0 {
c.printf("\nOne of:\n")
for _, ref := range schema.AllOf {
c.printf("- %v\n", c.ref(ref))
}
}
if schema.If != nil {
c.printf("\nIf %v", c.ref(schema.If))
if schema.Then != nil {
c.printf(", then %v", c.ref(schema.Then))
}
if schema.Else != nil {
c.printf(", else %v", c.ref(schema.Else))
}
c.printf("\n")
}
}
func (c *converter) convertSchemaObject(schema *jsonschema.Schema, level int) {
if schema.PropertyNames != nil {
c.printf("\nProperty names: %v\n", c.ref(schema.PropertyNames))
}
if additionalProperties, ok := schema.AdditionalProperties.(*jsonschema.Schema); ok {
c.printf("\nAdditional properties: %v\n", c.ref(additionalProperties))
}
required := map[string]bool{}
for _, name := range schema.Required {
required[name] = true
}
properties := make([]string, 0, len(schema.Properties))
for name, schema := range schema.Properties {
if schema.Always != nil && !*schema.Always {
continue
}
properties = append(properties, name)
}
sort.Strings(properties)
if len(properties) != 0 {
c.printf("\n%v Properties\n", strings.Repeat("#", level+1))
c.printf("\n---\n")
for _, name := range properties {
c.printf("\n%s `%s`", strings.Repeat("#", level+2), name)
if required[name] {
c.printf(" (_required_)")
}
c.printf("\n")
c.convertSchema(schema.Properties[name], level+2)
c.printf("\n---\n")
}
}
}
func (c *converter) convertSchemaArray(schema *jsonschema.Schema) {
if items := schemaItems(schema); items != nil {
c.printf("\nItems: %v\n", c.ref(items))
}
}
func (c *converter) convertSchema(schema *jsonschema.Schema, level int) {
if schema.Description != "" {
c.printf("\n%s\n", schema.Description)
}
c.convertSchemaTypes(schema)
c.convertSchemaConstant(schema)
c.convertSchemaEnum(schema)
c.convertSchemaStringValidators(schema)
c.convertSchemaRef(schema)
c.convertSchemaLogic(schema)
c.convertSchemaArray(schema)
c.convertSchemaObject(schema, level)
}
func (c *converter) convertRootSchema(schema *jsonschema.Schema) {
c.collectDefs(schema)
c.printf("# %s\n", c.schemaTitle(schema))
c.convertSchema(schema, 1)
defs := make([]*jsonschema.Schema, 0, len(c.defs))
for _, def := range c.defs {
defs = append(defs, def)
}
sort.Slice(defs, func(i, j int) bool {
return c.schemaTitle(defs[i]) < c.schemaTitle(defs[j])
})
for _, def := range defs {
if !c.inlineDef(def) {
c.printf("\n## %s\n", c.schemaTitle(def))
c.convertSchema(def, 2)
}
}
}
func main() {
schemaBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
compiler := jsonschema.NewCompiler()
compiler.ExtractAnnotations = true
compiler.LoadURL = func(s string) (io.ReadCloser, error) {
if s == "blob://stdin" {
return ioutil.NopCloser(bytes.NewReader(schemaBytes)), nil
}
return jsonschema.LoadURL(s)
}
schema, err := compiler.Compile("blob://stdin")
if err != nil {
log.Fatal(err)
}
converter := converter{
w: os.Stdout,
rootLocation: schema.Location,
defs: map[string]*jsonschema.Schema{},
}
converter.convertRootSchema(schema)
}