Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ type SchemaCacheEntry struct {
}

// SchemaCache defines the interface for schema caching implementations.
// The key is a [32]byte hash of the schema (from schema.GoLow().Hash()).
// The key is a uint64 hash of the schema (from schema.GoLow().Hash()).
type SchemaCache interface {
Load(key [32]byte) (*SchemaCacheEntry, bool)
Store(key [32]byte, value *SchemaCacheEntry)
Range(f func(key [32]byte, value *SchemaCacheEntry) bool)
Load(key uint64) (*SchemaCacheEntry, bool)
Store(key uint64, value *SchemaCacheEntry)
Range(f func(key uint64, value *SchemaCacheEntry) bool)
}
56 changes: 23 additions & 33 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ func TestDefaultCache_StoreAndLoad(t *testing.T) {
CompiledSchema: &jsonschema.Schema{},
}

// Create a test key (32-byte hash)
var key [32]byte
copy(key[:], []byte("test-schema-hash-12345678901234"))
// Create a test key (uint64 hash)
key := uint64(0x123456789abcdef0)

// Store the schema
cache.Store(key, testSchema)
Expand All @@ -49,8 +48,7 @@ func TestDefaultCache_LoadMissing(t *testing.T) {
cache := NewDefaultCache()

// Try to load a key that doesn't exist
var key [32]byte
copy(key[:], []byte("nonexistent-key-12345678901234"))
key := uint64(0xdeadbeef)

loaded, ok := cache.Load(key)
assert.False(t, ok, "Should not find non-existent key")
Expand All @@ -60,7 +58,7 @@ func TestDefaultCache_LoadMissing(t *testing.T) {
func TestDefaultCache_LoadNilCache(t *testing.T) {
var cache *DefaultCache

var key [32]byte
key := uint64(0)
loaded, ok := cache.Load(key)

assert.False(t, ok)
Expand All @@ -71,7 +69,7 @@ func TestDefaultCache_StoreNilCache(t *testing.T) {
var cache *DefaultCache

// Should not panic
var key [32]byte
key := uint64(0)
cache.Store(key, &SchemaCacheEntry{})

// Verify nothing was stored (cache is nil)
Expand All @@ -82,10 +80,9 @@ func TestDefaultCache_Range(t *testing.T) {
cache := NewDefaultCache()

// Store multiple entries
entries := make(map[[32]byte]*SchemaCacheEntry)
entries := make(map[uint64]*SchemaCacheEntry)
for i := 0; i < 5; i++ {
var key [32]byte
copy(key[:], []byte{byte(i)})
key := uint64(i)

entry := &SchemaCacheEntry{
RenderedInline: []byte{byte(i)},
Expand All @@ -97,8 +94,8 @@ func TestDefaultCache_Range(t *testing.T) {

// Range over all entries
count := 0
foundKeys := make(map[[32]byte]bool)
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
foundKeys := make(map[uint64]bool)
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
foundKeys[key] = true

Expand All @@ -118,14 +115,13 @@ func TestDefaultCache_RangeEarlyTermination(t *testing.T) {

// Store multiple entries
for i := 0; i < 10; i++ {
var key [32]byte
copy(key[:], []byte{byte(i)})
key := uint64(i)
cache.Store(key, &SchemaCacheEntry{})
}

// Range but stop after 3 iterations
count := 0
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
return count < 3 // Stop after 3
})
Expand All @@ -138,7 +134,7 @@ func TestDefaultCache_RangeNilCache(t *testing.T) {

// Should not panic
called := false
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
called = true
return true
})
Expand All @@ -151,7 +147,7 @@ func TestDefaultCache_RangeEmpty(t *testing.T) {

// Range over empty cache
count := 0
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
return true
})
Expand All @@ -162,8 +158,7 @@ func TestDefaultCache_RangeEmpty(t *testing.T) {
func TestDefaultCache_Overwrite(t *testing.T) {
cache := NewDefaultCache()

var key [32]byte
copy(key[:], []byte("test-key"))
key := uint64(0x12345678)

// Store first value
first := &SchemaCacheEntry{
Expand All @@ -188,10 +183,9 @@ func TestDefaultCache_MultipleKeys(t *testing.T) {
cache := NewDefaultCache()

// Store with different keys
var key1, key2, key3 [32]byte
copy(key1[:], []byte("key1"))
copy(key2[:], []byte("key2"))
copy(key3[:], []byte("key3"))
key1 := uint64(1)
key2 := uint64(2)
key3 := uint64(3)

cache.Store(key1, &SchemaCacheEntry{RenderedInline: []byte("value1")})
cache.Store(key2, &SchemaCacheEntry{RenderedInline: []byte("value2")})
Expand All @@ -218,8 +212,7 @@ func TestDefaultCache_ThreadSafety(t *testing.T) {
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func(val int) {
var key [32]byte
copy(key[:], []byte{byte(val)})
key := uint64(val)
cache.Store(key, &SchemaCacheEntry{
RenderedInline: []byte{byte(val)},
})
Expand All @@ -235,8 +228,7 @@ func TestDefaultCache_ThreadSafety(t *testing.T) {
// Concurrent reads
for i := 0; i < 10; i++ {
go func(val int) {
var key [32]byte
copy(key[:], []byte{byte(val)})
key := uint64(val)
loaded, ok := cache.Load(key)
assert.True(t, ok)
assert.NotNil(t, loaded)
Expand All @@ -251,7 +243,7 @@ func TestDefaultCache_ThreadSafety(t *testing.T) {

// Verify all entries exist
count := 0
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
return true
})
Expand Down Expand Up @@ -284,20 +276,18 @@ func TestDefaultCache_RangeWithInvalidTypes(t *testing.T) {
cache.m.Store("invalid-key-type", &SchemaCacheEntry{})

// Store an entry with wrong value type
var validKey [32]byte
copy(validKey[:], []byte{1})
validKey := uint64(1)
cache.m.Store(validKey, "invalid-value-type")

// Store a valid entry
var validKey2 [32]byte
copy(validKey2[:], []byte{2})
validKey2 := uint64(2)
validEntry := &SchemaCacheEntry{RenderedInline: []byte("valid")}
cache.Store(validKey2, validEntry)

// Range should skip invalid entries and only process valid ones
count := 0
var seenEntry *SchemaCacheEntry
cache.Range(func(key [32]byte, value *SchemaCacheEntry) bool {
cache.Range(func(key uint64, value *SchemaCacheEntry) bool {
count++
seenEntry = value
return true
Expand Down
8 changes: 4 additions & 4 deletions cache/default_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewDefaultCache() *DefaultCache {
}

// Load retrieves a schema from the cache.
func (c *DefaultCache) Load(key [32]byte) (*SchemaCacheEntry, bool) {
func (c *DefaultCache) Load(key uint64) (*SchemaCacheEntry, bool) {
if c == nil || c.m == nil {
return nil, false
}
Expand All @@ -28,20 +28,20 @@ func (c *DefaultCache) Load(key [32]byte) (*SchemaCacheEntry, bool) {
}

// Store saves a schema to the cache.
func (c *DefaultCache) Store(key [32]byte, value *SchemaCacheEntry) {
func (c *DefaultCache) Store(key uint64, value *SchemaCacheEntry) {
if c == nil || c.m == nil {
return
}
c.m.Store(key, value)
}

// Range calls f for each entry in the cache (for testing/inspection).
func (c *DefaultCache) Range(f func(key [32]byte, value *SchemaCacheEntry) bool) {
func (c *DefaultCache) Range(f func(key uint64, value *SchemaCacheEntry) bool) {
if c == nil || c.m == nil {
return
}
c.m.Range(func(k, v interface{}) bool {
key, ok := k.([32]byte)
key, ok := k.(uint64)
if !ok {
return true
}
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ go 1.25.0
require (
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad
github.com/dlclark/regexp2 v1.11.5
github.com/goccy/go-yaml v1.19.1
github.com/pb33f/jsonpath v0.7.0
github.com/pb33f/libopenapi v0.31.2
github.com/goccy/go-yaml v1.19.2
github.com/pb33f/jsonpath v0.7.1
github.com/pb33f/libopenapi v0.33.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/text v0.32.0
go.yaml.in/yaml/v4 v4.0.0-rc.4
golang.org/x/text v0.33.0
)

require (
Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pb33f/jsonpath v0.7.0 h1:3oG6yu1RqNoMZpqnRjBMqi8fSIXWoDAKDrsB0QGTcoU=
github.com/pb33f/jsonpath v0.7.0/go.mod h1:/+JlSIjWA2ijMVYGJ3IQPF4Q1nLMYbUTYNdk0exCDPQ=
github.com/pb33f/libopenapi v0.31.2 h1:dcFG9cPH7LvSejbemqqpSa3yrHYZs8eBHNdMx8ayIVc=
github.com/pb33f/libopenapi v0.31.2/go.mod h1:oaebeA5l58AFbZ7qRKTtMnu15JEiPlaBas1vLDcw9vs=
github.com/pb33f/jsonpath v0.7.1 h1:dEp6oIZuJbpDSyuHAl9m7GonoDW4M20BcD5vT0tPYRE=
github.com/pb33f/jsonpath v0.7.1/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo=
github.com/pb33f/libopenapi v0.33.0 h1:s0mZhtxNW4ko8npYzMKVOUYsEs5QqZdywxGlbUE52z0=
github.com/pb33f/libopenapi v0.33.0/go.mod h1:e/dmd2Pf1nkjqkI0r7guFSyt9T5V0IIQKgs0L6B/3b0=
github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ=
github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -34,8 +34,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
Expand Down Expand Up @@ -73,8 +73,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Expand Down
24 changes: 23 additions & 1 deletion schema_validation/validate_document.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
// SPDX-License-Identifier: MIT

package schema_validation
Expand Down Expand Up @@ -34,6 +34,28 @@ func ValidateOpenAPIDocument(doc libopenapi.Document, opts ...config.Option) (bo
info := doc.GetSpecInfo()
loadedSchema := info.APISchema
var validationErrors []*liberrors.ValidationError

// Check if SpecJSON is nil before dereferencing
if info.SpecJSON == nil {
violation := &liberrors.SchemaValidationFailure{
Reason: "document SpecJSON is nil - document may not be properly parsed",
Location: "document root",
ReferenceSchema: loadedSchema,
}
validationErrors = append(validationErrors, &liberrors.ValidationError{
ValidationType: "schema",
ValidationSubType: "document",
Message: "OpenAPI document validation failed",
Reason: "The document's SpecJSON is nil, indicating the document was not properly parsed or is empty",
SpecLine: 1,
SpecCol: 0,
SchemaValidationErrors: []*liberrors.SchemaValidationFailure{violation},
HowToFix: "ensure the OpenAPI document is valid YAML/JSON and can be properly parsed by libopenapi",
Context: "document root",
})
return false, validationErrors
}

decodedDocument := *info.SpecJSON

// Compile the JSON Schema
Expand Down
40 changes: 39 additions & 1 deletion schema_validation/validate_document_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
// SPDX-License-Identifier: MIT

package schema_validation
Expand Down Expand Up @@ -175,3 +175,41 @@ info:
assert.Len(t, errors, 1)
assert.Len(t, errors[0].SchemaValidationErrors, 6)
}

func TestValidateDocument_NilSpecJSON(t *testing.T) {
// Create a document with minimal valid OpenAPI content
spec := `openapi: 3.1.0
info:
version: 1.0.0
title: Test
`

doc, _ := libopenapi.NewDocument([]byte(spec))

// Simulate the nil SpecJSON scenario by setting it to nil
info := doc.GetSpecInfo()
info.SpecJSON = nil

// validate!
valid, errors := ValidateOpenAPIDocument(doc)

// Should fail validation due to nil SpecJSON
assert.False(t, valid)
assert.Len(t, errors, 1)

// Verify error structure
validationError := errors[0]
assert.Equal(t, "schema", validationError.ValidationType)
assert.Equal(t, "document", validationError.ValidationSubType)
assert.Equal(t, "OpenAPI document validation failed", validationError.Message)
assert.Contains(t, validationError.Reason, "SpecJSON is nil")
assert.Contains(t, validationError.HowToFix, "ensure the OpenAPI document is valid")
assert.Equal(t, 1, validationError.SpecLine)
assert.Equal(t, 0, validationError.SpecCol)

// Verify schema validation errors
assert.NotEmpty(t, validationError.SchemaValidationErrors)
schemaErr := validationError.SchemaValidationErrors[0]
assert.Equal(t, "document root", schemaErr.Location)
assert.Contains(t, schemaErr.Reason, "SpecJSON is nil")
}
2 changes: 1 addition & 1 deletion strict/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func (v *Validator) getSchemaKey(schema *base.Schema) string {
}
if low := schema.GoLow(); low != nil {
hash := low.Hash()
return fmt.Sprintf("%x", hash[:8]) // Use first 8 bytes for shorter key
return fmt.Sprintf("%x", hash) // uint64 hash as hex string
}
// fallback to pointer address for inline schemas without low-level info
return fmt.Sprintf("%p", schema)
Expand Down
2 changes: 1 addition & 1 deletion validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ func warmMediaTypeSchema(mediaType *v3.MediaType, schemaCache cache.SchemaCache,
func warmParameterSchema(param *v3.Parameter, schemaCache cache.SchemaCache, options *config.ValidationOptions) {
if param != nil {
var schema *base.Schema
var hash [32]byte
var hash uint64

// Parameters can have schemas in two places: schema property or content property
if param.Schema != nil {
Expand Down
Loading