254 lines
6.0 KiB
Go
254 lines
6.0 KiB
Go
/*
|
|
Copyright © 2022 The CDI Authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package schema
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/json"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/pkg/errors"
|
|
schema "github.com/xeipuuv/gojsonschema"
|
|
)
|
|
|
|
const (
|
|
// BuiltinSchemaName references the builtin schema for Load()/Set().
|
|
BuiltinSchemaName = "builtin"
|
|
// NoneSchemaName references a disabled/NOP schema for Load()/Set().
|
|
NoneSchemaName = "none"
|
|
// DefaultSchemaName is the none schema.
|
|
DefaultSchemaName = NoneSchemaName
|
|
|
|
// builtinSchemaFile is the builtin schema URI in our embedded FS.
|
|
builtinSchemaFile = "file:///schema.json"
|
|
)
|
|
|
|
// Schema is a JSON validation schema.
|
|
type Schema struct {
|
|
schema *schema.Schema
|
|
}
|
|
|
|
// Error wraps a JSON validation result.
|
|
type Error struct {
|
|
Result *schema.Result
|
|
}
|
|
|
|
// Set sets the default validating JSON schema.
|
|
func Set(s *Schema) {
|
|
current = s
|
|
}
|
|
|
|
// Get returns the active validating JSON schema.
|
|
func Get() *Schema {
|
|
return current
|
|
}
|
|
|
|
// BuiltinSchema returns the builtin validating JSON Schema.
|
|
func BuiltinSchema() *Schema {
|
|
return builtin
|
|
}
|
|
|
|
// NopSchema returns an validating JSON Schema that does no real validation.
|
|
func NopSchema() *Schema {
|
|
return &Schema{}
|
|
}
|
|
|
|
// ReadAndValidate all data from the given reader, using the active schema for validation.
|
|
func ReadAndValidate(r io.Reader) ([]byte, error) {
|
|
return current.ReadAndValidate(r)
|
|
}
|
|
|
|
// Validate validates the data read from an io.Reader against the active schema.
|
|
func Validate(r io.Reader) error {
|
|
return current.Validate(r)
|
|
}
|
|
|
|
// ValidateData validates the given JSON document against the active schema.
|
|
func ValidateData(data []byte) error {
|
|
return current.ValidateData(data)
|
|
}
|
|
|
|
// ValidateFile validates the given JSON file against the active schema.
|
|
func ValidateFile(path string) error {
|
|
return current.ValidateFile(path)
|
|
}
|
|
|
|
// ValidateType validates a go object against the schema.
|
|
func ValidateType(obj interface{}) error {
|
|
return current.ValidateType(obj)
|
|
}
|
|
|
|
// Load the given JSON Schema.
|
|
func Load(source string) (*Schema, error) {
|
|
var (
|
|
loader schema.JSONLoader
|
|
err error
|
|
s *schema.Schema
|
|
)
|
|
|
|
source = strings.TrimSpace(source)
|
|
|
|
switch {
|
|
case source == BuiltinSchemaName:
|
|
return BuiltinSchema(), nil
|
|
case source == NoneSchemaName, source == "":
|
|
return NopSchema(), nil
|
|
case strings.HasPrefix(source, "file://"):
|
|
case strings.HasPrefix(source, "http://"):
|
|
case strings.HasPrefix(source, "https://"):
|
|
default:
|
|
if strings.Index(source, "://") < 0 {
|
|
source, err = filepath.Abs(source)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err,
|
|
"failed to get JSON schema absolute path for %s", source)
|
|
}
|
|
source = "file://" + source
|
|
}
|
|
}
|
|
|
|
loader = schema.NewReferenceLoader(source)
|
|
|
|
s, err = schema.NewSchema(loader)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to load JSON schema")
|
|
}
|
|
|
|
return &Schema{schema: s}, nil
|
|
}
|
|
|
|
// ReadAndValidate all data from the given reader, using the schema for validation.
|
|
func (s *Schema) ReadAndValidate(r io.Reader) ([]byte, error) {
|
|
loader, reader := schema.NewReaderLoader(r)
|
|
data, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read data for validation")
|
|
}
|
|
return data, s.validate(loader)
|
|
}
|
|
|
|
// Validate validates the data read from an io.Reader against the schema.
|
|
func (s *Schema) Validate(r io.Reader) error {
|
|
_, err := s.ReadAndValidate(r)
|
|
return err
|
|
}
|
|
|
|
// ValidateData validates the given JSON data against the schema.
|
|
func (s *Schema) ValidateData(data []byte) error {
|
|
var (
|
|
any interface{}
|
|
err error
|
|
)
|
|
|
|
if !bytes.HasPrefix(bytes.TrimSpace(data), []byte{'{'}) {
|
|
err = yaml.Unmarshal(data, &any)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to YAML unmarshal data for validation")
|
|
}
|
|
data, err = json.Marshal(any)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to JSON remarshal data for validation")
|
|
}
|
|
}
|
|
|
|
return s.validate(schema.NewBytesLoader(data))
|
|
}
|
|
|
|
// ValidateFile validates the given JSON file against the schema.
|
|
func (s *Schema) ValidateFile(path string) error {
|
|
if filepath.Ext(path) == ".json" {
|
|
return s.validate(schema.NewReferenceLoader("file://" + path))
|
|
}
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.ValidateData(data)
|
|
}
|
|
|
|
// ValidateType validates a go object against the schema.
|
|
func (s *Schema) ValidateType(obj interface{}) error {
|
|
l := schema.NewGoLoader(obj)
|
|
return s.validate(l)
|
|
}
|
|
|
|
// Validate the (to be) loaded doc against the schema.
|
|
func (s *Schema) validate(doc schema.JSONLoader) error {
|
|
if s == nil || s.schema == nil {
|
|
return nil
|
|
}
|
|
|
|
docErr, jsonErr := s.schema.Validate(doc)
|
|
if jsonErr != nil {
|
|
return errors.Wrap(jsonErr, "failed to load JSON data for validation")
|
|
}
|
|
if docErr.Valid() {
|
|
return nil
|
|
}
|
|
|
|
return &Error{Result: docErr}
|
|
}
|
|
|
|
// Error returns the given Result's error as a multierror(.Error()).
|
|
func (e *Error) Error() string {
|
|
if e == nil || e.Result == nil || e.Result.Valid() {
|
|
return ""
|
|
}
|
|
|
|
var multi error
|
|
for _, err := range e.Result.Errors() {
|
|
multi = multierror.Append(multi, errors.Errorf("%v", err))
|
|
}
|
|
return strings.TrimRight(multi.Error(), "\n")
|
|
}
|
|
|
|
var (
|
|
// our builtin schema
|
|
builtin *Schema
|
|
// currently loaded schema, builtin by default
|
|
current *Schema
|
|
)
|
|
|
|
//go:embed *.json
|
|
var builtinFS embed.FS
|
|
|
|
func init() {
|
|
s, err := schema.NewSchema(
|
|
schema.NewReferenceLoaderFileSystem(
|
|
builtinSchemaFile,
|
|
http.FS(builtinFS),
|
|
),
|
|
)
|
|
|
|
if err != nil {
|
|
builtin = NopSchema()
|
|
} else {
|
|
builtin = &Schema{schema: s}
|
|
}
|
|
|
|
current = builtin
|
|
}
|