134d7cb818
This schema can be used to validate the contents of a Pulumi deployment. If a deployment validates against this schema, it should be considered syntactically valid, though it may contain certain classes of semantic errors (e.g. references to unknown resources in dependency lists, dependency cycles, etc.). This schema is not yet used for validation in practice and may not be entirely accurate. These changes also add this schema (and the schemas on which it depends) to the developer documentation. jsonschema2md.go has been updated to support multi-file schemas.
434 lines
10 KiB
Go
434 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"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 {
|
|
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 := 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)
|
|
|
|
level := 1
|
|
if c.multiSchema {
|
|
level = 2
|
|
}
|
|
|
|
c.printf("%s %s\n", strings.Repeat("#", level), c.schemaTitle(schema))
|
|
|
|
c.convertSchema(schema, level)
|
|
|
|
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 %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 := make([]*jsonschema.Schema, 0, 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)
|
|
}
|
|
}
|