2022-10-06 08:23:55 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"log"
|
|
|
|
"os"
|
2022-10-07 08:19:34 +01:00
|
|
|
"sort"
|
2022-10-06 08:23:55 +01:00
|
|
|
"strings"
|
2022-10-07 08:15:43 +01:00
|
|
|
"unicode"
|
2022-10-06 08:23:55 +01:00
|
|
|
|
|
|
|
"github.com/dave/jennifer/jen"
|
|
|
|
|
|
|
|
"git.sr.ht/~emersion/go-jsonschema"
|
|
|
|
)
|
|
|
|
|
|
|
|
func formatId(s string) string {
|
2022-10-07 08:15:43 +01:00
|
|
|
fields := strings.FieldsFunc(s, func(c rune) bool {
|
|
|
|
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
|
|
|
|
})
|
|
|
|
for i, v := range fields {
|
|
|
|
fields[i] = strings.Title(v)
|
|
|
|
}
|
|
|
|
return strings.Join(fields, "")
|
2022-10-06 08:23:55 +01:00
|
|
|
}
|
|
|
|
|
2022-10-06 15:13:46 +01:00
|
|
|
func refName(ref string) string {
|
|
|
|
prefix := "#/$defs/"
|
|
|
|
if !strings.HasPrefix(ref, prefix) {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return strings.TrimPrefix(ref, prefix)
|
|
|
|
}
|
|
|
|
|
2022-10-06 08:23:55 +01:00
|
|
|
func resolveRef(def *jsonschema.Schema, root *jsonschema.Schema) *jsonschema.Schema {
|
|
|
|
if def.Ref == "" {
|
|
|
|
return def
|
|
|
|
}
|
|
|
|
|
2022-10-06 15:13:46 +01:00
|
|
|
name := refName(def.Ref)
|
|
|
|
if name == "" {
|
2022-10-06 08:23:55 +01:00
|
|
|
log.Fatalf("unsupported $ref %q", def.Ref)
|
|
|
|
}
|
|
|
|
|
|
|
|
result, ok := root.Defs[name]
|
|
|
|
if !ok {
|
|
|
|
log.Fatalf("invalid $ref %q", def.Ref)
|
|
|
|
}
|
|
|
|
return &result
|
|
|
|
}
|
|
|
|
|
|
|
|
func schemaType(schema *jsonschema.Schema) jsonschema.Type {
|
|
|
|
if schema.Type != "" {
|
|
|
|
return schema.Type
|
|
|
|
}
|
|
|
|
|
|
|
|
var v interface{}
|
|
|
|
if schema.Const != nil {
|
|
|
|
v = schema.Const
|
|
|
|
} else if len(schema.Enum) > 0 {
|
|
|
|
v = schema.Enum[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
switch v.(type) {
|
|
|
|
case bool:
|
|
|
|
return jsonschema.TypeBoolean
|
|
|
|
case map[string]interface{}:
|
|
|
|
return jsonschema.TypeObject
|
|
|
|
case []interface{}:
|
|
|
|
return jsonschema.TypeArray
|
|
|
|
case float64:
|
|
|
|
return jsonschema.TypeNumber
|
|
|
|
case string:
|
|
|
|
return jsonschema.TypeString
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-06 18:15:48 +01:00
|
|
|
func isRequired(schema *jsonschema.Schema, propName string) bool {
|
|
|
|
for _, name := range schema.Required {
|
|
|
|
if name == propName {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-10-06 15:13:46 +01:00
|
|
|
func generateStruct(schema *jsonschema.Schema, root *jsonschema.Schema) jen.Code {
|
2022-10-07 08:21:00 +01:00
|
|
|
var names []string
|
|
|
|
for name := range schema.Properties {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
|
2022-10-06 15:13:46 +01:00
|
|
|
var fields []jen.Code
|
2022-10-07 08:21:00 +01:00
|
|
|
for _, name := range names {
|
|
|
|
prop := schema.Properties[name]
|
2022-10-06 15:13:46 +01:00
|
|
|
id := formatId(name)
|
2022-10-06 18:15:48 +01:00
|
|
|
required := isRequired(schema, name)
|
2022-10-06 18:39:34 +01:00
|
|
|
t := generateSchemaType(&prop, root, required)
|
2022-10-06 18:15:48 +01:00
|
|
|
jsonTag := name
|
|
|
|
if !required {
|
|
|
|
jsonTag += ",omitempty"
|
|
|
|
}
|
|
|
|
tags := map[string]string{"json": jsonTag}
|
2022-10-06 15:13:46 +01:00
|
|
|
fields = append(fields, jen.Id(id).Add(t).Tag(tags))
|
|
|
|
}
|
|
|
|
return jen.Struct(fields...)
|
|
|
|
}
|
|
|
|
|
2022-10-06 18:05:51 +01:00
|
|
|
func singlePatternProp(schema *jsonschema.Schema) *jsonschema.Schema {
|
|
|
|
if len(schema.PatternProperties) != 1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
for _, prop := range schema.PatternProperties {
|
|
|
|
return &prop
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-10-06 18:39:34 +01:00
|
|
|
func noAdditionalProps(schema *jsonschema.Schema) bool {
|
|
|
|
return schema.AdditionalProperties != nil && schema.AdditionalProperties.IsFalse()
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateSchemaType(schema *jsonschema.Schema, root *jsonschema.Schema, required bool) jen.Code {
|
2022-10-06 08:23:55 +01:00
|
|
|
if schema == nil {
|
|
|
|
return jen.Interface()
|
|
|
|
}
|
|
|
|
|
2022-10-06 18:39:34 +01:00
|
|
|
refName := refName(schema.Ref)
|
|
|
|
if refName != "" {
|
|
|
|
schema = resolveRef(schema, root)
|
|
|
|
t := jen.Id(formatId(refName))
|
|
|
|
if !required && schemaType(schema) == jsonschema.TypeObject && noAdditionalProps(schema) && len(schema.PatternProperties) == 0 {
|
|
|
|
t = jen.Op("*").Add(t)
|
|
|
|
}
|
|
|
|
return t
|
2022-10-06 18:09:57 +01:00
|
|
|
}
|
|
|
|
|
2022-10-06 08:23:55 +01:00
|
|
|
switch schemaType(schema) {
|
|
|
|
case jsonschema.TypeNull:
|
|
|
|
return jen.Struct()
|
|
|
|
case jsonschema.TypeBoolean:
|
|
|
|
return jen.Bool()
|
|
|
|
case jsonschema.TypeArray:
|
2022-10-06 18:39:34 +01:00
|
|
|
return jen.Index().Add(generateSchemaType(schema.Items, root, required))
|
2022-10-06 08:23:55 +01:00
|
|
|
case jsonschema.TypeNumber:
|
|
|
|
return jen.Float64()
|
|
|
|
case jsonschema.TypeString:
|
|
|
|
return jen.String()
|
|
|
|
case jsonschema.TypeInteger:
|
|
|
|
return jen.Int64()
|
|
|
|
case jsonschema.TypeObject:
|
2022-10-06 18:39:34 +01:00
|
|
|
noAdditionalProps := noAdditionalProps(schema)
|
2022-10-06 18:05:51 +01:00
|
|
|
if noAdditionalProps && len(schema.PatternProperties) == 0 {
|
2022-10-06 18:39:34 +01:00
|
|
|
t := generateStruct(schema, root)
|
|
|
|
if !required {
|
|
|
|
t = jen.Op("*").Add(t)
|
|
|
|
}
|
|
|
|
return t
|
2022-10-06 18:05:51 +01:00
|
|
|
} else if patternProp := singlePatternProp(schema); noAdditionalProps && patternProp != nil {
|
2022-10-06 18:39:34 +01:00
|
|
|
return jen.Map(jen.String()).Add(generateSchemaType(patternProp, root, true))
|
2022-10-06 15:13:46 +01:00
|
|
|
} else {
|
2022-10-06 18:39:34 +01:00
|
|
|
return jen.Map(jen.String()).Add(generateSchemaType(schema.AdditionalProperties, root, true))
|
2022-10-06 15:13:46 +01:00
|
|
|
}
|
2022-10-06 08:23:55 +01:00
|
|
|
default:
|
2022-10-07 10:28:43 +01:00
|
|
|
return jen.Qual("encoding/json", "RawMessage")
|
2022-10-06 08:23:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-07 10:28:43 +01:00
|
|
|
func generateDef(schema *jsonschema.Schema, root *jsonschema.Schema, f *jen.File, name string) {
|
|
|
|
id := formatId(name)
|
|
|
|
|
|
|
|
if schema.Ref == "" && schemaType(schema) == "" {
|
|
|
|
f.Type().Id(id).Struct(
|
|
|
|
jen.Qual("encoding/json", "RawMessage"),
|
|
|
|
).Line()
|
2022-10-07 10:32:51 +01:00
|
|
|
|
|
|
|
var children []jsonschema.Schema
|
|
|
|
for _, child := range schema.AllOf {
|
|
|
|
children = append(children, child)
|
|
|
|
}
|
|
|
|
for _, child := range schema.AnyOf {
|
|
|
|
children = append(children, child)
|
|
|
|
}
|
|
|
|
for _, child := range schema.OneOf {
|
|
|
|
children = append(children, child)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, child := range children {
|
|
|
|
refName := refName(child.Ref)
|
|
|
|
if refName == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
t := generateSchemaType(&child, root, false)
|
|
|
|
|
|
|
|
f.Func().Params(
|
|
|
|
jen.Id("v").Id(id),
|
|
|
|
).Id(formatId(refName)).Params().Params(
|
|
|
|
t,
|
|
|
|
jen.Id("error"),
|
|
|
|
).Block(
|
|
|
|
jen.Var().Id("out").Add(t),
|
|
|
|
jen.Id("err").Op(":=").Qual("encoding/json", "Unmarshal").Params(
|
|
|
|
jen.Id("v").Op(".").Id("RawMessage"),
|
|
|
|
jen.Op("&").Id("out"),
|
|
|
|
),
|
|
|
|
jen.Return(
|
|
|
|
jen.Id("out"),
|
|
|
|
jen.Id("err"),
|
|
|
|
),
|
|
|
|
).Line()
|
|
|
|
}
|
2022-10-07 10:28:43 +01:00
|
|
|
} else {
|
|
|
|
f.Type().Id(id).Add(generateSchemaType(schema, root, true)).Line()
|
|
|
|
}
|
2022-10-06 08:23:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func loadSchema(filename string) *jsonschema.Schema {
|
|
|
|
f, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("failed to open schema file: %v", err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
var schema jsonschema.Schema
|
|
|
|
if err := json.NewDecoder(f).Decode(&schema); err != nil {
|
|
|
|
log.Fatalf("failed to load schema JSON: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &schema
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
if len(os.Args) != 4 {
|
|
|
|
log.Fatalf("usage: jsonschemagen <schema> <output> <package>")
|
|
|
|
}
|
|
|
|
|
|
|
|
inputFilename := os.Args[1]
|
|
|
|
outputFilename := os.Args[2]
|
|
|
|
pkgName := os.Args[3]
|
|
|
|
|
|
|
|
schema := loadSchema(inputFilename)
|
|
|
|
f := jen.NewFile(pkgName)
|
|
|
|
|
2022-10-07 08:16:38 +01:00
|
|
|
if schema.Ref == "" {
|
|
|
|
generateDef(schema, schema, f, "root")
|
|
|
|
}
|
2022-10-07 08:19:34 +01:00
|
|
|
|
|
|
|
var names []string
|
|
|
|
for name := range schema.Defs {
|
|
|
|
names = append(names, name)
|
|
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
|
|
def := schema.Defs[name]
|
|
|
|
generateDef(&def, schema, f, name)
|
2022-10-06 08:23:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.Save(outputFilename); err != nil {
|
|
|
|
log.Fatalf("failed to save output file: %v", err)
|
|
|
|
}
|
|
|
|
}
|