Skip to content

Commit

Permalink
feat: generate kcl schema from json schema (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakezhu9 authored Aug 2, 2023
1 parent 96e2e7c commit 88ec03a
Show file tree
Hide file tree
Showing 30 changed files with 4,882 additions and 1 deletion.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@ require (
github.com/gofrs/flock v0.8.1
github.com/golang/protobuf v1.5.3
github.com/google/go-cmp v0.5.9
github.com/iancoleman/strcase v0.3.0
github.com/julienschmidt/httprouter v1.3.0
github.com/mitchellh/mapstructure v1.5.0
github.com/powerman/rpc-codec v1.2.2
github.com/qri-io/jsonpointer v0.1.1
github.com/stretchr/testify v1.8.2
github.com/urfave/cli/v2 v2.6.0
github.com/wk8/go-ordered-map/v2 v2.1.8
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v3 v3.0.1
kcl-lang.io/kcl-artifact-go v0.5.1
)

require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/net v0.9.0 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/chai2010/jsonv v1.1.3 h1:gBIHXn/5mdEPTuWZfjC54fn/yUSRR8OGobXobcc6now=
github.com/chai2010/jsonv v1.1.3/go.mod h1:mEoT1dQ9qVF4oP9peVTl0UymTmJwXoTDOh+sNA6+XII=
github.com/chai2010/protorpc v1.1.4 h1:CTtFUhzXRoeuR7FtgQ2b2vdT/KgWVpCM+sIus8zJjHs=
Expand All @@ -18,14 +22,21 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/powerman/rpc-codec v1.2.2 h1:BK0JScZivljhwW/vLLhZLtUgqSxc/CD3sHEs8LiwwKw=
github.com/powerman/rpc-codec v1.2.2/go.mod h1:3Qr/y/+u3CwcSww9tfJMRn/95lB2qUdUeIQe7BYlLDo=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand All @@ -37,6 +48,8 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli/v2 v2.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8=
github.com/urfave/cli/v2 v2.6.0/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
Expand Down
35 changes: 35 additions & 0 deletions pkg/3rdparty/jsonschema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# jsonschema

This package is a fork of the [jsonschema](https://github.com/qri-io/jsonschema)
package, which is a Go implementation of the JSON Schema specification.

Thanks to the original authors for their great work, we are able to use this package to
parse JSON Schema and support keywords in different versions of the specification easily.
We also make some modifications to support our needs, such as make some field public,
use orderedmap in properties keyword to keep the order, etc.

## License

```
The MIT License (MIT)
Copyright (c) 2017 Brendan O'Brien
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.
```
93 changes: 93 additions & 0 deletions pkg/3rdparty/jsonschema/draft2019_09_keywords.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package jsonschema

// LoadDraft2019_09 loads the Keywords for schema validation
// based on draft2019_09
// this is also the default keyword set loaded automatically
// if no other is loaded
func LoadDraft2019_09() {
// core Keywords
RegisterKeyword("$schema", NewSchemaURI)
RegisterKeyword("$id", NewID)
RegisterKeyword("description", NewDescription)
RegisterKeyword("title", NewTitle)
RegisterKeyword("$comment", NewComment)
RegisterKeyword("examples", NewExamples)
RegisterKeyword("readOnly", NewReadOnly)
RegisterKeyword("writeOnly", NewWriteOnly)
RegisterKeyword("$ref", NewRef)
RegisterKeyword("$recursiveRef", NewRecursiveRef)
RegisterKeyword("$anchor", NewAnchor)
RegisterKeyword("$recursiveAnchor", NewRecursiveAnchor)
RegisterKeyword("$defs", NewDefs)
RegisterKeyword("definitions", NewDefs)
RegisterKeyword("default", NewDefault)

SetKeywordOrder("$ref", 0)
SetKeywordOrder("$recursiveRef", 0)

// standard Keywords
RegisterKeyword("type", NewType)
RegisterKeyword("enum", NewEnum)
RegisterKeyword("const", NewConst)

// numeric Keywords
RegisterKeyword("multipleOf", NewMultipleOf)
RegisterKeyword("maximum", NewMaximum)
RegisterKeyword("exclusiveMaximum", NewExclusiveMaximum)
RegisterKeyword("minimum", NewMinimum)
RegisterKeyword("exclusiveMinimum", NewExclusiveMinimum)

// string Keywords
RegisterKeyword("maxLength", NewMaxLength)
RegisterKeyword("minLength", NewMinLength)
RegisterKeyword("pattern", NewPattern)

// boolean Keywords
RegisterKeyword("allOf", NewAllOf)
RegisterKeyword("anyOf", NewAnyOf)
RegisterKeyword("oneOf", NewOneOf)
RegisterKeyword("not", NewNot)

// object Keywords
RegisterKeyword("properties", NewProperties)
RegisterKeyword("patternProperties", NewPatternProperties)
RegisterKeyword("additionalProperties", NewAdditionalProperties)
RegisterKeyword("required", NewRequired)
RegisterKeyword("propertyNames", NewPropertyNames)
RegisterKeyword("maxProperties", NewMaxProperties)
RegisterKeyword("minProperties", NewMinProperties)
RegisterKeyword("dependentSchemas", NewDependentSchemas)
RegisterKeyword("dependentRequired", NewDependentRequired)
RegisterKeyword("unevaluatedProperties", NewUnevaluatedProperties)

SetKeywordOrder("properties", 2)
SetKeywordOrder("additionalProperties", 3)
SetKeywordOrder("unevaluatedProperties", 4)

// array Keywords
RegisterKeyword("items", NewItems)
RegisterKeyword("additionalItems", NewAdditionalItems)
RegisterKeyword("maxItems", NewMaxItems)
RegisterKeyword("minItems", NewMinItems)
RegisterKeyword("uniqueItems", NewUniqueItems)
RegisterKeyword("contains", NewContains)
RegisterKeyword("maxContains", NewMaxContains)
RegisterKeyword("minContains", NewMinContains)
RegisterKeyword("unevaluatedItems", NewUnevaluatedItems)

SetKeywordOrder("maxContains", 2)
SetKeywordOrder("minContains", 2)
SetKeywordOrder("additionalItems", 3)
SetKeywordOrder("unevaluatedItems", 4)

// conditional Keywords
RegisterKeyword("if", NewIf)
RegisterKeyword("then", NewThen)
RegisterKeyword("else", NewElse)

SetKeywordOrder("then", 2)
SetKeywordOrder("else", 2)

//optional formats
RegisterKeyword("format", NewFormat)
}
157 changes: 157 additions & 0 deletions pkg/3rdparty/jsonschema/keyword.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package jsonschema

import (
"bytes"
"context"
"encoding/json"
"fmt"

jptr "github.com/qri-io/jsonpointer"
)

var notSupported = map[string]bool{
// core
"$vocabulary": true,

// other
"contentEncoding": true,
"contentMediaType": true,
"contentSchema": true,
"deprecated": true,

// backward compatibility with draft7
"definitions": true,
"dependencies": true,
}

var (
keywordRegistry = map[string]KeyMaker{}
keywordOrder = map[string]int{}
keywordInsertOrder = map[string]int{}
)

// IsRegisteredKeyword validates if a given prop string is a registered keyword
func IsRegisteredKeyword(prop string) bool {
_, ok := keywordRegistry[prop]
return ok
}

// GetKeyword returns a new instance of the keyword
func GetKeyword(prop string) Keyword {
if !IsRegisteredKeyword(prop) {
return NewVoid()
}
return keywordRegistry[prop]()
}

// GetKeywordOrder returns the order index of
// the given keyword or defaults to 1
func GetKeywordOrder(prop string) int {
if order, ok := keywordOrder[prop]; ok {
return order
}
return 1
}

// GetKeywordInsertOrder returns the insert index of
// the given keyword
func GetKeywordInsertOrder(prop string) int {
if order, ok := keywordInsertOrder[prop]; ok {
return order
}
// TODO(arqu): this is an arbitrary max
return 1000
}

// SetKeywordOrder assignes a given order to a keyword
func SetKeywordOrder(prop string, order int) {
keywordOrder[prop] = order
}

// IsNotSupportedKeyword is a utility function to clarify when
// a given keyword, while expected is not supported
func IsNotSupportedKeyword(prop string) bool {
_, ok := notSupported[prop]
return ok
}

// IsRegistryLoaded checks if any Keywords are present
func IsRegistryLoaded() bool {
return keywordRegistry != nil && len(keywordRegistry) > 0
}

// RegisterKeyword registers a keyword with the registry
func RegisterKeyword(prop string, maker KeyMaker) {
keywordRegistry[prop] = maker
keywordInsertOrder[prop] = len(keywordInsertOrder)
}

// MaxKeywordErrStringLen sets how long a value can be before it's length is truncated
// when printing error strings
// a special value of -1 disables output trimming
var MaxKeywordErrStringLen = 20

// Keyword is an interface for anything that can validate.
// JSON-Schema Keywords are all examples of Keyword
type Keyword interface {
// ValidateKeyword checks decoded JSON data and writes
// validation errors (if any) to an outparam slice of KeyErrors
ValidateKeyword(ctx context.Context, currentState *ValidationState, data interface{})

// Register builds up the schema tree by evaluating the current key
// and the current location pointer which is later used with resolve to
// navigate the schema tree and substitute the propper schema for a given
// reference.
Register(uri string, registry *SchemaRegistry)
// Resolve unraps a pointer to the destination schema
// It usually starts with a $ref validation call which
// uses the pointer token by token to navigate the
// schema tree to get to the last schema in the chain.
// Since every keyword can have it's specifics around resolving
// each keyword need to implement it's own version of Resolve.
// Terminal Keywords should respond with nil as it's not a schema
// Keywords that wrap a schema should return the appropriate schema.
// In case of a non-existing location it will fail to resolve, return nil
// on ref resolution and error out.
Resolve(pointer jptr.Pointer, uri string) *Schema
}

// KeyMaker is a function that generates instances of a Keyword.
// Calls to KeyMaker will be passed directly to json.Marshal,
// so the returned value should be a pointer
type KeyMaker func() Keyword

// KeyError represents a Single error in an instance of a schema
// The only absolutely-required property is Message.
type KeyError struct {
// PropertyPath is a string path that leads to the
// property that produced the error
PropertyPath string `json:"propertyPath,omitempty"`
// InvalidValue is the value that returned the error
InvalidValue interface{} `json:"invalidValue,omitempty"`
// Message is a human-readable description of the error
Message string `json:"message"`
}

// Error implements the error interface for KeyError
func (v KeyError) Error() string {
if v.PropertyPath != "" && v.InvalidValue != nil {
return fmt.Sprintf("%s: %s %s", v.PropertyPath, InvalidValueString(v.InvalidValue), v.Message)
} else if v.PropertyPath != "" {
return fmt.Sprintf("%s: %s", v.PropertyPath, v.Message)
}
return v.Message
}

// InvalidValueString returns the errored value as a string
func InvalidValueString(data interface{}) string {
bt, err := json.Marshal(data)
if err != nil {
return ""
}
bt = bytes.Replace(bt, []byte{'\n', '\r'}, []byte{' '}, -1)
if MaxKeywordErrStringLen != -1 && len(bt) > MaxKeywordErrStringLen {
bt = append(bt[:MaxKeywordErrStringLen], []byte("...")...)
}
return string(bt)
}
Loading

0 comments on commit 88ec03a

Please sign in to comment.