or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/io.cucumber/cucumber-expressions@19.0.x

docs

index.md
tile.json

tessl/maven-io-cucumber--cucumber-expressions

tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0

Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps

common-patterns.mddocs/guides/

Common Patterns

This guide covers frequently used patterns when working with Cucumber Expressions.

Pattern 1: Step Definition Matching

Match step definitions in BDD test frameworks.

import io.cucumber.cucumberexpressions.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class StepDefinitions {
    private final ExpressionFactory factory;
    private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
    
    public StepDefinitions() {
        ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
        
        // Register custom types
        registry.defineParameterType(new ParameterType<>(
            "user", "[A-Za-z][A-Za-z0-9_]*", String.class, s -> s
        ));
        
        this.factory = new ExpressionFactory(registry);
    }
    
    public Optional<List<Argument<?>>> matchStep(String pattern, String text) {
        Expression expr = expressionCache.computeIfAbsent(
            pattern,
            factory::createExpression
        );
        return expr.match(text);
    }
    
    public Object[] extractArguments(String pattern, String text) {
        Optional<List<Argument<?>>> match = matchStep(pattern, text);
        if (match.isEmpty()) {
            return null;
        }
        
        return match.get().stream()
            .map(Argument::getValue)
            .toArray();
    }
}

// Usage
StepDefinitions steps = new StepDefinitions();
Object[] args = steps.extractArguments(
    "User {user} logs in with {string}",
    "User john logs in with \"password123\""
);
// args = ["john", "password123"]

Pattern 2: Custom Type Registration

Register domain-specific types for your application.

public class DomainTypes {
    public static void registerTypes(ParameterTypeRegistry registry) {
        // Email type
        registry.defineParameterType(new ParameterType<>(
            "email",
            "[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}",
            String.class,
            s -> s
        ));
        
        // URL type
        registry.defineParameterType(new ParameterType<>(
            "url",
            "https?://[\\w.-]+(?:/[\\w.-]*)*",
            String.class,
            s -> s
        ));
        
        // Date type
        registry.defineParameterType(new ParameterType<>(
            "date",
            "\\d{4}-\\d{2}-\\d{2}",
            java.time.LocalDate.class,
            java.time.LocalDate::parse
        ));
        
        // Enum types
        registry.defineParameterType(ParameterType.fromEnum(Status.class));
        registry.defineParameterType(ParameterType.fromEnum(Priority.class));
    }
}

// Usage
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
DomainTypes.registerTypes(registry);
ExpressionFactory factory = new ExpressionFactory(registry);

Expression expr = factory.createExpression("Send email to {email} on {date}");
Optional<List<Argument<?>>> match = expr.match("Send email to user@example.com on 2026-01-30");

Pattern 3: Error Handling with Recovery

Handle errors gracefully with automatic recovery.

public class SafeExpressionMatcher {
    private final ExpressionFactory factory;
    
    public SafeExpressionMatcher(ExpressionFactory factory) {
        this.factory = factory;
    }
    
    public MatchResult match(String pattern, String text) {
        try {
            Expression expr = factory.createExpression(pattern);
            Optional<List<Argument<?>>> match = expr.match(text);
            
            if (match.isPresent()) {
                return MatchResult.success(match.get());
            } else {
                return MatchResult.noMatch();
            }
        } catch (UndefinedParameterTypeException e) {
            return MatchResult.error(
                "Undefined type: " + e.getUndefinedParameterTypeName()
            );
        } catch (AmbiguousParameterTypeException e) {
            return MatchResult.error(
                "Ambiguous types: " + e.getParameterTypes()
            );
        } catch (CucumberExpressionException e) {
            return MatchResult.error("Expression error: " + e.getMessage());
        }
    }
    
    public static class MatchResult {
        private final List<Argument<?>> arguments;
        private final String error;
        private final Status status;
        
        enum Status { SUCCESS, NO_MATCH, ERROR }
        
