exprlang

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 7 Imported by: 0

README

Sift Expr-Lang Adapter

This package provides an expr-lang adapter for the Sift filter library. It translates Sift filter expressions into expr-lang expression syntax, allowing you to use Sift's universal filter language with expr-lang's powerful expression evaluation engine.

Installation

go get github.com/nisimpson/sift/thru/exprlang

The package automatically registers a custom formatter for expr-lang expressions when imported, enabling full serialization and parsing support with Sift's Format() and Parse() functions.

Usage

Basic Example
package main

import (
    "context"
    "fmt"
    
    "github.com/expr-lang/expr"
    "github.com/nisimpson/sift"
    "github.com/nisimpson/sift/thru/exprlang"
)

type User struct {
    Name   string
    Status string
    Age    int
}

func main() {
    // Create a sift filter
    filter := &sift.Condition{
        Name:      "Status",
        Operation: sift.OperationEQ,
        Value:     "active",
    }
    
    // Translate to expr-lang
    adapter := exprlang.NewAdapter()
    sift.Thru(context.Background(), adapter, filter)
    
    // Get the expression string
    exprStr := adapter.Expression()
    fmt.Println("Expression:", exprStr)
    // Output: Expression: Status == "active"
    
    // Compile and run with expr-lang
    program, _ := expr.Compile(exprStr, expr.Env(User{}))
    
    user := User{Name: "Alice", Status: "active", Age: 25}
    output, _ := expr.Run(program, user)
    fmt.Println("Match:", output.(bool))
    // Output: Match: true
}
Complex Filters
// (status = "active" AND age >= 18) OR role = "admin"
filter := &sift.OrOperation{
    Left: &sift.AndOperation{
        Left: &sift.Condition{
            Name:      "Status",
            Operation: sift.OperationEQ,
            Value:     "active",
        },
        Right: &sift.Condition{
            Name:      "Age",
            Operation: sift.OperationGTE,
            Value:     "18",
        },
    },
    Right: &sift.Condition{
        Name:      "Role",
        Operation: sift.OperationEQ,
        Value:     "admin",
    },
}

adapter := exprlang.NewAdapter()
sift.Thru(context.Background(), adapter, filter)

fmt.Println(adapter.Expression())
// Output: ((Status == "active") && (Age >= 18)) || (Role == "admin")

Parsing Expr-Lang Strings

The Parse function converts an expr-lang expression string into a sift.Expression AST — the reverse of the Adapter above. This is useful for accepting filter expressions from query parameters (e.g. ?filter=status == "active" && age > 18) and converting them into sift nodes for any backend adapter.

func Parse(input string, opts ...ParseOption) (sift.Expression, error)
Basic Parsing Example
import "github.com/nisimpson/sift/thru/exprlang"

// Parse an expr-lang string into a sift.Expression
expr, err := exprlang.Parse(`status == "active" && age > 18`)
if err != nil {
    log.Fatal(err)
}

// Use the resulting sift.Expression with any backend adapter
adapter := dynamodb.NewAdapter()
sift.Thru(ctx, adapter, sift.WithFilter(expr))
Supported Constructs
// Comparison operators
exprlang.Parse(`status == "active"`)   // sift.Condition{OperationEQ}
exprlang.Parse(`age != 30`)            // sift.Condition{OperationNEQ}
exprlang.Parse(`price < 100`)          // sift.Condition{OperationLT}
exprlang.Parse(`rating >= 4.5`)        // sift.Condition{OperationGTE}

// Logical operators
exprlang.Parse(`a == 1 && b == 2`)     // sift.AndOperation
exprlang.Parse(`a == 1 || b == 2`)     // sift.OrOperation
exprlang.Parse(`!(status == "deleted")`) // sift.NotOperation

// String operators
exprlang.Parse(`name contains "foo"`)      // sift.Condition{OperationContains}
exprlang.Parse(`name startsWith "bar"`)    // sift.Condition{OperationBeginsWith}

// Membership
exprlang.Parse(`"admin" in roles`)     // sift.Condition{OperationIn}

// Between (detected from range pattern)
exprlang.Parse(`age >= 18 and age <= 65`) // sift.Condition{OperationBetween, Value: "18,65"}

