Files
bifrost/transports/bifrost-http/lib/validator.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

97 lines
3.3 KiB
Go

// Package lib provides core functionality for the Bifrost HTTP service.
// This file contains JSON schema validation for config files.
package lib
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/santhosh-tekuri/jsonschema/v6"
)
// localSchemaCandidates lists paths (relative to CWD) where config.schema.json may be found
// when running from a source checkout. Checked in order before falling back to the remote URL.
var localSchemaCandidates = []string{
"config.schema.json", // running from transports/
"../config.schema.json", // running from transports/bifrost-http/
"transports/config.schema.json", // running from repo root
}
// tryLoadLocalSchema attempts to read config.schema.json from known local paths.
// Returns nil if none are found.
func tryLoadLocalSchema() []byte {
for _, p := range localSchemaCandidates {
data, err := os.ReadFile(p)
if err == nil {
return data
}
}
return nil
}
// ValidateConfigSchema validates config data against the JSON schema.
// Returns nil if valid, or a formatted error describing all validation failures.
// An optional schemaOverride can be provided to use a local schema instead of fetching from the remote URL.
func ValidateConfigSchema(data []byte, schemaOverride ...[]byte) error {
var configSchemaJSONBytes []byte
if len(schemaOverride) > 0 && len(schemaOverride[0]) > 0 {
configSchemaJSONBytes = schemaOverride[0]
} else if localSchema := tryLoadLocalSchema(); localSchema != nil {
// Prefer the local schema file from the source checkout when available.
// This avoids validating against a potentially stale remote schema.
configSchemaJSONBytes = localSchema
} else {
// Pulling config.schema from https://www.getbifrost.ai/schema
configSchemaJSON, err := http.Get("https://www.getbifrost.ai/schema")
if err != nil {
return fmt.Errorf("failed to get config schema: %w", err)
}
defer configSchemaJSON.Body.Close()
var readErr error
configSchemaJSONBytes, readErr = io.ReadAll(configSchemaJSON.Body)
if readErr != nil {
logger.Warn("failed to download config schema: %v. running without config.json schema validation", readErr)
return nil
}
}
// Parse the schema JSON
schemaDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(configSchemaJSONBytes))
if err != nil {
return fmt.Errorf("failed to parse config schema JSON: %w", err)
}
c := jsonschema.NewCompiler()
if err := c.AddResource("config.schema.json", schemaDoc); err != nil {
return fmt.Errorf("failed to add config schema resource: %w", err)
}
// Compile the schema
compiledSchema, err := c.Compile("config.schema.json")
if err != nil {
return fmt.Errorf("failed to compile config schema: %w", err)
}
var v any
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
err = compiledSchema.Validate(v)
if err == nil {
return nil
}
// Format validation errors for better readability
return formatValidationError(err)
}
// formatValidationError converts jsonschema validation errors into user-friendly messages
func formatValidationError(err error) error {
validationErr, ok := err.(*jsonschema.ValidationError)
if !ok {
return err
}
// Use the GoString format which provides detailed hierarchical output
return fmt.Errorf("schema validation failed:\n%s", validationErr.GoString())
}