Skip to content
Draft
147 changes: 147 additions & 0 deletions bitfield_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package schema_test

import (
"encoding/json"
"testing"

schema "github.com/lestrrat-go/json-schema"
"github.com/stretchr/testify/require"
)

func TestBitFieldFunctionality(t *testing.T) {
t.Parallel()
t.Run("Has method returns correct bit flags", func(t *testing.T) {
t.Parallel()
// Test with an empty schema
s := schema.New()
require.False(t, s.Has(schema.AnchorField), "Expected empty schema to have no fields set")

// Test with a builder
builder := schema.NewBuilder()
s = builder.
Anchor("test-anchor").
Maximum(100.0).
MinLength(5).
MustBuild()

// Test individual field checks still work
require.True(t, s.Has(schema.AnchorField), "Expected HasAnchor() to return true")
require.True(t, s.Has(schema.MaximumField), "Expected HasMaximum() to return true")
require.True(t, s.Has(schema.MinLengthField), "Expected HasMinLength() to return true")
require.False(t, s.Has(schema.MinimumField), "Expected HasMinimum() to return false")

// Test the new Has method with multiple fields
require.True(t, s.Has(schema.AnchorField|schema.MaximumField|schema.MinLengthField), "Expected Has() to return true for all set fields")

// Test with a missing field
require.False(t, s.Has(schema.AnchorField|schema.MaximumField|schema.MinimumField), "Expected Has() to return false when one field is missing")
})

t.Run("Bit field operations work correctly", func(t *testing.T) {
t.Parallel()
s := schema.NewBuilder().
Anchor("test").
Property("foo", schema.New()).
AllOf(schema.BoolSchema(true)).
MustBuild()

// Test the new Has() method for combined bit field operations
requiredFields := schema.AnchorField | schema.PropertiesField | schema.AllOfField
require.True(t, s.Has(requiredFields), "Expected all three fields to be set")

// Test that combination with an unset field returns false
requiredFieldsWithMissing := schema.AnchorField | schema.PropertiesField | schema.MinimumField
require.False(t, s.Has(requiredFieldsWithMissing), "Expected combination with unset field to fail")
})

t.Run("JSON unmarshaling sets bit fields", func(t *testing.T) {
t.Parallel()
jsonData := `{
"$anchor": "test-anchor",
"type": "string",
"minLength": 10,
"properties": {
"name": {"type": "string"}
}
}`

var s schema.Schema
err := json.Unmarshal([]byte(jsonData), &s)
require.NoError(t, err, "Failed to unmarshal JSON")

// Check that the appropriate bit fields are set
require.True(t, s.Has(schema.AnchorField), "Expected HasAnchor() to return true after JSON unmarshal")
require.True(t, len(s.Types()) > 0, "Expected HasTypes() to return true after JSON unmarshal")
require.True(t, s.Has(schema.MinLengthField), "Expected HasMinLength() to return true after JSON unmarshal")
require.True(t, s.Has(schema.PropertiesField), "Expected HasProperties() to return true after JSON unmarshal")

// Check that unset fields return false
require.False(t, s.Has(schema.MaxLengthField), "Expected HasMaxLength() to return false")

// Verify the bit field contains the expected flags using Has method
expectedFields := schema.AnchorField | schema.TypesField | schema.MinLengthField | schema.PropertiesField
require.True(t, s.Has(expectedFields), "Expected all bit fields to be set")
})

t.Run("Clone preserves bit fields", func(t *testing.T) {
t.Parallel()
original := schema.NewBuilder().
Anchor("original").
Maximum(50.0).
Required("field1", "field2").
MustBuild()

cloned := schema.NewBuilder().Clone(original).MustBuild()

// Check that the cloned schema has the same populated fields
expectedFields := schema.AnchorField | schema.MaximumField | schema.RequiredField
require.True(t, cloned.Has(expectedFields) && original.Has(expectedFields), "Expected both original and cloned schema to have all expected fields set")

// Test specific fields
require.True(t, cloned.Has(schema.AnchorField) && cloned.Anchor() == "original", "Expected cloned schema to have anchor 'original'")
require.True(t, cloned.Has(schema.MaximumField) && cloned.Maximum() == 50.0, "Expected cloned schema to have maximum 50.0")
require.True(t, cloned.Has(schema.RequiredField), "Expected cloned schema to have required fields")
})

t.Run("Reset method clears bit fields", func(t *testing.T) {
t.Parallel()
s := schema.NewBuilder().
Anchor("test").
Maximum(100.0).
Reset(schema.AnchorField).
MustBuild()

// Should have Maximum but not Anchor
require.False(t, s.Has(schema.AnchorField), "Expected HasAnchor() to return false after Reset(schema.AnchorField)")
require.True(t, s.Has(schema.MaximumField), "Expected HasMaximum() to return true")

// Bit field should only have schema.MaximumField set
require.True(t, s.Has(schema.MaximumField) && !s.Has(schema.AnchorField), "Expected only schema.MaximumField to be set after Reset(schema.AnchorField)")
})
}