// Nil checks
exprlang.Parse(`field != nil`)         // sift.Condition{OperationExists}
exprlang.Parse(`field == nil`)         // sift.Condition{OperationNotExists}
Strict Mode vs Lenient Mode

By default, the parser operates in strict mode: any expr-lang construct without a sift equivalent returns a descriptive error.

// Strict mode (default) — returns an error for unsupported constructs
_, err := exprlang.Parse(`len(items) > 5`)
// err: "exprlang: unsupported construct: ..."

Use WithLenientMode() to wrap unsupported constructs as RawExpression nodes instead of failing. This is useful when the downstream can handle raw expr-lang (e.g. in-memory evaluation).

// Lenient mode — wraps unsupported constructs as RawExpression
expr, err := exprlang.Parse(`status == "active" && len(items) > 5`, exprlang.WithLenientMode())
// err == nil
// expr is an AndOperation where:
//   Left  = sift.Condition{Name: "status", Operation: OperationEQ, Value: "active"}
//   Right = exprlang.RawExpression wrapping "len(items) > 5"
Future Considerations

A custom function registry is planned as a follow-up. This would allow consumers to register handlers that map specific expr-lang constructs (e.g. endsWith, custom functions) to structured sift nodes at parse time, rather than relying on RawExpression wrapping from lenient mode.

Supported Operations

Comparison Operations
Sift Operation Expr-Lang Syntax Example
OperationEQ == status == "active"
OperationNEQ != status != "deleted"
OperationLT < age < 18
OperationLTE <= age <= 65
OperationGT > price > 100
OperationGTE >= rating >= 4.5
String Operations
Sift Operation Expr-Lang Syntax Example
OperationContains field contains value email contains "@example.com"
OperationBeginsWith field startsWith value name startsWith "John"
Collection Operations
Sift Operation Expr-Lang Syntax Example
OperationIn value in field "admin" in roles
OperationBetween field >= min and field <= max age >= 18 and age <= 65
Existence Operations
Sift Operation Expr-Lang Syntax Example
OperationExists field != nil optional_field != nil
OperationNotExists field == nil deleted_at == nil
Logical Operations
Sift Operation Expr-Lang Syntax Example
AndOperation && (a == 1) && (b == 2)
OrOperation || (role == "admin") || (role == "moderator")
NotOperation ! !(status == "deleted")

Type Handling

The adapter automatically detects and formats values appropriately:

  • Numbers: "42"42, "3.14"3.14
  • Booleans: "true"true, "false"false
  • Strings: "hello""hello" (quoted)

This ensures proper type matching when evaluating expressions with expr-lang.

Custom Expressions

The adapter supports custom expr-lang expressions for advanced use cases that go beyond standard Sift operations. This allows you to leverage the full power of expr-lang's expression language.

Custom expressions are useful for:

  • Complex array filtering and transformations
  • String manipulation beyond basic operations
  • Date/time operations
  • Predicates with nested data structures
  • Any expr-lang feature not directly mapped to Sift operations
Helper Functions

Custom expressions are created using helper functions that wrap expr-lang syntax. When serialized as Sift expressions, they use the format exprlang(...).

RawExpression

Create a custom expression from any valid expr-lang syntax:

filter := exprlang.RawExpression("len(tweets) > 10 and any(tweets, len(.Content) > 240)")

// Sift serialization: exprlang(len(tweets) > 10 and any(tweets, len(.Content) > 240))
// Expr-lang output: len(tweets) > 10 and any(tweets, len(.Content) > 240)
Predicate

Create a predicate expression (commonly used with array functions):

filter := exprlang.Predicate("len(.Content) > 240")
ArrayFunction

Create an array function expression:

filter := exprlang.ArrayFunction("filter", "tweets", "len(.Content) > 240")
// Generates: filter(tweets, len(.Content) > 240)
StringFunction

Create a string function expression:

filter := exprlang.StringFunction("upper", "name")
// Generates: upper(name)

filter := exprlang.StringFunction("split", "email", `","`)
// Generates: split(email, ",")
DateFunction

Create a date function expression:

filter := exprlang.DateFunction("now")
// Generates: now()

filter := exprlang.DateFunction("date", `"2023-08-14"`)
// Generates: date("2023-08-14")
Common Use Cases
Array Filtering

Filter arrays based on complex conditions:

