Validate schemas without relying on source filenames
This commit is contained in:
46
internal/validate/schema_portability_test.go
Normal file
46
internal/validate/schema_portability_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
func TestSchemaPreparationDoesNotDependOnSourceName(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, schema string
|
||||
rejected bool
|
||||
}{
|
||||
{"source name", `{"$defs":{"v":{"type":"integer"}},"$ref":"schema.json#/$defs/v"}`, true},
|
||||
{"unused source name", `{"$defs":{"v":{"$ref":"schema.json"}}}`, true},
|
||||
{"fragment", `{"$defs":{"v":{"type":"integer"}},"$ref":"#/$defs/v"}`, false},
|
||||
{"explicit ID", `{"$id":"https://example.invalid/schema.json","$defs":{"v":{"type":"integer"}},"$ref":"schema.json#/$defs/v"}`, false},
|
||||
{"embedded ID", `{"$defs":{"v":{"$id":"child","type":"integer"}},"$ref":"child"}`, false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "schema.json"), []byte(test.schema), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fsys := fstest.MapFS{"schema.json": &fstest.MapFile{Data: []byte(test.schema)}}
|
||||
for i, validator := range []ValidationPreparer{NewStandardValidator(dir), NewFSValidator(fsys, ".")} {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
_, err := validator.PrepareValidation(context.Background(), domain.OutputContract{Format: domain.FormatJSON, ValidationMode: domain.ValidationJSONSchema, SchemaPath: "schema.json"})
|
||||
if test.rejected {
|
||||
if err == nil || !strings.Contains(err.Error(), "source location") {
|
||||
t.Fatalf("expected source-independent rejection, got %v", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -424,6 +424,20 @@ func (l externalSchemaLoader) Load(resourceURL string) (any, error) {
|
||||
}
|
||||
|
||||
func compileSchemaDocument(ctx context.Context, resourceURL string, document any) (*jsonschema.Schema, error) {
|
||||
root, err := compileSchemaAt(ctx, resourceURL, document)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// File and private-FS retrieval URLs are never available to the provider.
|
||||
// Compiling again under an unrelated HTTPS base detects reliance on those
|
||||
// aliases without rewriting references or duplicating URI resolution rules.
|
||||
if _, err := compileSchemaAt(ctx, "https://promptkit.invalid/submitted-schema", document); err != nil {
|
||||
return nil, fmt.Errorf("schema is not independent of its source location; use fragment references or explicit schema IDs: %w", err)
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func compileSchemaAt(ctx context.Context, resourceURL string, document any) (*jsonschema.Schema, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user