Skip to content

Functions in Expression Language

Overview

Functions in the expression language provide a powerful way to extend the language's capabilities beyond basic comparisons and logical operations. They are prefixed with @ and can perform complex operations, transformations, and validations.

Implementation Details

Functions are implemented through an interface:

  1. Function Interface
go
type Function interface {
    Evaluate(args []interface{}) (interface{}, error)
    ArgCount() int
}

Function Types and Return Values

Functions can return three types of values, each with specific usage patterns:

1. Boolean Functions

  • Return true or false
  • Can be used directly in expressions

Example implementation:

go
type IsAdminFunction struct{}

func (f *IsAdminFunction) Evaluate(args []interface{}) (interface{}, error) {
    return args[0] == "admin", nil
}

func (f *IsAdminFunction) ArgCount() int {
    return 1
}

Usage:

@isAdmin(user.role) or @isAdmin(user.role) == true

2. Numeric Functions

  • Return numeric values (integers or floats)
  • Must be used with comparison operators

Example implementation:

go
type CountFunction struct{}

func (f *CountFunction) Evaluate(args []interface{}) (interface{}, error) {
    if arr, ok := args[0].([]interface{}); ok {
        return float64(len(arr)), nil
    }
    return 0.0, fmt.Errorf("argument must be an array")
}

func (f *CountFunction) ArgCount() int {
    return 1
}

Usage:

@count(items) > 5
@length(array) >= 10

3. String Functions

  • Return string values
  • Must be used with comparison operators

Example implementation:

go
type ToLowerFunction struct{}

func (f *ToLowerFunction) Evaluate(args []interface{}) (interface{}, error) {
    if str, ok := args[0].(string); ok {
        return strings.ToLower(str), nil
    }
    return "", fmt.Errorf("argument must be a string")
}

func (f *ToLowerFunction) ArgCount() int {
    return 1
}

Usage:

@toLowerCase(name) == "admin"
@format(user.id) starts "usr_"

Error Handling

The function system includes comprehensive error handling for various scenarios:

1. Registration Errors

go
// Attempting to register a duplicate function
err := registry.Register("existing", &SomeFunction{})
if err != nil {
    // Error: "function existing already registered"
}

2. Argument Count Errors

js
# Too many arguments
@isAdmin(user.role, "extra")  
// Error: "function isAdmin expects 1 arguments, got 2"

# Too few arguments
@compare()  
// Error: "function compare expects 2 arguments, got 0"

3. Type Context Errors

js
# Using numeric function in boolean context
@count(items)  
// Error: "function count returned non-boolean value when used in boolean context"

# Using string function in boolean context
@toLowerCase(name)  
// Error: "function toLowerCase returned non-boolean value when used in boolean context"

4. Argument Evaluation Errors

js
# Invalid field access
@isAdmin(nonexistent.field)  
// Error: "error evaluating argument 0: field 'nonexistent' not found"

# Type conversion errors
@count("not_an_array")  
// Error: "argument must be an array"

Advanced Usage Patterns

1. Function Chaining

Functions can be used as arguments to other functions:

js
@validate(@transform(data))
@hasRole(@getUserType(user))

2. Complex Conditions

Functions can be combined with other expressions:

js
@isAdmin(user.role) and (@count(user.permissions) > 5)
(@length(items) > 0 and @hasAccess(user)) or @isOverride(context)

3. Array Operations

Functions working with arrays:

js
@contains(users, "admin")
@any(permissions, @hasAccess)
@filter(items, "status", "active")

Best Practices

  1. Type Safety

    • Always use comparison operators with numeric and string functions
    • Use boolean functions directly in logical expressions
    • Handle type conversions explicitly in function implementation
  2. Error Handling

    • Implement thorough argument validation in functions
    • Provide clear error messages for invalid inputs
    • Consider edge cases in function implementation
  3. Performance

    • Keep function implementations efficient
    • Cache results for expensive operations when possible
    • Consider argument evaluation costs
  4. Naming Conventions

    • Use clear, descriptive function names
    • Follow consistent naming patterns
    • Document expected arguments and return types
  5. Testing

    • Test functions with various input types
    • Include edge cases in tests
    • Verify error conditions
    • Test in different contexts (boolean, comparison)

Common Patterns and Examples

1. User Authorization

js
@hasRole(user, "admin") and @isActive(user)
@canAccess(user, resource) or @isAdmin(user)

2. Data Validation

js
@isValid(data) and @matchesSchema(data, "user")
@notEmpty(field) and @format(field) match "^[A-Z].*$"

3. Date/Time Operations

js
@dateAfter(timestamp, "2023-01-01")
@dateBetween(event.date, "2023-01-01", "2023-12-31")

4. String Manipulation

js
@contains(@toLowerCase(text), "error")
@matches(@trim(input), "^[0-9]+$")

5. Numeric Operations

js
@sum(values) > 1000
@average(scores) >= 75.0