// Find users with more than 5 tweets where any tweet has > 100 characters
filter := exprlang.RawExpression("len(Tweets) > 5 and any(Tweets, len(.Content) > 100)")
Array Transformations
// Check if all comments are short
filter := exprlang.RawExpression("all(Comments, len(.Text) < 200)")

// Count tweets with high engagement
filter := exprlang.RawExpression("count(Tweets, .Likes > 100) > 10")

// Find first tweet with specific content
filter := exprlang.RawExpression("find(Tweets, .Content contains 'golang') != nil")
String Operations
// Case-insensitive comparison
filter := exprlang.RawExpression("lower(Name) == 'john doe'")

// String manipulation
filter := exprlang.RawExpression("len(trim(Bio)) > 50")

// Multiple string checks
filter := exprlang.RawExpression("Email contains '@' and Email endsWith '.com'")
Date/Time Operations
// Recent posts (within last 24 hours)
filter := exprlang.RawExpression("now() - CreatedAt < duration('24h')")

// Posts from specific date range
filter := exprlang.RawExpression("CreatedAt >= date('2023-01-01') and CreatedAt < date('2024-01-01')")

// Weekend posts
filter := exprlang.RawExpression("CreatedAt.Weekday() in [0, 6]")
Nested Data Access
// Access nested fields with optional chaining
filter := exprlang.RawExpression("Author?.Profile?.Verified == true")

// Nil coalescing
filter := exprlang.RawExpression("Author?.Name ?? 'Anonymous'")
Complex Predicates
// Nested array filtering
filter := exprlang.RawExpression(`
    any(Posts, {
        let post = #;
        any(.Comments, .AuthorId == post.AuthorId)
    })
`)
Combining Standard and Custom Expressions

You can mix standard Sift operations with custom expr-lang expressions:

filter := sift.Eq("Status", "published").
    And(sift.In("Tags", "golang").Or(exprlang.RawExpression("len(Comments) > 10")))

This generates:

(Status == "published") && (("golang" in Tags) || (len(Comments) > 10))

When serialized as a Sift expression, this becomes:

and(eq(Status,published),or(in(Tags,golang),exprlang(len(Comments) > 10)))

This demonstrates how custom expressions integrate seamlessly with standard Sift operations while maintaining the ability to serialize and parse the entire filter tree.

Available Expr-Lang Features
Operators
  • Arithmetic: +, -, *, /, %, ^ or **
  • Comparison: ==, !=, <, >, <=, >=
  • Logical: and or &&, or or ||, not or !
  • String: contains, startsWith, endsWith, + (concatenation)
  • Membership: in, ., ?. (optional chaining)
  • Range: .. (e.g., 1..10)
  • Slice: [:] (e.g., array[1:3])
  • Pipe: | (e.g., name | lower() | split(" "))
  • Ternary: ?: (e.g., age >= 18 ? "adult" : "minor")
  • Nil coalescing: ?? (e.g., name ?? "Unknown")
Array Functions
  • all(array, predicate) - All elements satisfy predicate
  • any(array, predicate) - Any element satisfies predicate
  • one(array, predicate) - Exactly one element satisfies predicate
  • none(array, predicate) - No elements satisfy predicate
  • map(array, predicate) - Transform array elements
  • filter(array, predicate) - Filter array elements
  • find(array, predicate) - Find first matching element
  • findIndex(array, predicate) - Find index of first match
  • count(array, predicate) - Count matching elements
  • sum(array) - Sum of numbers
  • mean(array) - Average of numbers
  • first(array) - First element
  • last(array) - Last element
  • sort(array) - Sort array
  • reverse(array) - Reverse array
  • flatten(array) - Flatten nested arrays
  • uniq(array) - Remove duplicates
String Functions
  • upper(str) - Convert to uppercase
  • lower(str) - Convert to lowercase
  • trim(str) - Remove whitespace
  • split(str, delimiter) - Split into array
  • replace(str, old, new) - Replace substring
  • indexOf(str, substring) - Find substring index
  • hasPrefix(str, prefix) - Check prefix
  • hasSuffix(str, suffix) - Check suffix
Date Functions
  • now() - Current date/time
  • date(str) - Parse date string
  • duration(str) - Parse duration (e.g., "1h", "30m")
  • Date methods: .Year(), .Month(), .Day(), .Hour(), .Weekday()
