code/go/internal/yamlschema/schema_loader.go (90 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package yamlschema
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/url"
"path"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/elastic/gojsonschema"
"github.com/xeipuuv/gojsonreference"
"gopkg.in/yaml.v3"
)
type yamlReferenceLoader struct {
fs fs.FS
source string
version semver.Version
}
var _ gojsonschema.JSONLoader = new(yamlReferenceLoader)
// NewReferenceLoaderFileSystem method creates new instance of `yamlReferenceLoader`.
func NewReferenceLoaderFileSystem(source string, fs fs.FS, version semver.Version) gojsonschema.JSONLoader {
return &yamlReferenceLoader{
fs: fs,
source: source,
version: version,
}
}
func (l *yamlReferenceLoader) JsonSource() any { // golint:ignore
return l.source
}
func (l *yamlReferenceLoader) LoadJSON() (any, error) {
parsed, err := url.Parse(l.source)
if err != nil {
return nil, fmt.Errorf("parsing source failed (source: %s): %w", l.source, err)
}
resourcePath := strings.TrimPrefix(parsed.Path, "/")
itemSchemaData, err := fs.ReadFile(l.fs, resourcePath)
if err != nil {
return nil, fmt.Errorf("reading schema file failed: %w", err)
}
if len(itemSchemaData) == 0 {
return nil, errors.New("schema file is empty")
}
var schema itemSchemaSpec
err = yaml.Unmarshal(itemSchemaData, &schema)
if err != nil {
return nil, fmt.Errorf("schema unmarshalling failed (path: %s): %w", l.source, err)
}
// fixJSONNumbers ensures that the numbers in the resulting spec are of type `json.Number`, that is
// what the gojsonschema library expects. Without this, gojsonschema complains about some integer types
// not being integers.
// TODO: This shouldn't probably be needed, we could try to fix gojsonschema to accept other integers, or
// look for a YAML parser that can be customized to use `json.Number`.
schema.Spec, err = fixJSONNumbers(schema.Spec)
if err != nil {
return nil, fmt.Errorf("fixing numbers in parsed schema failed (path %s): %w", l.source, err)
}
return schema.resolve(l.version)
}
// fixJSONNumbers converts number types to `json.Number` by converting the struct to JSON and decoding it again.
func fixJSONNumbers[T any](v T) (result T, err error) {
d, err := json.Marshal(v)
if err != nil {
return result, err
}
dec := json.NewDecoder(bytes.NewReader(d))
dec.UseNumber()
return result, dec.Decode(&result)
}
func (l *yamlReferenceLoader) JsonReference() (gojsonreference.JsonReference, error) {
r, err := gojsonreference.NewJsonReference(l.JsonSource().(string))
if err != nil {
return r, err
}
// gojsonreference uses filepath to decide if the reference has a full file path,
// and in Windows it has additional special handling.
// Here we are operating on a fs.FS, where '/' is always used as separator, also on
// Windows. Override the value with the result of `path.IsAbs`.
r.HasFullFilePath = path.IsAbs(r.GetUrl().Path)
return r, nil
}
func (l *yamlReferenceLoader) LoaderFactory() gojsonschema.JSONLoaderFactory {
return &fileSystemYAMLLoaderFactory{
fs: l.fs,
version: l.version,
}
}
type fileSystemYAMLLoaderFactory struct {
fs fs.FS
version semver.Version
}
var _ gojsonschema.JSONLoaderFactory = new(fileSystemYAMLLoaderFactory)
func (f *fileSystemYAMLLoaderFactory) New(source string) gojsonschema.JSONLoader {
return &yamlReferenceLoader{
fs: f.fs,
source: source,
version: f.version,
}
}