code/go/internal/yamlschema/loader.go (116 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 (
"encoding/json"
"fmt"
"io/fs"
"path"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/elastic/gojsonschema"
"gopkg.in/yaml.v3"
"github.com/elastic/package-spec/v3/code/go/internal/spectypes"
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
)
var semver3_0_0 = semver.MustParse("3.0.0")
type FileSchemaLoader struct{}
func NewFileSchemaLoader() *FileSchemaLoader {
return &FileSchemaLoader{}
}
func (*FileSchemaLoader) Load(fs fs.FS, schemaPath string, options spectypes.FileSchemaLoadOptions) (spectypes.FileSchema, error) {
schemaLoader := NewReferenceLoaderFileSystem("file:///"+schemaPath, fs, options.SpecVersion)
schema, err := gojsonschema.NewSchema(schemaLoader)
if err != nil {
return nil, fmt.Errorf("failed to load schema for %q: %v", schemaPath, err)
}
return &FileSchema{schema, options}, nil
}
type FileSchema struct {
schema *gojsonschema.Schema
options spectypes.FileSchemaLoadOptions
}
var formatCheckersMutex sync.Mutex
func (s *FileSchema) Validate(fsys fs.FS, filePath string) specerrors.ValidationErrors {
data, err := loadItemSchema(fsys, filePath, s.options.ContentType, s.options.SpecVersion)
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
formatCheckersMutex.Lock()
defer func() {
unloadRelativePathFormatChecker()
unloadDataStreamNameFormatChecker()
formatCheckersMutex.Unlock()
}()
loadRelativePathFormatChecker(fsys, path.Dir(filePath), s.options.Limits.MaxRelativePathSize())
loadDataStreamNameFormatChecker(fsys, path.Dir(filePath))
result, err := s.schema.Validate(gojsonschema.NewBytesLoader(data))
if err != nil {
return specerrors.ValidationErrors{specerrors.NewStructuredError(err, specerrors.UnassignedCode)}
}
if !result.Valid() {
var errs specerrors.ValidationErrors
for _, re := range result.Errors() {
errs = append(errs,
specerrors.NewStructuredErrorf("field %s: %s", re.Field(), adjustErrorDescription(re.Description())),
)
}
return errs
}
return nil // item content is valid according to the loaded schema
}
func loadItemSchema(fsys fs.FS, path string, contentType *spectypes.ContentType, specVersion semver.Version) ([]byte, error) {
data, err := fs.ReadFile(fsys, path)
if err != nil {
return nil, specerrors.ValidationErrors{specerrors.NewStructuredErrorf("reading item file failed: %w", err)}
}
if contentType != nil && contentType.MediaType == "application/x-yaml" {
return convertYAMLToJSON(data, specVersion.LessThan(semver3_0_0))
}
return data, nil
}
func convertYAMLToJSON(data []byte, expandKeys bool) ([]byte, error) {
var c interface{}
err := yaml.Unmarshal(data, &c)
if err != nil {
return nil, fmt.Errorf("unmarshalling YAML file failed: %w", err)
}
if expandKeys {
c = expandItemKey(c)
}
data, err = json.Marshal(&c)
if err != nil {
return nil, fmt.Errorf("converting YAML to JSON failed: %w", err)
}
return data, nil
}
func expandItemKey(c interface{}) interface{} {
if c == nil {
return c
}
// c is an array
if cArr, isArray := c.([]interface{}); isArray {
arr := []interface{}{}
for _, ca := range cArr {
arr = append(arr, expandItemKey(ca))
}
return arr
}
// c is map[string]interface{}
if cMap, isMapString := c.(map[string]interface{}); isMapString {
expanded := MapStr{}
for k, v := range cMap {
ex := expandItemKey(v)
_, err := expanded.Put(k, ex)
if err != nil {
panic(fmt.Errorf("unexpected error while setting key value (key: %s): %w", k, err))
}
}
return expanded
}
return c // c is something else, e.g. string, int, etc.
}
func adjustErrorDescription(description string) string {
if description == "Does not match format '"+relativePathFormat+"'" {
return fmt.Sprintf("relative path is invalid, target doesn't exist or it exceeds the file size limit")
} else if description == "Does not match format '"+dataStreamNameFormat+"'" {
return "data stream doesn't exist"
}
return description
}