Type Conversion
  • int(v) - Convert to integer
  • float(v) - Convert to float
  • string(v) - Convert to string
  • toJSON(v) - Convert to JSON string
  • fromJSON(v) - Parse JSON string
Miscellaneous
  • len(v) - Length of array, map, or string
  • type(v) - Get type name
  • get(v, index) - Safe index access
Best Practices
  1. Use standard Sift operations when possible - They're more portable across adapters
  2. Combine standard and custom - Use custom expressions only for features not available in Sift
  3. Keep expressions readable - Break complex logic into multiple filters when possible
  4. Test thoroughly - Custom expressions bypass Sift's type safety
  5. Document custom expressions - Add comments explaining complex logic
Serialization

Custom expressions serialize using the exprlang(...) format in Sift's string representation. The expr-lang adapter automatically registers a custom formatter that handles serialization and parsing.

// Standard Sift operation
filter := &sift.Condition{Name: "status", Operation: sift.OperationEQ, Value: "active"}
sift.Format(filter) // "eq(status,active)"

// Custom expression
filter := exprlang.RawExpression("len(tweets) > 10")
sift.Format(filter) // "exprlang(len\\(tweets\\) > 10)"

// Mixed
filter := &sift.AndOperation{
    Left: &sift.Condition{Name: "status", Operation: sift.OperationEQ, Value: "active"},
    Right: exprlang.RawExpression("len(tweets) > 10"),
}
sift.Format(filter) // "and(eq(status,active),exprlang(len\\(tweets\\) > 10))"

The formatter automatically:

  • Escapes special Sift characters (backslashes, commas, parentheses) in expr-lang expressions
  • Parses escaped expressions back to their original form
  • Supports round-trip serialization (Format → Parse → Format produces the same result)
// Round-trip example
original := exprlang.RawExpression("(a + b) * (c + d)")
formatted, _ := sift.Format(original)           // "exprlang(\\(a + b\\) * \\(c + d\\))"
parsed, _ := sift.Parse(formatted)              // Reconstructs the expression
reformatted, _ := sift.Format(parsed)           // Same as formatted

This allows custom expressions to be:

  • Serialized to strings for storage or transmission
  • Combined with standard Sift operations
  • Parsed back from stored strings
  • Logged and debugged easily

Use Cases

API Query Filters
// Parse user-provided filter from query string
filterStr := r.URL.Query().Get("filter")
filter, _ := sift.Parse(filterStr)

// Translate to expr-lang
adapter := exprlang.NewAdapter()
sift.Thru(ctx, adapter, filter)

// Compile once, reuse for all records
program, _ := expr.Compile(adapter.Expression(), expr.Env(Record{}))

// Filter records
var results []Record
for _, record := range allRecords {
    match, _ := expr.Run(program, record)
    if match.(bool) {
        results = append(results, record)
    }
}
In-Memory Filtering
func FilterUsers(users []User, filter sift.Expression) []User {
    adapter := exprlang.NewAdapter()
    sift.Thru(context.Background(), adapter, filter)
    
    program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))
    
    var filtered []User
    for _, user := range users {
        match, _ := expr.Run(program, user)
        if match.(bool) {
            filtered = append(filtered, user)
        }
    }
    return filtered
}
Dynamic Business Rules
// Store filter expressions as business rules
rules := map[string]sift.Expression{
    "premium_users": &sift.AndOperation{
        Left: &sift.Condition{
            Name:      "Subscription",
            Operation: sift.OperationEQ,
            Value:     "premium",
        },
        Right: &sift.Condition{
            Name:      "Active",
            Operation: sift.OperationEQ,
            Value:     "true",
        },
    },
}

// Evaluate rules at runtime
adapter := exprlang.NewAdapter()
sift.Thru(ctx, adapter, rules["premium_users"])
program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))
isPremium, _ := expr.Run(program, currentUser)

Performance

The adapter is designed for performance:

  1. Compile Once: Compile the expr-lang program once and reuse it for multiple evaluations
  2. Zero Allocation: The adapter builds the expression string with minimal allocations
  3. Type Safety: Expr-lang provides compile-time type checking when using expr.Env()
  4. Use type-safe environments: Pass struct types to expr.Env() for compile-time checking
  5. Avoid expensive operations in loops: Be mindful of nested array operations
  6. Use predicates efficiently: Leverage short-circuit evaluation with and/or
