- Core library with SVG and PNG generation - CLI tool with generate and batch commands - Cross-platform path handling for Windows compatibility - Comprehensive test suite with integration tests
104 lines
2.7 KiB
Go
104 lines
2.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/ungluedlabs/go-jdenticon/internal/util"
|
|
)
|
|
|
|
// Generate creates an identicon with the specified hash and size
|
|
// This method includes caching and singleflight to prevent duplicate work
|
|
func (g *Generator) Generate(ctx context.Context, hash string, size float64) (*Icon, error) {
|
|
// Basic validation
|
|
if hash == "" {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: hash cannot be empty")
|
|
}
|
|
if size <= 0 {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid size: %f", size)
|
|
}
|
|
|
|
// Check icon size limits
|
|
if g.maxIconSize > 0 && int(size) > g.maxIconSize {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: icon size %d exceeds maximum allowed size %d", int(size), g.maxIconSize)
|
|
}
|
|
|
|
// Validate hash format
|
|
if !util.IsValidHash(hash) {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid hash format: %s", hash)
|
|
}
|
|
|
|
// Check for context cancellation before proceeding
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate cache key
|
|
key := g.cacheKey(hash, size)
|
|
|
|
// Check cache first (with read lock)
|
|
g.mu.RLock()
|
|
if cached, ok := g.cache.Get(key); ok {
|
|
g.mu.RUnlock()
|
|
g.metrics.recordHit()
|
|
return cached, nil
|
|
}
|
|
g.mu.RUnlock()
|
|
|
|
// Use singleflight to prevent multiple concurrent generations for the same key
|
|
result, err, _ := g.sf.Do(key, func() (interface{}, error) {
|
|
// Check cache again inside singleflight (another goroutine might have populated it)
|
|
g.mu.RLock()
|
|
if cached, ok := g.cache.Get(key); ok {
|
|
g.mu.RUnlock()
|
|
g.metrics.recordHit()
|
|
return cached, nil
|
|
}
|
|
g.mu.RUnlock()
|
|
|
|
// Generate the icon
|
|
icon, err := g.generateIcon(ctx, hash, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store in cache (with write lock)
|
|
g.mu.Lock()
|
|
g.cache.Add(key, icon)
|
|
g.mu.Unlock()
|
|
|
|
g.metrics.recordMiss()
|
|
return icon, nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result.(*Icon), nil
|
|
}
|
|
|
|
// GenerateWithoutCache creates an identicon without using cache
|
|
// This method is useful for testing or when caching is not desired
|
|
func (g *Generator) GenerateWithoutCache(ctx context.Context, hash string, size float64) (*Icon, error) {
|
|
// Basic validation
|
|
if hash == "" {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: hash cannot be empty")
|
|
}
|
|
if size <= 0 {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid size: %f", size)
|
|
}
|
|
|
|
// Validate hash format
|
|
if !util.IsValidHash(hash) {
|
|
return nil, fmt.Errorf("jdenticon: engine: generation failed: invalid hash format: %s", hash)
|
|
}
|
|
|
|
// Check for context cancellation
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return g.generateIcon(ctx, hash, size)
|
|
}
|