Initial commit
This commit is contained in:
commit
dde5b75d7a
6 changed files with 342 additions and 0 deletions
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2022 Simon Ser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# go-jsonschema
|
||||
|
||||
A JSON schema code generator for Go.
|
||||
|
||||
## Usage
|
||||
|
||||
jsonschemagen <schema> <output> <package>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
151
cmd/jsonschemagen/main.go
Normal file
151
cmd/jsonschemagen/main.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dave/jennifer/jen"
|
||||
|
||||
"git.sr.ht/~emersion/go-jsonschema"
|
||||
)
|
||||
|
||||
func formatId(s string) string {
|
||||
s = strings.Title(s)
|
||||
// TODO: improve robustness
|
||||
s = strings.ReplaceAll(s, "-", "")
|
||||
s = strings.ReplaceAll(s, "_", "")
|
||||
return s
|
||||
}
|
||||
|
||||
func resolveRef(def *jsonschema.Schema, root *jsonschema.Schema) *jsonschema.Schema {
|
||||
if def.Ref == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
prefix := "#/$defs/"
|
||||
if !strings.HasPrefix(def.Ref, prefix) {
|
||||
log.Fatalf("unsupported $ref %q", def.Ref)
|
||||
}
|
||||
name := strings.TrimPrefix(def.Ref, prefix)
|
||||
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
func generateSchemaType(schema *jsonschema.Schema, root *jsonschema.Schema) jen.Code {
|
||||
if schema == nil {
|
||||
return jen.Interface()
|
||||
}
|
||||
|
||||
schema = resolveRef(schema, root)
|
||||
switch schemaType(schema) {
|
||||
case jsonschema.TypeNull:
|
||||
return jen.Struct()
|
||||
case jsonschema.TypeBoolean:
|
||||
return jen.Bool()
|
||||
case jsonschema.TypeArray:
|
||||
return jen.Index().Add(generateSchemaType(schema.Items, root))
|
||||
case jsonschema.TypeNumber:
|
||||
return jen.Float64()
|
||||
case jsonschema.TypeString:
|
||||
return jen.String()
|
||||
case jsonschema.TypeInteger:
|
||||
return jen.Int64()
|
||||
case jsonschema.TypeObject:
|
||||
return jen.Map(jen.String()).Add(generateSchemaType(schema.AdditionalProperties, root))
|
||||
default:
|
||||
return jen.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func generateDef(def *jsonschema.Schema, root *jsonschema.Schema, f *jen.File, name string) {
|
||||
if schemaType(def) != jsonschema.TypeObject {
|
||||
return
|
||||
}
|
||||
if def.AdditionalProperties == nil || !def.AdditionalProperties.IsFalse() {
|
||||
return
|
||||
}
|
||||
if len(def.PatternProperties) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var fields []jen.Code
|
||||
for name, prop := range def.Properties {
|
||||
id := formatId(name)
|
||||
t := generateSchemaType(&prop, root)
|
||||
tags := map[string]string{"json": name}
|
||||
fields = append(fields, jen.Id(id).Add(t).Tag(tags))
|
||||
}
|
||||
|
||||
f.Type().Id(formatId(name)).Struct(fields...).Line()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
generateDef(schema, schema, f, "root")
|
||||
for k, def := range schema.Defs {
|
||||
generateDef(&def, schema, f, k)
|
||||
}
|
||||
|
||||
if err := f.Save(outputFilename); err != nil {
|
||||
log.Fatalf("failed to save output file: %v", err)
|
||||
}
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module git.sr.ht/~emersion/go-jsonschema
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/dave/jennifer v1.5.1
|
34
go.sum
Normal file
34
go.sum
Normal file
|
@ -0,0 +1,34 @@
|
|||
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms=
|
||||
github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM=
|
||||
github.com/dave/courtney v0.3.0/go.mod h1:BAv3hA06AYfNUjfjQr+5gc6vxeBVOupLqrColj+QSD8=
|
||||
github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ=
|
||||
github.com/dave/jennifer v1.5.1 h1:AI8gaM02nCYRw6/WTH0W+S6UNck9YqPZ05xoIxQtuoE=
|
||||
github.com/dave/jennifer v1.5.1/go.mod h1:AxTG893FiZKqxy3FP1kL80VMshSMuz2G+EgvszgGRnk=
|
||||
github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8=
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc=
|
||||
github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
122
schema.go
Normal file
122
schema.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package jsonschema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypeNull Type = "null"
|
||||
TypeBoolean Type = "boolean"
|
||||
TypeObject Type = "object"
|
||||
TypeArray Type = "array"
|
||||
TypeNumber Type = "number"
|
||||
TypeString Type = "string"
|
||||
TypeInteger Type = "integer"
|
||||
)
|
||||
|
||||
type Schema struct {
|
||||
// Core
|
||||
Schema string `json:"$schema"`
|
||||
Vocabulary map[string]bool `json:"$vocabulary"`
|
||||
ID string `json:"$id"`
|
||||
Ref string `json:"$ref"`
|
||||
DynamicRef string `json:"$dynamicRef"`
|
||||
Defs map[string]Schema `json:"$defs"`
|
||||
Comment string `json:"$comment"`
|
||||
|
||||
// Applying subschemas with logic
|
||||
AllOf []Schema `json:"allOf"`
|
||||
AnyOf []Schema `json:"anyOf"`
|
||||
OneOf []Schema `json:"oneOf"`
|
||||
Not []Schema `json:"not"`
|
||||
|
||||
// Applying subschemas conditionally
|
||||
If *Schema `json:"if"`
|
||||
Then *Schema `json:"then"`
|
||||
Else *Schema `json:"else"`
|
||||
DependentSchemas map[string]Schema `json:"dependentSchemas"`
|
||||
|
||||
// Applying subschemas to arrays
|
||||
PrefixItems []Schema `json:"prefixItems"`
|
||||
Items *Schema `json:"items"`
|
||||
Contains *Schema `json:"contains"`
|
||||
|
||||
// Applying subschemas to objects
|
||||
Properties map[string]Schema `json:"properties"`
|
||||
PatternProperties map[string]Schema `json:"patternProperties"`
|
||||
AdditionalProperties *Schema `json:"additionalProperties"`
|
||||
PropertyNames *Schema `json:"propertyNames"`
|
||||
|
||||
// Validation
|
||||
Type Type `json:"type"`
|
||||
Enum []interface{} `json:"enum"`
|
||||
Const interface{} `json:"const"`
|
||||
|
||||
// Validation for numbers
|
||||
MultipleOf json.Number `json:"multipleOf"`
|
||||
Maximum json.Number `json:"maximum"`
|
||||
ExclusiveMaximum json.Number `json:"exclusiveMaximum"`
|
||||
Minimum json.Number `json:"minimum"`
|
||||
ExclusiveMinimum json.Number `json:"exclusiveMinimum"`
|
||||
|
||||
// Validation for strings
|
||||
MaxLength int `json:"maxLength"`
|
||||
MinLength int `json:"minLength"`
|
||||
Pattern string `json:"pattern"`
|
||||
|
||||
// Validation for arrays
|
||||
MaxItems int `json:"maxItems"`
|
||||
MinItems int `json:"minItems"`
|
||||
UniqueItems bool `json:"uniqueItems"`
|
||||
MaxContains int `json:"maxContains"`
|
||||
MinContains int `json:"minContains"`
|
||||
|
||||
// Validation for objects
|
||||
MaxProperties int `json:"maxProperties"`
|
||||
MinProperties int `json:"minProperties"`
|
||||
Required []string `json:"required"`
|
||||
DependentRequired map[string][]string `json:"dependentRequired"`
|
||||
|
||||
// Basic metadata annotations
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Default interface{} `json:"default"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
ReadOnly bool `json:"readOnly"`
|
||||
WriteOnly bool `json:"writeOnly"`
|
||||
Examples []interface{} `json:"examples"`
|
||||
}
|
||||
|
||||
func (schema *Schema) UnmarshalJSON(b []byte) error {
|
||||
if bytes.Equal(b, []byte("true")) {
|
||||
// Nothing to do
|
||||
} else if bytes.Equal(b, []byte("false")) {
|
||||
*schema = Schema{Not: []Schema{
|
||||
Schema{},
|
||||
}}
|
||||
} else {
|
||||
type rawSchema Schema
|
||||
var out rawSchema
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
return err
|
||||
}
|
||||
*schema = Schema(out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (schema *Schema) IsTrue() bool {
|
||||
return len(schema.AllOf) == 0 && len(schema.AnyOf) == 0 && len(schema.OneOf) == 0 && len(schema.Not) == 0 && schema.If == nil && schema.Then == nil && schema.Else == nil && len(schema.DependentSchemas) == 0 && len(schema.PrefixItems) == 0 && schema.Items == nil && schema.Contains == nil && len(schema.Properties) == 0 && len(schema.PatternProperties) == 0 && schema.AdditionalProperties == nil && schema.PropertyNames == nil
|
||||
}
|
||||
|
||||
func (schema *Schema) IsFalse() bool {
|
||||
for _, not := range schema.Not {
|
||||
if not.IsTrue() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Loading…
Reference in a new issue