// Good: Compile once, run many times
adapter := exprlang.NewAdapter()
sift.Thru(ctx, adapter, filter)
program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))

for _, user := range users {
    match, _ := expr.Run(program, user)
    // ...
}

// Bad: Compiling in the loop
for _, user := range users {
    adapter := exprlang.NewAdapter()
    sift.Thru(ctx, adapter, filter)
    program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))
    match, _ := expr.Run(program, user)
    // ...
}

Limitations

  1. Between Operation: Requires comma-separated values (e.g., "18,65")
  2. In Operation: The expr-lang in operator expects the field to be a collection (array, slice, map)
  3. Custom Expressions: Only exprlang.CustomExpression types are supported; custom expressions from other adapters will return an error

Examples

See example_test.go for comprehensive examples including:

  • Basic filtering
  • Complex logical expressions
  • String operations (contains, startsWith)
  • Collection membership (in operator)
  • Between operations
  • Existence checks
  • Custom expr-lang expressions
  • Mixing standard Sift operations with custom expressions
  • Array predicates and functions
  • Serialization of custom expressions
  • Real-world use cases

License

This package is part of the Sift project and follows the same license.

Documentation

Overview

Package exprlang provides an expr-lang adapter for the sift filter library. It translates sift filter expressions into expr-lang expression syntax.

Example (BasicFilter)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

// User represents a user in the system.
type User struct {
	Name     string
	Email    string
	Age      int
	Role     string
	Status   string
	Verified bool
}

func main() {
	// Create a sift filter: status = "active"
	filter := &sift.Condition{
		Name:      "Status",
		Operation: sift.OperationEQ,
		Value:     "active",
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))

	users := []User{
		{Name: "Alice", Status: "active"},
		{Name: "Bob", Status: "inactive"},
		{Name: "Charlie", Status: "active"},
	}

	for _, user := range users {
		output, _ := expr.Run(program, user)
		if output.(bool) {
			fmt.Println(user.Name)
		}
	}

}
Output:
Alice
Charlie
Example (BetweenOperation)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

// User represents a user in the system.
type User struct {
	Name     string
	Email    string
	Age      int
	Role     string
	Status   string
	Verified bool
}

func main() {
	// Filter: age between 18 and 65
	filter := &sift.Condition{
		Name:      "Age",
		Operation: sift.OperationBetween,
		Value:     "18,65",
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))

	users := []User{
		{Name: "Alice", Age: 25},
		{Name: "Bob", Age: 16},
		{Name: "Charlie", Age: 70},
		{Name: "David", Age: 45},
	}

	fmt.Println("Matching users:")
	for _, user := range users {
		output, _ := expr.Run(program, user)
		if output.(bool) {
			fmt.Printf("  %s (age=%d)\n", user.Name, user.Age)
		}
	}

}
Output:
Expression: Age >= 18 and Age <= 65
Matching users:
  Alice (age=25)
  David (age=45)
Example (ComplexFilter)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

// User represents a user in the system.
type User struct {
	Name     string
	Email    string
	Age      int
	Role     string
	Status   string
	Verified bool
}

func main() {
	// Create a complex filter: (status = "active" AND age >= 18) OR role = "admin"
	filter := &sift.OrOperation{
		Left: &sift.AndOperation{
			Left: &sift.Condition{
				Name:      "Status",
				Operation: sift.OperationEQ,
				Value:     "active",
			},
			Right: &sift.Condition{
				Name:      "Age",
				Operation: sift.OperationGTE,
				Value:     "18",
			},
		},
		Right: &sift.Condition{
			Name:      "Role",
			Operation: sift.OperationEQ,
			Value:     "admin",
		},
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))

	users := []User{
		{Name: "Alice", Status: "active", Age: 25, Role: "user"},
		{Name: "Bob", Status: "inactive", Age: 30, Role: "user"},
		{Name: "Charlie", Status: "active", Age: 16, Role: "user"},
		{Name: "David", Status: "inactive", Age: 40, Role: "admin"},
	}

	fmt.Println("Matching users:")
	for _, user := range users {
		output, _ := expr.Run(program, user)
		if output.(bool) {
			fmt.Printf("  %s (status=%s, age=%d, role=%s)\n",
				user.Name, user.Status, user.Age, user.Role)
		}
	}

}
Output:
Expression: ((Status == "active") && (Age >= 18)) || (Role == "admin")
Matching users:
  Alice (status=active, age=25, role=user)
  David (status=inactive, age=40, role=admin)
