Share self-contained schema compilation

This commit is contained in:
2026-11-05 13:56:03 +00:00
parent 1e122f0473
commit e056cb1607
4 changed files with 60 additions and 205 deletions

View File

@@ -75,51 +75,23 @@ func (v *StandardValidator) PrepareValidation(ctx context.Context, contract doma
if err := ctx.Err(); err != nil {
return nil, err
}
prepared := &preparedValidation{contract: contract}
if contract.ValidationMode != domain.ValidationJSONSchema {
return prepared, nil
}
resolvedSchemaPath, err := v.resolveSchemaPath(ctx, contract.SchemaPath)
name, err := v.resolveSchemaPath(ctx, contract.SchemaPath)
if err != nil {
return nil, err
}
schemaDocument, err := loadJSONSchemaFile(ctx, resolvedSchemaPath)
document, err := loadJSONSchemaFile(ctx, name)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", resolvedSchemaPath, err)
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", name, err)
}
schemaRoot, err := v.schemaRoot()
schema, err := compileSchemaDocument(ctx, fileSchemaResourceURL(name).String(), document)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", name, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
compiler := newSchemaCompiler(standardSchemaLoader{ctx: ctx, root: schemaRoot})
resourceURL := fileSchemaResourceURL(resolvedSchemaPath)
if err := ctx.Err(); err != nil {
return nil, err
}
if err := compiler.AddResource(resourceURL.String(), schemaDocument); err != nil {
if contextErr := ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, fmt.Errorf("failed to register JSON schema %q: %w", resolvedSchemaPath, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
schema, err := compileJSONSchema(ctx, resourceURL.String(), compiler.Compile)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", resolvedSchemaPath, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
prepared.schemaDocument = schemaDocument
prepared.schema = schema
prepared.schemaDocument, prepared.schema = document, schema
return prepared, nil
}
@@ -127,40 +99,19 @@ func (v *FSValidator) PrepareValidation(ctx context.Context, contract domain.Out
if err := ctx.Err(); err != nil {
return nil, err
}
prepared := &preparedValidation{contract: contract}
if contract.ValidationMode != domain.ValidationJSONSchema {
return prepared, nil
}
schemaName, schemaDocument, err := v.loadSchemaDocument(ctx, contract.SchemaPath)
name, document, err := v.loadSchemaDocument(ctx, contract.SchemaPath)
if err != nil {
return nil, err
}
resourceURL := fsSchemaResourceURL(schemaName)
compiler := newSchemaCompiler(fsSchemaLoader{ctx: ctx, fsys: v.fsys, root: filecatalog.CleanFSRoot(v.root)})
if err := ctx.Err(); err != nil {
return nil, err
}
if err := compiler.AddResource(resourceURL.String(), schemaDocument); err != nil {
if contextErr := ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, fmt.Errorf("failed to register JSON schema %q: %w", schemaName, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
schema, err := compileJSONSchema(ctx, resourceURL.String(), compiler.Compile)
schema, err := compileSchemaDocument(ctx, fsSchemaResourceURL(name).String(), document)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", schemaName, err)
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", name, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
prepared.schemaDocument = schemaDocument
prepared.schema = schema
prepared.schemaDocument, prepared.schema = document, schema
return prepared, nil
}
@@ -247,56 +198,30 @@ func validateArtifact(ctx context.Context, artifact *domain.Artifact, contract d
}
func (v *StandardValidator) validateJSONSchema(ctx context.Context, instance any, schemaPath string) ([]string, error) {
resolvedSchemaPath, err := v.resolveSchemaPath(ctx, schemaPath)
name, err := v.resolveSchemaPath(ctx, schemaPath)
if err != nil {
return nil, err
}
schemaRoot, err := v.schemaRoot()
document, err := loadJSONSchemaFile(ctx, name)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", name, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
compiler := newSchemaCompiler(standardSchemaLoader{ctx: ctx, root: schemaRoot})
resourceURL := fileSchemaResourceURL(resolvedSchemaPath)
schema, err := compileJSONSchema(ctx, resourceURL.String(), compiler.Compile)
schema, err := compileSchemaDocument(ctx, fileSchemaResourceURL(name).String(), document)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", resolvedSchemaPath, err)
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", name, err)
}
return executeJSONSchema(ctx, schema, instance)
}
func (v *FSValidator) validateJSONSchema(ctx context.Context, instance any, schemaPath string) ([]string, error) {
schemaName, schemaDoc, err := v.loadSchemaDocument(ctx, schemaPath)
name, document, err := v.loadSchemaDocument(ctx, schemaPath)
if err != nil {
return nil, err
}
resourceURL := fsSchemaResourceURL(schemaName)
if err := validateSchemaDialect(schemaDoc); err != nil {
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", schemaName, err)
}
compiler := newSchemaCompiler(fsSchemaLoader{ctx: ctx, fsys: v.fsys, root: filecatalog.CleanFSRoot(v.root)})
if err := ctx.Err(); err != nil {
return nil, err
}
if err := compiler.AddResource(resourceURL.String(), schemaDoc); err != nil {
if contextErr := ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, fmt.Errorf("failed to register JSON schema %q: %w", schemaName, err)
}
if err := ctx.Err(); err != nil {
return nil, err
}
schema, err := compileJSONSchema(ctx, resourceURL.String(), compiler.Compile)
schema, err := compileSchemaDocument(ctx, fsSchemaResourceURL(name).String(), document)
if err != nil {
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", schemaName, err)
return nil, fmt.Errorf("failed to compile JSON schema %q: %w", name, err)
}
return executeJSONSchema(ctx, schema, instance)
}
@@ -489,11 +414,29 @@ func fsSchemaResourceURL(schemaName string) *url.URL {
}
}
func newSchemaCompiler(loader jsonschema.URLLoader) *jsonschema.Compiler {
type externalSchemaLoader struct{ ctx context.Context }
func (l externalSchemaLoader) Load(resourceURL string) (any, error) {
if err := l.ctx.Err(); err != nil {
return nil, err
}
return nil, fmt.Errorf("external schema resource %q is not supported; use a self-contained schema", resourceURL)
}
func compileSchemaDocument(ctx context.Context, resourceURL string, document any) (*jsonschema.Schema, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
compiler := jsonschema.NewCompiler()
compiler.DefaultDraft(jsonschema.Draft2020)
compiler.UseLoader(loader)
return compiler
compiler.UseLoader(externalSchemaLoader{ctx: ctx})
if err := compiler.AddResource(resourceURL, document); err != nil {
if contextErr := ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, fmt.Errorf("register schema: %w", err)
}
return compileJSONSchema(ctx, resourceURL, compiler.Compile)
}
type schemaExecutor interface {
@@ -544,36 +487,6 @@ func validateSchemaDialect(doc any) error {
return nil
}
type standardSchemaLoader struct {
ctx context.Context
root string
}
func (l standardSchemaLoader) Load(resourceURL string) (any, error) {
if err := l.ctx.Err(); err != nil {
return nil, err
}
parsed, err := url.Parse(resourceURL)
if err != nil || parsed.Scheme != "file" || parsed.Host != "" || parsed.RawQuery != "" || parsed.Opaque != "" {
return nil, fmt.Errorf("schema reference %q is not a contained file reference", resourceURL)
}
fileName, err := (jsonschema.FileLoader{}).ToFile(resourceURL)
if err != nil {
return nil, fmt.Errorf("schema reference %q is not a contained file reference: %w", resourceURL, err)
}
resolved, err := containedFilesystemPath(l.root, fileName)
if err != nil {
if contextErr := l.ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, err
}
if err := l.ctx.Err(); err != nil {
return nil, err
}
return loadJSONSchemaFile(l.ctx, resolved)
}
func containedFilesystemPath(root, name string) (string, error) {
candidate := name
if !filepath.IsAbs(candidate) {
@@ -666,66 +579,3 @@ func readSchemaFile(ctx context.Context, open func() (fs.File, error)) ([]byte,
}
}
}
type fsSchemaLoader struct {
ctx context.Context
fsys fs.FS
root string
}
func (l fsSchemaLoader) Load(resourceURL string) (any, error) {
if err := l.ctx.Err(); err != nil {
return nil, err
}
parsed, err := url.Parse(resourceURL)
if err != nil {
return nil, fmt.Errorf("invalid schema reference %q: %w", resourceURL, err)
}
if parsed.Scheme != "promptkit-schema" || parsed.Host != "" || parsed.RawQuery != "" || parsed.Opaque != "" {
return nil, fmt.Errorf("schema reference %q is not allowed", resourceURL)
}
name := strings.TrimPrefix(parsed.Path, "/")
name = path.Clean(name)
if l.root == "." {
if strings.HasPrefix(name, "../") || name == ".." {
return nil, fmt.Errorf("schema reference %q escapes source root", resourceURL)
}
} else if name != l.root && !strings.HasPrefix(name, l.root+"/") {
return nil, fmt.Errorf("schema reference %q escapes source root", resourceURL)
}
rootInfo, err := fs.Stat(l.fsys, l.root)
if err != nil {
if contextErr := l.ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, err
}
if err := l.ctx.Err(); err != nil {
return nil, err
}
if !rootInfo.IsDir() && name != l.root {
return nil, fmt.Errorf("schema reference %q is outside the configured schema file", resourceURL)
}
raw, err := readSchemaFile(l.ctx, func() (fs.File, error) {
return l.fsys.Open(name)
})
if err != nil {
return nil, err
}
doc, err := decodeJSONValue(l.ctx, raw)
if err != nil {
return nil, err
}
if err := validateSchemaDialect(doc); err != nil {
if contextErr := l.ctx.Err(); contextErr != nil {
return nil, contextErr
}
return nil, err
}
if err := l.ctx.Err(); err != nil {
return nil, err
}
return doc, nil
}