        private MatchResult(List<Argument<?>> arguments, String error, Status status) {
            this.arguments = arguments;
            this.error = error;
            this.status = status;
        }
        
        public static MatchResult success(List<Argument<?>> args) {
            return new MatchResult(args, null, Status.SUCCESS);
        }
        
        public static MatchResult noMatch() {
            return new MatchResult(null, null, Status.NO_MATCH);
        }
        
        public static MatchResult error(String error) {
            return new MatchResult(null, error, Status.ERROR);
        }
        
        public boolean isSuccess() { return status == Status.SUCCESS; }
        public boolean isNoMatch() { return status == Status.NO_MATCH; }
        public boolean isError() { return status == Status.ERROR; }
        public List<Argument<?>> getArguments() { return arguments; }
        public String getError() { return error; }
    }
}

// Usage
SafeExpressionMatcher matcher = new SafeExpressionMatcher(factory);
SafeExpressionMatcher.MatchResult result = matcher.match(
    "I have {int} cucumbers",
    "I have 42 cucumbers"
);

if (result.isSuccess()) {
    Integer count = (Integer) result.getArguments().get(0).getValue();
    System.out.println("Count: " + count);
} else if (result.isError()) {
    System.err.println("Error: " + result.getError());
}

Pattern 4: Expression Caching

Cache expressions for better performance.

public class CachedExpressionFactory {
    private final ExpressionFactory factory;
    private final Map<String, Expression> cache = new ConcurrentHashMap<>();
    private final Map<String, Exception> errorCache = new ConcurrentHashMap<>();
    
    public CachedExpressionFactory(ParameterTypeRegistry registry) {
        this.factory = new ExpressionFactory(registry);
    }
    
    public Expression getOrCreateExpression(String expressionString) {
        // Check error cache first
        Exception cachedError = errorCache.get(expressionString);
        if (cachedError != null) {
            throw new RuntimeException("Previously failed to create expression", cachedError);
        }
        
        // Get or create expression
        return cache.computeIfAbsent(expressionString, key -> {
            try {
                return factory.createExpression(key);
            } catch (Exception e) {
                errorCache.put(key, e);
                throw e;
            }
        });
    }
    
    public void clearCache() {
        cache.clear();
        errorCache.clear();
    }
    
    public int getCacheSize() {
        return cache.size();
    }
    
    public Map<String, Expression> getCachedExpressions() {
        return Collections.unmodifiableMap(cache);
    }
}

// Usage
CachedExpressionFactory cache = new CachedExpressionFactory(registry);

// First call: creates and caches expression
Expression expr1 = cache.getOrCreateExpression("I have {int} cucumbers");

// Second call: returns cached expression (fast!)
Expression expr2 = cache.getOrCreateExpression("I have {int} cucumbers");

assert expr1 == expr2; // Same instance

Pattern 5: Type-Safe Argument Extraction

Extract arguments with type safety and validation.

public class ArgumentExtractor {
    public static <T> T getValue(Optional<List<Argument<?>>> match, 
                                 int index, 
                                 Class<T> expectedType) {
        if (match.isEmpty()) {
            throw new IllegalArgumentException("No match found");
        }
        
        List<Argument<?>> args = match.get();
        if (index >= args.size()) {
            throw new IndexOutOfBoundsException(
                "Argument index " + index + " out of range (size: " + args.size() + ")"
            );
        }
        
        Object value = args.get(index).getValue();
        if (value == null) {
            return null;
        }
        
        if (!expectedType.isInstance(value)) {
            throw new ClassCastException(
                "Expected " + expectedType.getName() + 
                " but got " + value.getClass().getName()
            );
        }
        
        return expectedType.cast(value);
    }
    
