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.
385 lines
8.9 KiB
Go
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)
|
|
}
|