2026-07-07 21:32:56 +00:00
package validjsonschema
import (
"bytes"
"context"
"fmt"
2026-08-09 01:38:42 +00:00
"sync"
2026-07-07 21:32:56 +00:00
"github.com/santhosh-tekuri/jsonschema/v6"
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)
const Key = "generic/valid_json_schema"
const ReasonCodeInvalidJSON = "invalid_json"
const ReasonCodeSchemaInvalid = "json_schema_invalid"
2026-07-17 07:02:30 +00:00
type Options struct { }
2026-08-09 01:38:42 +00:00
type schemaIdentity struct {
id string
name string
version string
digest string
}
type compiledSchema struct {
schema * jsonschema . Schema
err error
}
type schemaCompiler func ( [ ] byte ) ( * jsonschema . Schema , error )
type Validator struct {
mu sync . Mutex
schemas map [ schemaIdentity ] compiledSchema
compile schemaCompiler
}
2026-07-17 07:02:30 +00:00
var _ contracts . SerializedValidator = ( * Validator ) ( nil )
2026-07-07 21:32:56 +00:00
2026-08-09 01:38:42 +00:00
func New ( Options ) * Validator {
return & Validator { schemas : make ( map [ schemaIdentity ] compiledSchema ) , compile : compileSchema }
}
2026-07-17 07:02:30 +00:00
func ( v * Validator ) Name ( ) string { return Key }
func ( v * Validator ) ExecutionClass ( ) contracts . ExecutionClass {
return contracts . ExecutionClassDeterministic
2026-07-07 21:32:56 +00:00
}
2026-07-17 07:02:30 +00:00
func ( v * Validator ) Validate ( _ context . Context , req contracts . SerializedValidationRequest ) ( contracts . ValidationResult , error ) {
2026-08-09 01:38:42 +00:00
if len ( req . Schema . JSONSchema ) == 0 {
_ , err := v . compiledSchema ( req . Schema )
return contracts . ValidationResult { } , err
2026-07-07 21:32:56 +00:00
}
2026-08-09 01:38:42 +00:00
instance , err := jsonschema . UnmarshalJSON ( bytes . NewReader ( req . Content ) )
2026-07-07 21:32:56 +00:00
if err != nil {
2026-08-27 11:50:37 +00:00
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCodeInvalidJSON , Message : "payload is not valid JSON" , CorrectionGuidance : "Return one complete replacement response encoded as valid JSON." } , nil
2026-07-07 21:32:56 +00:00
}
2026-08-09 01:38:42 +00:00
schema , err := v . compiledSchema ( req . Schema )
if err != nil {
return contracts . ValidationResult { } , err
}
if err := schema . Validate ( instance ) ; err != nil {
2026-08-27 11:50:37 +00:00
return contracts . ValidationResult { Approved : false , ReasonCode : ReasonCodeSchemaInvalid , Message : "payload does not conform to response schema" , CorrectionGuidance : "Return one complete JSON replacement that conforms exactly to the supplied response schema." } , nil
2026-08-09 01:38:42 +00:00
}
return contracts . ValidationResult { Approved : true } , nil
}
func ( v * Validator ) compiledSchema ( schema contracts . ArtifactSchema ) ( * jsonschema . Schema , error ) {
identity := schemaIdentity {
id : schema . ID ,
name : schema . Name ,
version : schema . Version ,
digest : contracts . DigestArtifactSchema ( schema ) ,
}
v . mu . Lock ( )
defer v . mu . Unlock ( )
if v . schemas == nil {
v . schemas = make ( map [ schemaIdentity ] compiledSchema )
}
if cached , ok := v . schemas [ identity ] ; ok {
return cached . schema , cached . err
}
compiler := v . compile
if compiler == nil {
compiler = compileSchema
}
compiled , err := compiler ( schema . JSONSchema )
v . schemas [ identity ] = compiledSchema { schema : compiled , err : err }
return compiled , err
}
func compileSchema ( schemaContent [ ] byte ) ( * jsonschema . Schema , error ) {
if len ( schemaContent ) == 0 {
return nil , fmt . Errorf ( "response schema content is not available" )
}
2026-07-17 07:02:30 +00:00
schemaDocument , err := jsonschema . UnmarshalJSON ( bytes . NewReader ( schemaContent ) )
2026-07-07 21:32:56 +00:00
if err != nil {
2026-08-09 01:38:42 +00:00
return nil , fmt . Errorf ( "parse response schema: %w" , err )
2026-07-07 21:32:56 +00:00
}
compiler := jsonschema . NewCompiler ( )
if err := compiler . AddResource ( "schema.json" , schemaDocument ) ; err != nil {
2026-08-09 01:38:42 +00:00
return nil , fmt . Errorf ( "load response schema: %w" , err )
2026-07-07 21:32:56 +00:00
}
schema , err := compiler . Compile ( "schema.json" )
if err != nil {
2026-08-09 01:38:42 +00:00
return nil , fmt . Errorf ( "compile response schema: %w" , err )
2026-07-07 21:32:56 +00:00
}
2026-08-09 01:38:42 +00:00
return schema , nil
2026-07-07 21:32:56 +00:00
}
func Spec ( ) pipeline . ValidatorSpec {
2026-07-17 07:02:30 +00:00
return pipeline . ValidatorSpec { Key : Key , ExecutionClass : contracts . ExecutionClassDeterministic }
2026-07-07 21:32:56 +00:00
}
func Register ( registry * pipeline . ValidatorRegistry ) error {
2026-07-17 08:13:08 +00:00
return pipeline . RegisterSerializedValidatorBuilder ( registry , pipeline . SerializedValidatorSpec {
2026-07-17 07:02:30 +00:00
ValidatorSpec : Spec ( ) , SupportsChunks : true , SupportsArtifacts : true ,
} , validateOptions , func ( request pipeline . BuildRequest ) ( contracts . SerializedValidator , error ) {
options , err := DecodeOptions ( request . Options )
if err != nil {
return nil , err
}
return New ( options ) , nil
2026-07-07 21:32:56 +00:00
} )
}
2026-07-17 07:02:30 +00:00
func DecodeOptions ( options map [ string ] any ) ( Options , error ) {
if err := pipeline . RejectUnknownOptions ( options ) ; err != nil {
return Options { } , err
}
return Options { } , nil
}
func validateOptions ( options map [ string ] any ) error { _ , err := DecodeOptions ( options ) ; return err }