    public static <T> List<T> getValues(Optional<List<Argument<?>>> match, 
                                        Class<T> expectedType) {
        if (match.isEmpty()) {
            return Collections.emptyList();
        }
        
        return match.get().stream()
            .map(arg -> {
                Object value = arg.getValue();
                if (value == null) {
                    return null;
                }
                if (!expectedType.isInstance(value)) {
                    throw new ClassCastException(
                        "Expected " + expectedType.getName() + 
                        " but got " + value.getClass().getName()
                    );
                }
                return expectedType.cast(value);
            })
            .collect(Collectors.toList());
    }
}

// Usage
Expression expr = factory.createExpression("User {string} has {int} items");
Optional<List<Argument<?>>> match = expr.match("User \"John\" has 5 items");

String username = ArgumentExtractor.getValue(match, 0, String.class);
Integer itemCount = ArgumentExtractor.getValue(match, 1, Integer.class);

Pattern 6: Validation with Transformers

Validate data during transformation.

public class ValidatedTypes {
    public static void registerTypes(ParameterTypeRegistry registry) {
        // Age with validation
        registry.defineParameterType(new ParameterType<>(
            "age",
            "\\d+",
            Integer.class,
            (String s) -> {
                int value = Integer.parseInt(s);
                if (value < 0 || value > 150) {
                    throw new IllegalArgumentException(
                        "Age must be between 0 and 150, got: " + value
                    );
                }
                return value;
            }
        ));
        
        // Email with validation
        registry.defineParameterType(new ParameterType<>(
            "email",
            "[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}",
            String.class,
            (String s) -> {
                if (!s.contains("@")) {
                    throw new IllegalArgumentException("Invalid email: " + s);
                }
                return s;
            }
        ));
        
        // Positive number
        registry.defineParameterType(new ParameterType<>(
            "positive",
            "\\d+",
            Integer.class,
            (String s) -> {
                int value = Integer.parseInt(s);
                if (value <= 0) {
                    throw new IllegalArgumentException(
                        "Must be positive, got: " + value
                    );
                }
                return value;
            }
        ));
    }
}

// Usage
ValidatedTypes.registerTypes(registry);
Expression expr = factory.createExpression("Person is {age} years old");

// Valid age
Optional<List<Argument<?>>> match1 = expr.match("Person is 25 years old");
Integer age1 = (Integer) match1.get().get(0).getValue(); // 25

// Invalid age - throws during getValue()
Optional<List<Argument<?>>> match2 = expr.match("Person is 200 years old");
try {
    Integer age2 = (Integer) match2.get().get(0).getValue();
} catch (IllegalArgumentException e) {
    System.err.println("Validation error: " + e.getMessage());
}

Pattern 7: Multi-Capture Group Parameters

Handle parameters with multiple capture groups.

// Define a Point type with x,y coordinates
class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

// Register parameter type with multiple captures
ParameterType<Point> pointType = new ParameterType<>(
    "point",
    "(\\d+),(\\d+)",  // Two capture groups
    Point.class,
    (String[] args) -> new Point(
        Integer.parseInt(args[0]),
        Integer.parseInt(args[1])
    )
);
registry.defineParameterType(pointType);

// Use in expression
Expression expr = factory.createExpression("Move cursor to {point}");
Optional<List<Argument<?>>> match = expr.match("Move cursor to 10,20");

if (match.isPresent()) {
    Point point = (Point) match.get().get(0).getValue();
    System.out.println("X: " + point.x + ", Y: " + point.y);
}

Pattern 8: Default Transformer for Anonymous Parameters

Handle anonymous {} parameters with custom logic.

// Set default transformer
registry.setDefaultParameterTransformer((fromValue, toValueType) -> {
    if (fromValue == null) return null;
    
    // Handle UUID
    if (toValueType == java.util.UUID.class) {
        return java.util.UUID.fromString(fromValue);
    }
    
    // Handle LocalDate
    if (toValueType == java.time.LocalDate.class) {
        return java.time.LocalDate.parse(fromValue);
    }
    
    // Handle custom types
    if (toValueType == CustomType.class) {
        return CustomType.parse(fromValue);
    }
    
    // Fallback
    return fromValue;
});

