pulumi/developer-docs/utils/jsonschema2md.go

435 lines
10 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"os"
"regexp"
"sort"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"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 {
multiSchema bool
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 := slice.Prealloc[string](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)
level := 1
if c.multiSchema {
level = 2
}
c.printf("%s %s\n", strings.Repeat("#", level), c.schemaTitle(schema))
c.convertSchema(schema, level)
defs := slice.Prealloc[*jsonschema.Schema](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 %s\n", strings.Repeat("#", level+1), c.schemaTitle(def))
c.convertSchema(def, level+1)
}
}
}
func main() {
title := flag.String("title", "", "the top-level title for the output, if any")
idString := flag.String("ids", "", "a comma-separated list of 'id=path' mappings")
flag.Parse()
const rootID = "blob://stdin"
ids := map[string]string{
rootID: "-",
}
if *idString != "" {
for _, idm := range strings.Split(*idString, ",") {
eq := strings.IndexByte(idm, '=')
if eq == -1 {
log.Fatalf("invalid 'id=path' mapping '%v'", idm)
}
id, path := idm[:eq], idm[eq+1:]
if id == "" || path == "" {
log.Fatalf("invalid 'id=path' mapping '%v'", idm)
}
ids[id] = path
if path == "-" {
delete(ids, rootID)
}
}
if len(ids) > 1 && *title == "" {
log.Fatal("-title is required if more than one ID is mapped")
}
}
compiler := jsonschema.NewCompiler()
compiler.ExtractAnnotations = true
compiler.LoadURL = func(s string) (io.ReadCloser, error) {
if path, ok := ids[s]; ok {
if path == "-" {
return os.Stdin, nil
}
return os.Open(path)
}
return jsonschema.LoadURL(s)
}
schemas := slice.Prealloc[*jsonschema.Schema](len(ids))
for id := range ids {
schema, err := compiler.Compile(id)
if err != nil {
log.Fatal(err)
}
schemas = append(schemas, schema)
}
sort.Slice(schemas, func(i, j int) bool { return schemas[i].Location < schemas[j].Location })
if *title != "" {
fprintf(os.Stdout, "# %v\n", *title)
}
for _, schema := range schemas {
fprintf(os.Stdout, "\n")
converter := converter{
multiSchema: len(ids) > 1,
w: os.Stdout,
rootLocation: schema.Location,
defs: map[string]*jsonschema.Schema{},
}
converter.convertRootSchema(schema)
}
}