Example (CustomExpression)
package main

import (
	"context"
	"fmt"
	"strings"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

func main() {
	type Tweet struct {
		Content string
		Likes   int
	}

	type UserWithTweets struct {
		Name   string
		Tweets []Tweet
	}

	// Use custom expr-lang expression for complex filtering
	// Filter users who have more than 5 tweets with content longer than 100 chars
	filter := exprlang.RawExpression("len(Tweets) > 5 and any(Tweets, len(.Content) > 100)")

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(UserWithTweets{}))

	users := []UserWithTweets{
		{
			Name: "Alice",
			Tweets: []Tweet{
				{Content: "Short", Likes: 10},
				{Content: strings.Repeat("Long tweet content ", 10), Likes: 50},
				{Content: "Another short", Likes: 5},
				{Content: strings.Repeat("More long content ", 10), Likes: 30},
				{Content: "Short again", Likes: 8},
				{Content: strings.Repeat("Yet another long one ", 10), Likes: 40},
			},
		},
		{
			Name: "Bob",
			Tweets: []Tweet{
				{Content: "Short", Likes: 10},
				{Content: "Also short", Likes: 5},
			},
		},
	}

	fmt.Println("Matching users:")
	for _, user := range users {
		output, _ := expr.Run(program, user)
		if output.(bool) {
			fmt.Printf("  %s (tweets=%d)\n", user.Name, len(user.Tweets))
		}
	}

}
Output:
Expression: len(Tweets) > 5 and any(Tweets, len(.Content) > 100)
Matching users:
  Alice (tweets=6)
Example (InOperation)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

func main() {
	// Create a type with a slice field for the 'in' operator
	type Team struct {
		Name  string
		Roles []string
	}

	// Filter: "admin" in Roles
	filter := &sift.Condition{
		Name:      "Roles",
		Operation: sift.OperationIn,
		Value:     "admin",
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(Team{}))

	teams := []Team{
		{Name: "Engineering", Roles: []string{"admin", "developer"}},
		{Name: "Marketing", Roles: []string{"user", "editor"}},
		{Name: "Operations", Roles: []string{"admin", "operator"}},
	}

	fmt.Println("Matching teams:")
	for _, team := range teams {
		output, _ := expr.Run(program, team)
		if output.(bool) {
			fmt.Printf("  %s\n", team.Name)
		}
	}

}
Output:
Expression: "admin" in Roles
Matching teams:
  Engineering
  Operations
Example (MixedStandardAndCustom)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

func main() {
	type Post struct {
		Title    string
		Status   string
		Tags     []string
		Comments []string
	}

	// Combine standard Sift operations with custom expr-lang expressions
	// Filter: status = "published" AND (has "golang" tag OR has more than 10 comments)
	filter := &sift.AndOperation{
		Left: &sift.Condition{
			Name:      "Status",
			Operation: sift.OperationEQ,
			Value:     "published",
		},
		Right: &sift.OrOperation{
			Left: &sift.Condition{
				Name:      "Tags",
				Operation: sift.OperationIn,
				Value:     "golang",
			},
			Right: exprlang.RawExpression("len(Comments) > 10"),
		},
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(Post{}))

	posts := []Post{
		{Title: "Go Tutorial", Status: "published", Tags: []string{"golang", "tutorial"}, Comments: make([]string, 5)},
		{Title: "Python Guide", Status: "published", Tags: []string{"python"}, Comments: make([]string, 15)},
		{Title: "Draft Post", Status: "draft", Tags: []string{"golang"}, Comments: make([]string, 20)},
	}

	fmt.Println("Matching posts:")
	for _, post := range posts {
		output, _ := expr.Run(program, post)
		if output.(bool) {
			fmt.Printf("  %s\n", post.Title)
		}
	}

}
Output:
Expression: (Status == "published") && (("golang" in Tags) || (len(Comments) > 10))
Matching posts:
  Go Tutorial
  Python Guide
Example (Negation)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

