omissis/go-jsonschema

View on GitHub
pkg/schemas/loaders.go

Summary

Maintainability
A
1 hr
Test Coverage
package schemas

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "path"
    "path/filepath"
    "strings"
)

var (
    ErrCannotResolveSchema      = errors.New("cannot resolve schema")
    ErrCannotLoadSchema         = errors.New("cannot load schema")
    ErrUnsupportedContentType   = errors.New("unsupported content type")
    ErrUnsupportedFileExtension = errors.New("unsupported file extension")
    ErrUnsupportedURL           = errors.New("unsupported URL")
)

type Loader interface {
    Load(uri, parentURI string) (*Schema, error)
}

func NewCachedLoader(loader Loader, cache map[string]*Schema) *CachedLoader {
    return &CachedLoader{
        loader: loader,
        cache:  cache,
    }
}

type CachedLoader struct {
    loader Loader
    cache  map[string]*Schema
}

func (l *CachedLoader) Load(uri, parentURI string) (*Schema, error) {
    if schema, ok := l.cache[uri]; ok {
        return schema, nil
    }

    schema, err := l.loader.Load(uri, parentURI)
    if err != nil {
        return nil, errors.Join(ErrCannotLoadSchema, err)
    }

    l.cache[uri] = schema

    return schema, nil
}

func NewFileLoader(resolveExtensions, yamlExtensions []string) *FileLoader {
    return &FileLoader{
        resolveExtensions: resolveExtensions,
        yamlExtensions:    toExtensionSet(yamlExtensions),
    }
}

type FileLoader struct {
    resolveExtensions []string
    yamlExtensions    map[string]bool
}

func (l *FileLoader) Load(fileName, parentFileName string) (*Schema, error) {
    qualified, err := QualifiedFileName(fileName, parentFileName, l.resolveExtensions)
    if err != nil {
        return nil, err
    }

    schema, err := l.parseFile(qualified)
    if err != nil {
        return nil, err
    }

    return schema, nil
}

func (l *FileLoader) parseFile(fileName string) (*Schema, error) {
    if l.yamlExtensions[path.Ext(fileName)] {
        sc, err := FromYAMLFile(fileName)
        if err != nil {
            return nil, fmt.Errorf("error parsing YAML file %s: %w", fileName, err)
        }

        return sc, nil
    }

    sc, err := FromJSONFile(fileName)
    if err != nil {
        return nil, fmt.Errorf("error parsing JSON file %s: %w", fileName, err)
    }

    return sc, nil
}

func NewDefaultCacheLoader(resolveExtensions, yamlExtensions []string) *CachedLoader {
    return NewCachedLoader(NewDefaultMultiLoader(resolveExtensions, yamlExtensions), map[string]*Schema{})
}

func NewDefaultMultiLoader(resolveExtensions, yamlExtensions []string) MultiLoader {
    httpLoader := NewHTTPLoader(yamlExtensions)

    return MultiLoader{
        RefTypeFile:  NewFileLoader(resolveExtensions, yamlExtensions),
        RefTypeHTTP:  httpLoader,
        RefTypeHTTPS: httpLoader,
    }
}

type MultiLoader map[RefType]Loader

func (l MultiLoader) Load(uri, parentURI string) (*Schema, error) {
    ref, err := GetRefType(uri)
    if err != nil {
        return nil, err
    }

    loader, ok := l[ref]
    if !ok {
        return nil, ErrUnsupportedRefFormat
    }

    schema, err := loader.Load(uri, parentURI)
    if err != nil {
        return nil, fmt.Errorf("failed to load schema %q: %w", uri, err)
    }

    return schema, nil
}

func NewHTTPLoader(yamlExtensions []string) *HTTPLoader {
    return &HTTPLoader{YAMLExtensions: toExtensionSet(yamlExtensions)}
}

type HTTPLoader struct {
    YAMLExtensions map[string]bool
}

func (l *HTTPLoader) Load(uri, parentURI string) (*Schema, error) {
    u, err := url.Parse(uri)
    if err != nil {
        return nil, fmt.Errorf("failed to parse url: %w", err)
    }

    if u.Scheme == "http" || u.Scheme == "https" {
        req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
        if err != nil {
            return nil, fmt.Errorf("failed to create request: %w", err)
        }

        resp, err := (&http.Client{}).Do(req)
        if err != nil {
            return nil, fmt.Errorf("failed to perform request: %w", err)
        }

        defer func() {
            if resp != nil && resp.Body != nil {
                _ = resp.Body.Close()
            }
        }()

        switch resp.Header.Get("Content-Type") {
        case "application/json":
            return FromJSONReader(resp.Body)

        case "application/yaml", "application/x-yaml", "text/yaml", "text/x-yaml":
            return FromYAMLReader(resp.Body)

        default:
            if l.YAMLExtensions[path.Ext(u.Path)] {
                return FromYAMLReader(resp.Body)
            }

            return FromJSONReader(resp.Body)
        }
    }

    return nil, fmt.Errorf("%w: %q", ErrUnsupportedURL, uri)
}

func QualifiedFileName(fileName, parentFileName string, resolveExtensions []string) (string, error) {
    r, err := GetRefType(fileName)
    if err != nil {
        return "", err
    }

    if r != RefTypeFile {
        return fileName[strings.Index(fileName, "://")+3:], nil
    }

    fileName = strings.TrimPrefix(fileName, "file://")

    if !filepath.IsAbs(fileName) {
        fileName = filepath.Join(filepath.Dir(parentFileName), fileName)
    }

    exts := append([]string{""}, resolveExtensions...)
    for _, ext := range exts {
        qualified := fileName + ext

        if !fileExists(qualified) {
            continue
        }

        var err error

        qualified, err = filepath.EvalSymlinks(qualified)
        if err != nil {
            return "", fmt.Errorf("error resolving symlinks in %s: %w", qualified, err)
        }

        return qualified, nil
    }

    return "", fmt.Errorf("%w %q", ErrCannotResolveSchema, fileName)
}

func fileExists(fileName string) bool {
    _, err := os.Stat(fileName)

    return err == nil || !os.IsNotExist(err)
}

func toExtensionSet(items []string) map[string]bool {
    set := make(map[string]bool, len(items))

    for _, item := range items {
        if !strings.HasPrefix(item, ".") {
            item = "." + item
        }

        set[item] = true
    }

    return set
}