Files
silo-server/internal/settingscontract/load.go
T
Quick 90224c583f fix(settings): clear the remaining review findings
Small, unrelated except that each was raised on #479.

compileObjectSchemas parsed every non-directory file under schemas/ as a JSON
Schema, so a stray editor backup or .DS_Store would panic the server at
startup through MustLoad. schema_ref can only name a .json file; anything
else is skipped.

cmd/silo used MustLoad while the ETag check beside it and every other startup
failure use log.Fatalf. It now fails the same way, so a bad contract prints
an error instead of a stack trace.

TestRegistryDefaultsMatchTheContract called scalarDefault before handling
null, and scalarDefault rejects null as non-scalar — so the subtest skipped
and the comparison after it was unreachable. A nullable contract default
could disagree with a non-empty registry default and nothing failed.
Confirmed by injecting that drift, which now reports it.

The three appearance providers each adapted the auth context to
AppearanceAuth with identical code, putting the shape of auth back in three
places that widening cache ownership would have to find. useAppearanceCacheOwner
now does it once.

useTheme.test.ts cleared storage.KEYS between cases, but appearanceCache
writes namespaced keys and an owner pointer that are not in that list, so
both survived and the suite was order-dependent. It clears the store, as
storage.test.ts already did.

The abs_smart_collection_store comment is reworded rather than given back its
SQL quotes: gofmt folds a pair of apostrophes in a doc comment into a
typographic quote, which is how it became one in the first place.

Reported by CodeRabbit review on #479.
2026-07-27 00:40:04 +00:00

223 lines
6.5 KiB
Go