// User represents a user in the system.
type User struct {
	Name     string
	Email    string
	Age      int
	Role     string
	Status   string
	Verified bool
}

func main() {
	// Filter: NOT(status = "deleted")
	filter := &sift.NotOperation{
		Child: &sift.Condition{
			Name:      "Status",
			Operation: sift.OperationEQ,
			Value:     "deleted",
		},
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))

	users := []User{
		{Name: "Alice", Status: "active"},
		{Name: "Bob", Status: "deleted"},
		{Name: "Charlie", Status: "inactive"},
	}

	fmt.Println("Matching users:")
	for _, user := range users {
		output, _ := expr.Run(program, user)
		if output.(bool) {
			fmt.Println(" ", user.Name)
		}
	}

}
Output:
Expression: !(Status == "deleted")
Matching users:
  Alice
  Charlie
Example (Serialization)
package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

func main() {
	// Custom expressions serialize as exprlang(...) in Sift format

	// Simple custom expression
	filter1 := exprlang.RawExpression("len(tweets) > 10")
	fmt.Println("Custom expression:")
	fmt.Println("  Sift format:", filter1.String())

	adapter1 := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter1, sift.WithFilter(filter1))
	fmt.Println("  Expr-lang:", adapter1.Expression())

	// Mixed standard and custom
	filter2 := &sift.AndOperation{
		Left: &sift.Condition{
			Name:      "Status",
			Operation: sift.OperationEQ,
			Value:     "active",
		},
		Right: exprlang.RawExpression("len(Comments) > 5"),
	}
	fmt.Println("\nMixed expression:")
	fmt.Println("  Sift format:", filter2.String())

	adapter2 := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter2, sift.WithFilter(filter2))
	fmt.Println("  Expr-lang:", adapter2.Expression())

}
Output:
Custom expression:
  Sift format: exprlang(len(tweets) > 10)
  Expr-lang: len(tweets) > 10

Mixed expression:
  Sift format: and(eq(Status,active),exprlang(len(Comments) > 5))
  Expr-lang: (Status == "active") && (len(Comments) > 5)
Example (StringOperations)
package main

import (
	"context"
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/nisimpson/sift"
	"github.com/nisimpson/sift/thru/exprlang"
)

// User represents a user in the system.
type User struct {
	Name     string
	Email    string
	Age      int
	Role     string
	Status   string
	Verified bool
}

func main() {
	// Filter: email contains "@example.com" AND name starts with "John"
	filter := &sift.AndOperation{
		Left: &sift.Condition{
			Name:      "Email",
			Operation: sift.OperationContains,
			Value:     "@example.com",
		},
		Right: &sift.Condition{
			Name:      "Name",
			Operation: sift.OperationBeginsWith,
			Value:     "John",
		},
	}

	// Translate to expr-lang
	adapter := exprlang.NewAdapter()
	_ = sift.Thru(context.Background(), adapter, sift.WithFilter(filter))

	fmt.Println("Expression:", adapter.Expression())

	// Compile and run with expr-lang
	program, _ := expr.Compile(adapter.Expression(), expr.Env(User{}))

	users := []User{
		{Name: "John Doe", Email: "john@example.com"},
		{Name: "Jane Smith", Email: "jane@example.com"},
		{Name: "Johnny Walker", Email: "johnny@example.com"},
		{Name: "John Adams", Email: "john@other.com"},
	}

	fmt.Println("Matching users:")
	for _, user := range users {
		output, _ := expr.Run(program, user)
		if output.(bool) {
			fmt.Printf("  %s <%s>\n", user.Name, user.Email)
		}
	}

}
Output:
Expression: (Email contains "@example.com") && (Name startsWith "John")
Matching users:
  John Doe <john@example.com>
  Johnny Walker <johnny@example.com>

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ArrayFunction

func ArrayFunction(fn, array, predicate string) sift.Expression

ArrayFunction creates a custom expression for array functions. Example: ArrayFunction("filter", "tweets", "len(.Content) > 240")

func DateFunction

func DateFunction(fn string, args ...string) sift.Expression

DateFunction creates a custom expression for date functions. Example: DateFunction("now")

func NewCustomExpression

func NewCustomExpression(expr string) sift.Expression

NewCustomExpression creates a new custom expr-lang expression. The expression string should be valid expr-lang syntax.

