Appearance
Implementing Custom Functions
This guide explains how to implement custom functions that can be used within the query language.
Function Interface
Every custom function must implement the Function
interface:
go
type Function interface {
// Evaluate executes the function with given arguments
Evaluate(args []interface{}) (interface{}, error)
// ArgCount returns the required number of arguments
// Return -1 for variadic functions
ArgCount() int
}
Basic Function Types
1. Boolean Function
go
// Function that returns true/false
type IsAdminFunction struct{}
func (f *IsAdminFunction) Evaluate(args []interface{}) (interface{}, error) {
if len(args) != 1 {
return nil, fmt.Errorf("isAdmin expects 1 argument")
}
user, ok := args[0].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("argument must be a user object")
}
role, ok := user["role"].(string)
if !ok {
return false, nil
}
return role == "admin", nil
}
func (f *IsAdminFunction) ArgCount() int {
return 1
}
// Usage in query: @isAdmin(user)
2. Numeric Function
go
// Function that returns a number
type CountFunction struct{}
func (f *CountFunction) Evaluate(args []interface{}) (interface{}, error) {
if len(args) != 1 {
return nil, fmt.Errorf("count expects 1 argument")
}
arr, ok := args[0].([]interface{})
if !ok {
return nil, fmt.Errorf("argument must be an array")
}
return float64(len(arr)), nil
}
func (f *CountFunction) ArgCount() int {
return 1
}
// Usage in query: @count(items) > 0
3. String Function
go
// Function that returns a string
type FormatFunction struct{}
func (f *FormatFunction) Evaluate(args []interface{}) (interface{}, error) {
if len(args) != 2 {
return nil, fmt.Errorf("format expects 2 arguments")
}
template, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("first argument must be a string")
}
value := args[1]
return fmt.Sprintf(template, value), nil
}
func (f *FormatFunction) ArgCount() int {
return 2
}
// Usage in query: @format("%s_suffix", name) == "john_suffix"
Registering Custom Functions
Functions must be registered with a FunctionRegistry
before they can be used:
go
// Create a new registry
registry := expressions.NewFunctionRegistry()
// Register custom functions
registry.Register("isAdmin", &IsAdminFunction{})
registry.Register("count", &CountFunction{})
registry.Register("format", &FormatFunction{})
Function Return Types and Usage
Boolean Functions
- Return
true
orfalse
- Can be used directly in boolean context
- Example:
@isAdmin(user)
- Return
Numeric Functions
- Return numbers (use
float64
for consistency) - Must be used with comparison operators
- Example:
@count(items) > 0
- Return numbers (use
String Functions
- Return strings
- Must be used with comparison or string operators
- Example:
@format(name) == "value"
Error Handling Best Practices
- Argument Count Validation
go
func (f *MyFunction) Evaluate(args []interface{}) (interface{}, error) {
if len(args) != f.ArgCount() {
return nil, fmt.Errorf("function expects %d arguments, got %d",
f.ArgCount(), len(args))
}
// ... rest of implementation
}
- Type Assertions
go
func (f *MyFunction) Evaluate(args []interface{}) (interface{}, error) {
value, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("argument must be a string")
}
// ... rest of implementation
}
- Nil Handling
go
func (f *MyFunction) Evaluate(args []interface{}) (interface{}, error) {
if args[0] == nil {
return nil, fmt.Errorf("argument cannot be nil")
}
// ... rest of implementation
}
Complete Example
Here's a complete example of a custom function that checks if a number is within a range:
go
// RangeCheckFunction checks if a number is within a given range
type RangeCheckFunction struct{}
func (f *RangeCheckFunction) Evaluate(args []interface{}) (interface{}, error) {
// Validate argument count
if len(args) != 3 {
return nil, fmt.Errorf("rangeCheck expects 3 arguments (value, min, max)")
}
// Extract and validate value
value, ok := args[0].(float64)
if !ok {
if intVal, ok := args[0].(int); ok {
value = float64(intVal)
} else {
return nil, fmt.Errorf("first argument must be a number")
}
}
// Extract and validate min
min, ok := args[1].(float64)
if !ok {
if intMin, ok := args[1].(int); ok {
min = float64(intMin)
} else {
return nil, fmt.Errorf("second argument must be a number")
}
}
// Extract and validate max
max, ok := args[2].(float64)
if !ok {
if intMax, ok := args[2].(int); ok {
max = float64(intMax)
} else {
return nil, fmt.Errorf("third argument must be a number")
}
}
// Perform range check
return value >= min && value <= max, nil
}
func (f *RangeCheckFunction) ArgCount() int {
return 3
}
// Usage:
// registry.Register("rangeCheck", &RangeCheckFunction{})
// Query: @rangeCheck(value, 0, 100)
Testing Custom Functions
Always test your custom functions:
go
func TestRangeCheckFunction(t *testing.T) {
fn := &RangeCheckFunction{}
tests := []struct {
name string
args []interface{}
want interface{}
wantErr bool
errMsg string
}{
{
name: "valid range check",
args: []interface{}{50.0, 0.0, 100.0},
want: true,
wantErr: false,
},
{
name: "below range",
args: []interface{}{-1.0, 0.0, 100.0},
want: false,
wantErr: false,
},
{
name: "above range",
args: []interface{}{101.0, 0.0, 100.0},
want: false,
wantErr: false,
},
{
name: "invalid arg count",
args: []interface{}{50.0, 0.0},
want: nil,
wantErr: true,
errMsg: "rangeCheck expects 3 arguments (value, min, max)",
},
{
name: "invalid value type",
args: []interface{}{"50", 0.0, 100.0},
want: nil,
wantErr: true,
errMsg: "first argument must be a number",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := fn.Evaluate(tt.args)
if tt.wantErr {
if err == nil {
t.Errorf("expected error but got none")
return
}
if err.Error() != tt.errMsg {
t.Errorf("got error = %v, want error = %v",
err.Error(), tt.errMsg)
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if got != tt.want {
t.Errorf("got = %v, want %v", got, tt.want)
}
})
}
}
Best Practices
Type Safety
- Use type assertions with fallbacks where appropriate
- Convert numeric types to float64 for consistency
- Provide clear error messages for type mismatches
Error Handling
- Validate argument count before processing
- Check for nil values
- Return descriptive error messages
- Use fmt.Errorf for error formatting
Documentation
- Document expected argument types
- Provide usage examples
- Explain return value types
- List possible error conditions
Testing
- Test happy path cases
- Test edge cases
- Test error conditions
- Test type conversions
- Test with nil values