package settingscontract
import (
"bytes"
"encoding/json"
"fmt"
"io/fs"
"path"
"strings"
"sync"
"github.com/santhosh-tekuri/jsonschema/v6"
settingsv1 "github.com/Silo-Server/silo-server/contracts/settings/v1"
)
// contractFS is the embedded canonical contract. Clients vendor a pinned copy
// of the same files.
var contractFS = settingsv1.FS
const (
manifestPath = "manifest.json"
manifestSchemaPath = "manifest.schema.json"
schemasDir = "schemas"
)
var (
loadOnce sync.Once
loaded *Manifest
loadedErr error
loadedRaw []byte
loadedSchemas map[string][]byte
objSchemas map[string]*jsonschema.Schema
)
// loaded contract, as returned by load(). Keeping this a value rather than
// having load() assign the package globals means load() stays pure and can be
// called with a test filesystem without clobbering the process-wide contract.
type contract struct {
manifest *Manifest
raw []byte
schemaRaw map[string][]byte
compiled map[string]*jsonschema.Schema
}
// Load returns the embedded canonical manifest, parsed and fully validated.
//
// It is loaded once per process. A malformed or self-inconsistent manifest is a
// build-time defect, not a runtime condition: the contract tests fail on it, and
// callers that reach this at runtime should treat the error as fatal at startup
// rather than degrading.
func Load() (*Manifest, error) {
loadOnce.Do(func() {
result, err := load(contractFS)
if err != nil {
loadedErr = err
return
}
loaded = result.manifest
loadedRaw = result.raw
loadedSchemas = result.schemaRaw
objSchemas = result.compiled
})
return loaded, loadedErr
}
// MustLoad returns the embedded manifest or panics. For use in server startup
// where a broken embedded contract cannot be recovered from.
func MustLoad() *Manifest {
m, err := Load()
if err != nil {
panic(fmt.Sprintf("settingscontract: embedded manifest is invalid: %v", err))
}
return m
}
// RawBytes returns the embedded manifest file exactly as checked in.
func RawBytes() ([]byte, error) {
if _, err := Load(); err != nil {
return nil, err
}
return append([]byte(nil), loadedRaw...), nil
}
// SchemaBytes returns every value schema file exactly as checked in, keyed by
// file name. These decide which object-typed values the contract accepts, so
// they are part of its identity — see ETag.
func SchemaBytes() (map[string][]byte, error) {
if _, err := Load(); err != nil {
return nil, err
}
out := make(map[string][]byte, len(loadedSchemas))
for name, body := range loadedSchemas {
out[name] = append([]byte(nil), body...)
}
return out, nil
}
// ObjectSchema returns the compiled JSON Schema for an object-typed value.
func ObjectSchema(ref string) (*jsonschema.Schema, bool) {
if _, err := Load(); err != nil {
return nil, false
}
schema, ok := objSchemas[ref]
return schema, ok
}
func load(fsys fs.FS) (contract, error) {
raw, err := fs.ReadFile(fsys, manifestPath)
if err != nil {
return contract{}, fmt.Errorf("reading manifest: %w", err)
}
if err := validateAgainstManifestSchema(fsys, raw); err != nil {
return contract{}, err
}
var manifest Manifest
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&manifest); err != nil {
return contract{}, fmt.Errorf("parsing manifest: %w", err)
}
if err := manifest.index(); err != nil {
return contract{}, err
}
schemaRaw, compiled, err := compileObjectSchemas(fsys)
if err != nil {
return contract{}, err
}
if err := manifest.Validate(compiled); err != nil {
return contract{}, err
}
return contract{manifest: &manifest, raw: raw, schemaRaw: schemaRaw, compiled: compiled}, nil
}
// validateAgainstManifestSchema checks the manifest file against its own JSON
// Schema before Go decoding, so shape errors report as schema violations with a
// location rather than as opaque unmarshal failures.
func validateAgainstManifestSchema(fsys fs.FS, raw []byte) error {
schemaBytes, err := fs.ReadFile(fsys, manifestSchemaPath)
if err != nil {
return fmt.Errorf("reading manifest schema: %w", err)
}
schemaDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaBytes))
if err != nil {
return fmt.Errorf("parsing manifest schema: %w", err)
}
compiler := jsonschema.NewCompiler()
if err := compiler.AddResource(manifestSchemaPath, schemaDoc); err != nil {
return fmt.Errorf("registering manifest schema: %w", err)
}
schema, err := compiler.Compile(manifestSchemaPath)
if err != nil {
return fmt.Errorf("compiling manifest schema: %w", err)
}
doc, err := jsonschema.UnmarshalJSON(bytes.NewReader(raw))
if err != nil {
return fmt.Errorf("parsing manifest: %w", err)
}
if err := schema.Validate(doc); err != nil {
return fmt.Errorf("manifest does not satisfy manifest.schema.json: %w", err)
}
return nil
}
// compileObjectSchemas compiles every schema under schemas/ so object-typed
// values and their defaults can be validated. Compiling all of them up front
// also catches a malformed schema file that no definition happens to reference
// yet.
func compileObjectSchemas(fsys fs.FS) (map[string][]byte, map[string]*jsonschema.Schema, error) {
entries, err := fs.ReadDir(fsys, schemasDir)
if err != nil {
return nil, nil, fmt.Errorf("reading value schema directory: %w", err)
}
compiler := jsonschema.NewCompiler()
raw := make(map[string][]byte, len(entries))
names := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// schema_ref can only name a .json file, so anything else here is not a
// value schema. Parsing it anyway would turn a stray file — an editor
// backup, a .DS_Store — into a panic at startup via MustLoad.
if !strings.HasSuffix(name, ".json") {
continue
}
body, err := fs.ReadFile(fsys, path.Join(schemasDir, name))
if err != nil {
return nil, nil, fmt.Errorf("reading value schema %s: %w", name, err)
}
doc, err := jsonschema.UnmarshalJSON(bytes.NewReader(body))
if err != nil {
return nil, nil, fmt.Errorf("parsing value schema %s: %w", name, err)
}
if err := compiler.AddResource(name, doc); err != nil {
return nil, nil, fmt.Errorf("registering value schema %s: %w", name, err)
}
raw[name] = body
names = append(names, name)
}
compiled := make(map[string]*jsonschema.Schema, len(names))
for _, name := range names {
schema, err := compiler.Compile(name)
if err != nil {
return nil, nil, fmt.Errorf("compiling value schema %s: %w", name, err)
}
compiled[name] = schema
}
return raw, compiled, nil
}