// Use anonymous parameters with type hints
Expression expr = factory.createExpression("ID is {}");
Optional<List<Argument<?>>> match = expr.match(
    "ID is 550e8400-e29b-41d4-a716-446655440000",
    java.util.UUID.class  // Type hint
);

java.util.UUID uuid = (java.util.UUID) match.get().get(0).getValue();

Pattern 9: Expression Generation for Snippets

Generate expressions from example text.

public class SnippetGenerator {
    private final CucumberExpressionGenerator generator;
    
    public SnippetGenerator(ParameterTypeRegistry registry) {
        this.generator = new CucumberExpressionGenerator(registry);
    }
    
    public String generateJavaSnippet(String stepText, String annotation) {
        List<GeneratedExpression> expressions = generator.generateExpressions(stepText);
        if (expressions.isEmpty()) {
            return "// Could not generate expression for: " + stepText;
        }
        
        GeneratedExpression expr = expressions.get(0);  // Most specific
        
        StringBuilder snippet = new StringBuilder();
        snippet.append("@").append(annotation).append("(\"")
               .append(expr.getSource()).append("\")\n");
        snippet.append("public void step(");
        
        List<String> paramNames = expr.getParameterNames();
        List<ParameterType<?>> paramTypes = expr.getParameterTypes();
        
        for (int i = 0; i < paramNames.size(); i++) {
            if (i > 0) snippet.append(", ");
            snippet.append(paramTypes.get(i).getType().getTypeName())
                   .append(" ").append(paramNames.get(i));
        }
        
        snippet.append(") {\n");
        snippet.append("    // TODO: implement\n");
        snippet.append("}\n");
        
        return snippet.toString();
    }
}

// Usage
SnippetGenerator generator = new SnippetGenerator(registry);
String snippet = generator.generateJavaSnippet(
    "I have 42 cucumbers in my belly",
    "Given"
);
System.out.println(snippet);

// Output:
// @Given("I have {int} cucumbers in my belly")
// public void step(java.lang.Integer int1) {
//     // TODO: implement
// }

Pattern 10: Conditional Type Registration

Register types only if not already present.

public class ConditionalTypeRegistry {
    private final ParameterTypeRegistry registry;
    private final Set<String> registeredTypes = new HashSet<>();
    
    public ConditionalTypeRegistry(ParameterTypeRegistry registry) {
        this.registry = registry;
    }
    
    public void registerIfNotExists(ParameterType<?> parameterType) {
        String name = parameterType.getName();
        if (registeredTypes.contains(name)) {
            return; // Already registered
        }
        
        try {
            registry.defineParameterType(parameterType);
            registeredTypes.add(name);
        } catch (DuplicateTypeNameException e) {
            // Already registered by another source
            registeredTypes.add(name);
        }
    }
    
    public boolean isRegistered(String typeName) {
        return registeredTypes.contains(typeName);
    }
}

// Usage
ConditionalTypeRegistry conditionalRegistry = new ConditionalTypeRegistry(registry);

// Register types conditionally
conditionalRegistry.registerIfNotExists(colorType);
conditionalRegistry.registerIfNotExists(sizeType);

// Safe to call multiple times
conditionalRegistry.registerIfNotExists(colorType); // No-op

Best Practices Summary

  1. Cache Expressions: Create once, reuse many times
  2. Register at Startup: Configure registry before concurrent use
  3. Validate in Transformers: Fail fast with clear error messages
  4. Use Type Hints: Guide anonymous parameter transformation
  5. Handle Errors: Catch and recover from exceptions
  6. Thread Safety: Initialize once, use immutably
  7. Type Safety: Use generic extraction helpers
  8. Generate Snippets: Automate boilerplate code generation
  9. Conditional Registration: Avoid duplicate type errors
  10. Domain Types: Group related types in utility classes

Next Steps

  • Real-World Scenarios - Complete integration examples
  • Edge Cases - Advanced scenarios
  • Reference Documentation - Detailed API documentation