func TestBitFieldEfficiency(t *testing.T) {
t.Parallel()
// This demonstrates the efficiency improvement mentioned in the spec
s := schema.NewBuilder().
Anchor("test").
Property("field1", schema.New()).
AllOf(schema.BoolSchema(true)).
MustBuild()

// The old way (this is what we're replacing):
// if s.HasAnchor() && s.HasProperties() && s.HasAllOf() { ... }

// The new efficient way using bit fields with Has() method:
requiredFlags := schema.AnchorField | schema.PropertiesField | schema.AllOfField
require.True(t, s.Has(requiredFlags), "Expected all three fields to be populated")

// Test that missing field makes the check fail
s2 := schema.NewBuilder().
Anchor("test").
Property("field1", schema.New()).
// Note: no AllOf
MustBuild()

require.False(t, s2.Has(requiredFlags), "Expected check to fail when AllOf is not set")
}
108 changes: 108 additions & 0 deletions bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package schema

import (
"encoding/json"
"fmt"
)

// SchemaOrBool is an interface for types that can be either a Schema or boolean
type SchemaOrBool interface { //nolint:revive
schemaOrBool() // internal identifier
}

// BoolSchema represents a boolean value in allOf, oneOf, anyOf, etc
type BoolSchema bool

// schemaOrBool implements the SchemaOrBool interface
func (s BoolSchema) schemaOrBool() {}

// UnmarshalJSON implements json.Unmarshaler
func (s *BoolSchema) UnmarshalJSON(data []byte) error {
var b bool
if err := json.Unmarshal(data, &b); err != nil {
return fmt.Errorf("failed to unmarshal BoolSchema: %w", err)
}
*s = BoolSchema(b)
return nil
}

// MarshalJSON implements json.Marshaler
func (s BoolSchema) MarshalJSON() ([]byte, error) {
return json.Marshal(bool(s))
}

// Convenience variables and functions for BoolSchema values
var trueSchema = BoolSchema(true)
var falseSchema = BoolSchema(false)

// TrueSchema returns a BoolSchema representing true
func TrueSchema() BoolSchema {
return trueSchema
}

// FalseSchema returns a BoolSchema representing false
func FalseSchema() BoolSchema {
return falseSchema
}

// unmarshalSchemaOrBoolSlice parses a JSON array using token-based decoding
func unmarshalSchemaOrBoolSlice(dec *json.Decoder) ([]SchemaOrBool, error) {
// We need to decode the array as raw JSON first, then handle each element
var rawArray []json.RawMessage
if err := dec.Decode(&rawArray); err != nil {
return nil, fmt.Errorf("failed to decode array: %w", err)
}

result := make([]SchemaOrBool, 0, len(rawArray))

for i, rawElement := range rawArray {
// Try to decode as boolean first
var b bool
if err := json.Unmarshal(rawElement, &b); err == nil {
result = append(result, BoolSchema(b))
continue
}

// Try to decode as Schema object
var schema Schema
if err := json.Unmarshal(rawElement, &schema); err == nil {
result = append(result, &schema)
continue
}

return nil, fmt.Errorf("element at index %d is neither boolean nor valid schema object", i)
}

return result, nil
}

// unmarshalSchemaOrBoolMap parses a JSON map using token-based decoding
func unmarshalSchemaOrBoolMap(dec *json.Decoder) (map[string]SchemaOrBool, error) {
// We need to decode the map as raw JSON first, then handle each value
var rawMap map[string]json.RawMessage
if err := dec.Decode(&rawMap); err != nil {
return nil, fmt.Errorf("failed to decode map: %w", err)
}

result := make(map[string]SchemaOrBool)

for key, rawValue := range rawMap {
// Try to decode as boolean first
var b bool
if err := json.Unmarshal(rawValue, &b); err == nil {
result[key] = BoolSchema(b)
continue
}

// Try to decode as Schema object
var schema Schema
if err := json.Unmarshal(rawValue, &schema); err == nil {
result[key] = &schema
continue
}

return nil, fmt.Errorf("value for key %q is neither boolean nor valid schema object", key)
}

return result, nil
}
Loading
Loading