README
¶
Complex UniqueItems Validation
This directory contains comprehensive tests and examples for complex uniqueItems validation support in ogen.
Overview
Previously, ogen only supported uniqueItems: true on arrays of primitive types (strings, numbers, etc.). This implementation adds support for complex objects (structs) by generating Equal() and Hash() methods and using hash-based duplicate detection.
Motivation
GitHub issue #1563 identified that 11 operations in the JIRA API v3 spec were blocked because they use uniqueItems on arrays of complex objects. This implementation removes that limitation.
How It Works
1. Equal() Method Generation
For each type used in a uniqueItems array, ogen generates an Equal() method:
func (a WorkflowStatus) Equal(b WorkflowStatus, depth int) bool {
if depth > 10 {
panic(&validate.DepthLimitError{
MaxDepth: 10,
TypeName: "WorkflowStatus",
})
}
// Compare all fields...
if a.ID != b.ID {
return false
}
// ... including nested objects with depth+1
if a.Properties.Set {
if !a.Properties.Value.Equal(b.Properties.Value, depth+1) {
return false
}
}
return true
}
Features:
- Depth tracking prevents infinite recursion on circular references
- Handles all field types: primitives, optionals, arrays, maps, nested objects
- Panics with
DepthLimitErrorif depth exceeds limit (default: 10)
2. Hash() Method Generation
For performance, each type also gets a Hash() method using FNV-1a:
func (a WorkflowStatus) Hash() uint64 {
h := fnv.New64a()
h.Write([]byte(fmt.Sprintf("%v", a.ID)))
h.Write([]byte(fmt.Sprintf("%v", a.Name)))
if a.Properties.Set {
h.Write([]byte{1})
nestedHash := a.Properties.Value.Hash()
binary.Write(h, binary.LittleEndian, nestedHash)
} else {
h.Write([]byte{0})
}
return h.Sum64()
}
Properties:
- Equal objects must produce equal hashes
- Fast hash computation for O(n) duplicate detection
- Handles all field types consistently
3. Runtime Validation
ogen generates validateUnique[TypeName]() functions that use hash buckets:
func validateUniqueWorkflowStatus(items []WorkflowStatus) (err error) {
if len(items) <= 1 {
return nil
}
// Recover from depth limit panics
defer func() {
if r := recover(); r != nil {
if e, ok := r.(*validate.DepthLimitError); ok {
err = e
} else {
panic(r)
}
}
}()
// Hash bucket structure
type entry struct {
item WorkflowStatus
index int
}
buckets := make(map[uint64][]entry, len(items))
// O(n) duplicate detection
for i, item := range items {
hash := item.Hash()
bucket := buckets[hash]
// Check for duplicates in this hash bucket
for _, existing := range bucket {
if item.Equal(existing.item, 0) {
return &validate.DuplicateItemsError{
Indices: []int{existing.index, i},
}
}
}
buckets[hash] = append(bucket, entry{item: item, index: i})
}
return nil
}
Algorithm:
- O(n) time complexity using hash buckets
- Hash collisions resolved by calling
Equal() - Returns indices of duplicate items for debugging
- Catches and returns depth limit errors
Examples
Simple Schema
See workflow-status.yaml - demonstrates basic nested objects:
WorkflowStatus:
type: object
required: [id, name]
properties:
id: {type: string}
name: {type: string}
description: {type: string}
properties:
$ref: '#/components/schemas/StatusProperties'
Deep Nesting
See workflow-deep.yaml - tests 7 levels of nesting (JIRA's deepest pattern):
WorkflowTransition → ConditionGroup → Condition → RuleConfiguration
→ ParameterGroup → Parameter → ParameterValue
All Field Types
See all-field-types.yaml - comprehensive test with 18 field types:
- Required primitives (string, number, integer, boolean)
- Optional primitives
- Nullable fields
- Enums (required and optional)
- Arrays (primitives and nested objects)
- Maps (additionalProperties)
- Nested objects (multiple levels)
Real-World JIRA API
See jira-subset.yaml - minimal JIRA API v3 subset:
updateWorkflowTransitionRules: 3 uniqueItems arraysupdateWorkflowMapping: uniqueItems workflow mappings
Test Coverage
38 tests passing across 5 test suites:
Suite 1: Basic Tests (16 tests)
- Empty arrays
- Single elements
- Duplicate detection
- All unique items
- Nested object equality
- Hash collision handling
- Field type unit tests (T046-T052)
Suite 2: Depth Limit Tests (6 tests)
- Direct panic at depth 11
- Fully nested object panic
- Hash consistency for deep objects
- DepthLimitError recovery
- Within-limit validation
- Duplicate detection within limit
Suite 3: Integration Tests (8 tests)
- All field types combined
- Duplicate detection with complex objects
- Optional/Nested/Array/Map differences
- Hash/Equal consistency
- Minimal items
Suite 4: Golden File Tests (4 tests)
- Equal() generation stability
- Hash() generation stability
- Regression prevention
Suite 5: JIRA Subset Tests (4 tests)
- Real-world JIRA patterns
- Multiple uniqueItems arrays
- Complete request validation
Performance
All validation runs in O(n) time:
| Items | Time | Benchmark |
|---|---|---|
| 50 JIRA rules | ~7μs | BenchmarkJIRAWorkflowRules_50Rules |
| 100 complex items | ~0.14ms | BenchmarkValidateUniqueComprehensiveItem_100Items |
| 1,000 simple items | ~0.24ms | BenchmarkValidateUniqueWorkflowStatus_1000Items |
40x faster than the 10ms target for 1,000 items.
Error Types
DuplicateItemsError
Returned when duplicates are found:
type DuplicateItemsError struct {
Indices []int
}
// Error: "duplicate item found at indices 0 and 3"
DepthLimitError
Returned when nesting exceeds the depth limit:
type DepthLimitError struct {
MaxDepth int
TypeName string
}
// Error: "equality check depth limit exceeded for type WorkflowStatus (max: 10)"
Implementation Files
Core Generation
gen/ir/equality.go- IR types for equality specsgen/gen_equality_detect.go- Type collection and traversalgen/gen_equality.go- Equal() and Hash() generationgen/gen_validators_unique.go- validateUnique() generationgen/_template/validators.tmpl- Integration into validation
Validation
validate/errors.go- Error types (DuplicateItemsError, DepthLimitError)
Tests
generated/- Basic tests with 2-level nestinggenerated-depth-limit/- 12-level nesting testsgenerated-integration/- All field types combinedgenerated-golden/- Regression testsgenerated-jira/- Real-world JIRA patterns
Limitations
- Depth Limit: Default maximum depth is 10 to prevent stack overflow
- Hash Collisions: Possible but rare; resolved by Equal() verification
- Performance: O(n) for typical cases, worst-case O(n²) if all items hash to same value (extremely unlikely with FNV-1a)
Future Improvements
- Configurable depth limits via schema extensions
- Alternative hash algorithms for specific use cases
- Optimization for arrays with many identical nested structures
Directories
¶
| Path | Synopsis |
|---|---|
|
Code generated by ogen, DO NOT EDIT.
|
Code generated by ogen, DO NOT EDIT. |