func NewRegistry

func NewRegistry() *sift.Registry

NewRegistry creates a sift registry with expr-lang custom expressions registered. Use this when you need to serialize/deserialize expr-lang-specific expressions.

Example:

registry := exprlang.NewRegistry()
expr := exprlang.Predicate("len(.Content) > 240")
formatted, _ := sift.Format(expr, registry)
// Output: "exprlang(len\(.Content\) > 240)"

func Parse added in v0.2.0

func Parse(input string, opts ...ParseOption) (sift.Expression, error)

Parse parses an expr-lang expression string into a sift.Expression AST. It uses expr.Compile() to parse the string into an expr-lang AST, then walks the AST to produce sift nodes.

By default, the parser operates in strict mode where unsupported expr-lang constructs return descriptive errors. Use WithLenientMode() to wrap unsupported constructs as RawExpression nodes instead.

func Predicate

func Predicate(expr string) sift.Expression

Predicate creates a custom expression using expr-lang predicate syntax. Example: Predicate("len(.Content) > 240")

func RawExpression

func RawExpression(expr string) sift.Expression

RawExpression creates a custom expression from raw expr-lang syntax. Use this for any expr-lang expression that doesn't fit other helpers.

func StringFunction

func StringFunction(fn string, args ...string) sift.Expression

StringFunction creates a custom expression for string functions. Example: StringFunction("upper", "name")

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter translates sift filter expressions into expr-lang expression syntax. It accumulates the expression string during traversal.

func NewAdapter

func NewAdapter() *Adapter

NewAdapter creates a new expr-lang adapter.

func (*Adapter) EvaluateAnd

func (a *Adapter) EvaluateAnd(ctx context.Context, node *sift.AndOperation) error

EvaluateAnd combines two expressions with logical AND.

func (*Adapter) EvaluateCondition

func (a *Adapter) EvaluateCondition(ctx context.Context, node *sift.Condition) error

EvaluateCondition translates a sift condition into an expr-lang expression.

func (*Adapter) EvaluateCustom

func (a *Adapter) EvaluateCustom(ctx context.Context, node sift.CustomExpression) error

EvaluateCustom handles custom expr-lang expressions.

func (*Adapter) EvaluateNot

func (a *Adapter) EvaluateNot(ctx context.Context, node *sift.NotOperation) error

EvaluateNot negates an expression.

func (*Adapter) EvaluateOr

func (a *Adapter) EvaluateOr(ctx context.Context, node *sift.OrOperation) error

EvaluateOr combines two expressions with logical OR.

func (*Adapter) Evaluator

func (a *Adapter) Evaluator(ctx context.Context) *sift.Evaluator

Evaluator returns a sift evaluator configured for expr-lang.

func (*Adapter) Expression

func (a *Adapter) Expression() string

Expression returns the accumulated expr-lang expression string.

type CustomExpression

type CustomExpression struct {
	// contains filtered or unexported fields
}

CustomExpression represents an expr-lang specific expression that doesn't map directly to standard Sift operations.

func (*CustomExpression) Expression

func (c *CustomExpression) Expression() string

Expression returns the raw expr-lang expression string.

func (*CustomExpression) String

func (c *CustomExpression) String() string

String returns the string representation of the custom expression.

func (*CustomExpression) Type

func (c *CustomExpression) Type() string

Type returns the type identifier for this custom expression.

type Formatter

type Formatter struct{}

Formatter implements sift.CustomFormatter for expr-lang custom expressions.

func (Formatter) FormatCustomExpression

func (f Formatter) FormatCustomExpression(expression sift.CustomExpression) (string, error)

FormatCustomExpression serializes an expr-lang custom expression to a string.

func (Formatter) ParseCustomExpression

func (f Formatter) ParseCustomExpression(p *sift.Parser) (sift.CustomExpression, error)

ParseCustomExpression deserializes a string to an expr-lang custom expression.

type ParseOption added in v0.2.0

type ParseOption func(*parserConfig)

ParseOption configures the parser behavior.

func WithLenientMode added in v0.2.0

func WithLenientMode() ParseOption

WithLenientMode enables lenient parsing where unsupported expr-lang constructs are wrapped as exprlang.RawExpression custom sift nodes instead of returning errors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL