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

transformers.mddocs/reference/

Transformers

Transformers convert matched strings to Java types using functional interfaces. The library supports three transformer types for different conversion scenarios: single capture groups, multiple capture groups, and type-based default transformation.

Thread Safety

All transformer interfaces are functional interfaces and thread-safety depends on the implementation:

  • Transformer implementations: Must be thread-safe if used concurrently (lambda expressions are typically thread-safe if they don't mutate shared state)
  • Transformer invocation: Called during Argument.getValue(), which can be concurrent

Capabilities

Transformer Interface

Transform a string from a single capture group to type T.

package io.cucumber.cucumberexpressions;

import org.jspecify.annotations.Nullable;

/**
 * Transform string from single capture group to type T
 * Functional interface for simple single-value transformations
 */
@FunctionalInterface
public interface Transformer<T> {
    /**
     * Transform a captured string to type T
     * 
     * IMPORTANT: Exceptions thrown from this method are preserved and re-thrown
     * when Argument.getValue() is called. This allows validation in transformers.
     * 
     * @param arg - Captured string value (may be null)
     * @return Transformed value of type T (may be null)
     * @throws Throwable if transformation fails (signals validation or conversion errors)
     */
    @Nullable T transform(@Nullable String arg) throws Throwable;
}

Usage Examples:

import io.cucumber.cucumberexpressions.*;

// Simple string to integer transformer
Transformer<Integer> intTransformer = (String arg) -> {
    return arg == null ? null : Integer.parseInt(arg);
};

// String to custom type transformer
Transformer<Color> colorTransformer = (String arg) -> {
    if (arg == null) return null;
    switch (arg.toLowerCase()) {
        case "red": return Color.RED;
        case "blue": return Color.BLUE;
        case "green": return Color.GREEN;
        default: throw new IllegalArgumentException("Unknown color: " + arg);
    }
};

// Use in parameter type
ParameterType<Color> colorType = new ParameterType<>(
    "color",
    "red|blue|green",
    Color.class,
    colorTransformer
);

// Transformer with complex logic
Transformer<LocalDate> dateTransformer = (String arg) -> {
    if (arg == null) return null;
    // Support multiple date formats
    try {
        return LocalDate.parse(arg, DateTimeFormatter.ISO_DATE);
    } catch (DateTimeParseException e1) {
        try {
            return LocalDate.parse(arg, DateTimeFormatter.ofPattern("MM/dd/yyyy"));
        } catch (DateTimeParseException e2) {
            throw new IllegalArgumentException(
                "Invalid date format. Expected ISO date or MM/dd/yyyy");
        }
    }
};

// Transformer with validation
Transformer<Age> ageTransformer = (String arg) -> {
    if (arg == null) {
        throw new IllegalArgumentException("Age cannot be null");
    }
    int value = Integer.parseInt(arg);
    if (value < 0 || value > 150) {
        throw new IllegalArgumentException(
            "Age must be between 0 and 150, got: " + value);
    }
    return new Age(value);
};

// Transformer with trimming
Transformer<String> trimTransformer = (String arg) -> {
    return arg == null ? null : arg.trim();
};

// Transformer with case conversion
Transformer<String> upperTransformer = (String arg) -> {
    return arg == null ? null : arg.toUpperCase();
};

CaptureGroupTransformer Interface

Transform strings from multiple capture groups to type T, useful for complex types requiring multiple input values.

package io.cucumber.cucumberexpressions;

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;

/**
 * Transform strings from multiple capture groups to type T
 * Functional interface for multi-value transformations
 */
@FunctionalInterface
@API(status = API.Status.STABLE)
public interface CaptureGroupTransformer<T> {
    /**
     * Transform multiple captured strings to type T
     * 
     * The args array contains all captured groups:
     * - args[0] is the first capture group
     * - args[1] is the second capture group
     * - etc.
     * 
     * IMPORTANT: Exceptions thrown from this method are preserved and re-thrown
     * when Argument.getValue() is called.
     * 
     * @param args - Array of captured string values (elements may be null)
     * @return Transformed value of type T (may be null)
     * @throws Throwable if transformation fails (signals validation or conversion errors)
     */
    @Nullable T transform(@Nullable String[] args) throws Throwable;
}

Usage Examples:

import io.cucumber.cucumberexpressions.*;
import java.util.Arrays;

// Transform two captures into a Point
CaptureGroupTransformer<Point> pointTransformer = (String[] args) -> {
    if (args == null || args.length < 2) {
        throw new IllegalArgumentException("Point requires two coordinates");
    }
    int x = Integer.parseInt(args[0]);
    int y = Integer.parseInt(args[1]);
    return new Point(x, y);
};

// Use in parameter type with multiple capture groups
ParameterType<Point> pointType = new ParameterType<>(
    "point",
    "(\\d+),(\\d+)",  // Two capture groups
    Point.class,
    pointTransformer
);

// Transform RGB values into Color
CaptureGroupTransformer<Color> rgbTransformer = (String[] args) -> {
    if (args == null || args.length < 3) {
        throw new IllegalArgumentException("RGB requires three values");
    }
    int r = Integer.parseInt(args[0]);
    int g = Integer.parseInt(args[1]);
    int b = Integer.parseInt(args[2]);
    
    // Validation
    if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
        throw new IllegalArgumentException(
            "RGB values must be 0-255, got: " + Arrays.toString(args));
    }
    
    return new Color(r, g, b);
};

ParameterType<Color> rgbType = new ParameterType<>(
    "rgb",
    "(\\d+)/(\\d+)/(\\d+)",  // Three capture groups
    Color.class,
    rgbTransformer
);

// Transform with validation
CaptureGroupTransformer<DateRange> dateRangeTransformer = (String[] args) -> {
    if (args == null || args.length < 2) {
        throw new IllegalArgumentException("DateRange requires two dates");
    }
    LocalDate start = LocalDate.parse(args[0]);
    LocalDate end = LocalDate.parse(args[1]);
    if (end.isBefore(start)) {
        throw new IllegalArgumentException(
            "End date must be after start date");
    }
    return new DateRange(start, end);
};

ParameterType<DateRange> dateRangeType = new ParameterType<>(
    "dateRange",
    "(\\d{4}-\\d{2}-\\d{2})\\.\\.\\.(\\d{4}-\\d{2}-\\d{2})",
    DateRange.class,
    dateRangeTransformer
);

// Transform coordinates with optional values
CaptureGroupTransformer<Coordinate> coordTransformer = (String[] args) -> {
    if (args == null) return null;
    
    // Latitude (required)
    Double lat = args[0] != null ? Double.parseDouble(args[0]) : 0.0;
    
    // Longitude (required)
    Double lon = args[1] != null ? Double.parseDouble(args[1]) : 0.0;
    
    // Altitude (optional)
    Double alt = (args.length > 2 && args[2] != null) 
        ? Double.parseDouble(args[2]) 
        : null;
    
    return new Coordinate(lat, lon, alt);
};

// Example usage in expression
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("Point at {point}");
Optional<List<Argument<?>>> match = expr.match("Point at 10,20");
Point point = (Point) match.get().get(0).getValue();
// point = Point(10, 20)

ParameterByTypeTransformer Interface

Default transformer for built-in and anonymous parameter types, used when no specific parameter type matches.

package io.cucumber.cucumberexpressions;

import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import java.lang.reflect.Type;

/**
 * Default transformer for built-in and anonymous parameter types
 * Functional interface for type-based transformation
 * Used for anonymous {} parameters with type hints
 */
@FunctionalInterface
@API(status = API.Status.STABLE)
public interface ParameterByTypeTransformer {
    /**
     * Transform string value to target type
     * 
     * This transformer is invoked for:
     * - Anonymous {} parameters with type hints
     * - Built-in types when no specific transformer matches
     * - Fallback transformation scenarios
     * 
     * IMPORTANT: Exceptions thrown from this method are preserved and re-thrown
     * when Argument.getValue() is called.
     * 
     * @param fromValue - String value to transform (may be null)
     * @param toValueType - Target Java type (from type hint or parameter definition)
     * @return Transformed value (may be null)
     * @throws Throwable if transformation fails
     */
    @Nullable Object transform(@Nullable String fromValue, Type toValueType)
        throws Throwable;
}

Usage Examples:

import io.cucumber.cucumberexpressions.*;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.UUID;
import com.fasterxml.jackson.databind.ObjectMapper;

// Simple default transformer for common types
ParameterByTypeTransformer simpleTransformer = (fromValue, toValueType) -> {
    if (fromValue == null) return null;
    
    if (toValueType == UUID.class) {
        return UUID.fromString(fromValue);
    }
    if (toValueType == LocalDate.class) {
        return LocalDate.parse(fromValue);
    }
    if (toValueType == java.net.URL.class) {
        return new java.net.URL(fromValue);
    }
    
    // Fallback - return as string
    return fromValue;
};

ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);
registry.setDefaultParameterTransformer(simpleTransformer);

// JSON-based default transformer using Jackson
ObjectMapper objectMapper = new ObjectMapper();
ParameterByTypeTransformer jsonTransformer = (fromValue, toValueType) -> {
    if (fromValue == null) return null;
    
    // Try to deserialize as JSON
    try {
        return objectMapper.readValue(fromValue, 
            objectMapper.constructType(toValueType));
    } catch (Exception e) {
        // Fallback to string
        return fromValue;
    }
};

registry.setDefaultParameterTransformer(jsonTransformer);

// Use with anonymous parameters
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("User ID is {}");

// Type hint guides the transformer
Optional<List<Argument<?>>> match = expr.match(
    "User ID is 550e8400-e29b-41d4-a716-446655440000",
    UUID.class  // Type hint
);
UUID userId = (UUID) match.get().get(0).getValue();

// Advanced: Support enums and custom types
ParameterByTypeTransformer advancedTransformer = (fromValue, toValueType) -> {
    if (fromValue == null) return null;
    
    // Handle enums
    if (toValueType instanceof Class<?>) {
        Class<?> clazz = (Class<?>) toValueType;
        if (clazz.isEnum()) {
            @SuppressWarnings("unchecked")
            Class<? extends Enum> enumClass = (Class<? extends Enum>) clazz;
            return Enum.valueOf(enumClass, fromValue);
        }
    }
    
    // Handle primitives and wrappers
    if (toValueType == Integer.class || toValueType == int.class) {
        return Integer.parseInt(fromValue);
    }
    if (toValueType == Long.class || toValueType == long.class) {
        return Long.parseLong(fromValue);
    }
    if (toValueType == Boolean.class || toValueType == boolean.class) {
        return Boolean.parseBoolean(fromValue);
    }
    if (toValueType == Double.class || toValueType == double.class) {
        return Double.parseDouble(fromValue);
    }
    if (toValueType == Float.class || toValueType == float.class) {
        return Float.parseFloat(fromValue);
    }
    
    // Handle custom types with static valueOf method
    if (toValueType instanceof Class<?>) {
        Class<?> clazz = (Class<?>) toValueType;
        try {
            var method = clazz.getMethod("valueOf", String.class);
            return method.invoke(null, fromValue);
        } catch (Exception e) {
            // No valueOf method, continue
        }
        
        // Try constructor with String parameter
        try {
            var constructor = clazz.getConstructor(String.class);
            return constructor.newInstance(fromValue);
        } catch (Exception e) {
            // No string constructor, continue
        }
    }
    
    // Fallback
    return fromValue;
};

Built-In Transformer

The library includes a built-in transformer (BuiltInParameterTransformer) that handles:

  • All numeric types (int, long, float, double, byte, short, BigInteger, BigDecimal)
  • Boolean values
  • Enum types
  • Localized number parsing

This transformer is used internally for built-in parameter types and as the initial default transformer.

// The built-in transformer is automatically used for built-in types
ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH);

// Built-in types work automatically
ExpressionFactory factory = new ExpressionFactory(registry);
Expression expr = factory.createExpression("I have {int} items");
Optional<List<Argument<?>>> match = expr.match("I have 42 items");
Integer count = (Integer) match.get().get(0).getValue();
// count = 42

// Enum support in built-in transformer
enum Status { ACTIVE, INACTIVE }
Expression enumExpr = factory.createExpression("Status is {}");
Optional<List<Argument<?>>> match2 = enumExpr.match("Status is ACTIVE", Status.class);
Status status = (Status) match2.get().get(0).getValue();
// status = Status.ACTIVE

Transformer Error Handling

Transformers can throw exceptions to signal transformation failures:

import io.cucumber.cucumberexpressions.*;

// Transformer with validation
Transformer<Age> ageTransformer = (String arg) -> {
    if (arg == null) {
        throw new IllegalArgumentException("Age cannot be null");
    }
    int value = Integer.parseInt(arg);
    if (value < 0 || value > 150) {
        throw new IllegalArgumentException("Age must be between 0 and 150");
    }
    return new Age(value);
};

ParameterType<Age> ageType = new ParameterType<>(
    "age",
    "\\d+",
    Age.class,
    ageTransformer
);

registry.defineParameterType(ageType);

Expression expr = factory.createExpression("Person is {age} years old");

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

// Invalid age
try {
    Optional<List<Argument<?>>> match2 = expr.match("Person is 200 years old");
    if (match2.isPresent()) {
        Age age2 = (Age) match2.get().get(0).getValue();
        // getValue() will throw the IllegalArgumentException from transformer
    }
} catch (IllegalArgumentException e) {
    System.err.println("Validation error: " + e.getMessage());
    // Handle validation error
}

// Number format exception
try {
    Optional<List<Argument<?>>> match3 = expr.match("Person is abc years old");
    // This won't match because "abc" doesn't match \\d+
    if (match3.isPresent()) {
        // Won't reach here
    } else {
        System.out.println("No match - pattern rejected invalid format");
    }
} catch (NumberFormatException e) {
    // Won't be thrown because pattern rejects non-numeric input
}

Exception Handling Best Practices:

// Pattern 1: Specific exception types for different error conditions
Transformer<Email> emailTransformer = (String arg) -> {
    if (arg == null || arg.isEmpty()) {
        throw new IllegalArgumentException("Email cannot be empty");
    }
    if (!arg.contains("@")) {
        throw new IllegalArgumentException("Email must contain @");
    }
    if (!arg.matches("[\\w.+-]+@[\\w.-]+\\.[a-z]{2,}")) {
        throw new IllegalArgumentException("Invalid email format");
    }
    return new Email(arg);
};

// Pattern 2: Wrapper for detailed error information
public class TransformationException extends RuntimeException {
    private final String inputValue;
    private final Type targetType;
    
    public TransformationException(String message, String inputValue, Type targetType) {
        super(message);
        this.inputValue = inputValue;
        this.targetType = targetType;
    }
    
    public String getInputValue() { return inputValue; }
    public Type getTargetType() { return targetType; }
}

Transformer<CustomType> customTransformer = (String arg) -> {
    try {
        return CustomType.parse(arg);
    } catch (Exception e) {
        throw new TransformationException(
            "Failed to parse as CustomType: " + e.getMessage(),
            arg,
            CustomType.class
        );
    }
};

Null Handling

All transformer interfaces support nullable parameters and return values:

// Transformer that handles null gracefully
Transformer<String> nullSafeTransformer = (String arg) -> {
    return arg == null ? "default" : arg.trim();
};

// Transformer that returns null
Transformer<Optional<String>> optionalTransformer = (String arg) -> {
    return (arg == null || arg.isEmpty()) ? null : arg;
};

// CaptureGroupTransformer with null handling
CaptureGroupTransformer<Coordinate> coordinateTransformer = (String[] args) -> {
    if (args == null) return null;
    
    // Individual array elements might also be null
    Double lat = args[0] != null ? Double.parseDouble(args[0]) : 0.0;
    Double lon = args[1] != null ? Double.parseDouble(args[1]) : 0.0;
    
    return new Coordinate(lat, lon);
};

// Transformer that validates against null
Transformer<NonNullValue> nonNullTransformer = (String arg) -> {
    if (arg == null) {
        throw new IllegalArgumentException("Value cannot be null");
    }
    return new NonNullValue(arg);
};

Transformer Composition

Transformers can be composed for complex transformation pipelines:

import io.cucumber.cucumberexpressions.*;
import java.util.function.Function;

// Helper to compose transformers
public class TransformerUtils {
    public static <T, U> Transformer<U> compose(
        Transformer<T> first,
        Function<T, U> second
    ) {
        return (String arg) -> {
            T intermediate = first.transform(arg);
            return intermediate == null ? null : second.apply(intermediate);
        };
    }
    
    public static <T> Transformer<T> withDefault(
        Transformer<T> transformer,
        T defaultValue
    ) {
        return (String arg) -> {
            T result = transformer.transform(arg);
            return result != null ? result : defaultValue;
        };
    }
    
    public static <T> Transformer<T> withValidation(
        Transformer<T> transformer,
        java.util.function.Predicate<T> validator,
        String errorMessage
    ) {
        return (String arg) -> {
            T result = transformer.transform(arg);
            if (result != null && !validator.test(result)) {
                throw new IllegalArgumentException(errorMessage);
            }
            return result;
        };
    }
}

// Example: trim then parse
Transformer<Integer> trimAndParse = TransformerUtils.compose(
    (String s) -> s == null ? null : s.trim(),
    (String s) -> Integer.parseInt(s)
);

ParameterType<Integer> trimmedIntType = new ParameterType<>(
    "trimmedInt",
    "\\s*\\d+\\s*",
    Integer.class,
    trimAndParse
);

// Example: parse with default
Transformer<Integer> parseWithDefault = TransformerUtils.withDefault(
    (String s) -> s == null || s.isEmpty() ? null : Integer.parseInt(s),
    0
);

// Example: parse with validation
Transformer<Integer> parsePositive = TransformerUtils.withValidation(
    (String s) -> Integer.parseInt(s),
    (Integer i) -> i > 0,
    "Value must be positive"
);

Advanced Usage Patterns

Pattern 1: Stateful Transformer with Context

public class ContextualTransformer<T> implements Transformer<T> {
    private final Function<String, T> baseTransform;
    private final Map<String, Object> context;
    
    public ContextualTransformer(Function<String, T> baseTransform, Map<String, Object> context) {
        this.baseTransform = baseTransform;
        this.context = context;
    }
    
    @Override
    public T transform(String arg) throws Throwable {
        // Can access context during transformation
        Object sessionData = context.get("session");
        // Use session data in transformation...
        return baseTransform.apply(arg);
    }
}

// Usage
Map<String, Object> context = new HashMap<>();
context.put("locale", Locale.GERMAN);
context.put("timezone", ZoneId.of("Europe/Berlin"));

ContextualTransformer<LocalDateTime> dateTimeTransformer = 
    new ContextualTransformer<>(
        (String s) -> {
            Locale locale = (Locale) context.get("locale");
            // Use locale in parsing...
            return LocalDateTime.parse(s);
        },
        context
    );

Pattern 2: Caching Transformer

public class CachingTransformer<T> implements Transformer<T> {
    private final Transformer<T> delegate;
    private final Map<String, T> cache = new ConcurrentHashMap<>();
    
    public CachingTransformer(Transformer<T> delegate) {
        this.delegate = delegate;
    }
    
    @Override
    public T transform(String arg) throws Throwable {
        if (arg == null) return delegate.transform(null);
        
        return cache.computeIfAbsent(arg, k -> {
            try {
                return delegate.transform(k);
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        });
    }
}

// Usage for expensive transformations
Transformer<ComplexObject> expensiveTransformer = (String s) -> {
    // Expensive computation...
    return ComplexObject.parseAndValidate(s);
};

CachingTransformer<ComplexObject> cachedTransformer = 
    new CachingTransformer<>(expensiveTransformer);

Pattern 3: Logging Transformer

public class LoggingTransformer<T> implements Transformer<T> {
    private final Transformer<T> delegate;
    private final Logger logger;
    
    public LoggingTransformer(Transformer<T> delegate, Logger logger) {
        this.delegate = delegate;
        this.logger = logger;
    }
    
    @Override
    public T transform(String arg) throws Throwable {
        logger.debug("Transforming: {}", arg);
        try {
            T result = delegate.transform(arg);
            logger.debug("Transformed {} to {}", arg, result);
            return result;
        } catch (Throwable e) {
            logger.error("Transformation failed for: {}", arg, e);
            throw e;
        }
    }
}

Pattern 4: Builder-Style Transformer Creation

public class TransformerBuilder<T> {
    private Function<String, T> baseTransform;
    private boolean trimInput = false;
    private boolean caseInsensitive = false;
    private T defaultValue = null;
    private java.util.function.Predicate<T> validator = null;
    private String validationMessage = "Validation failed";
    
    public TransformerBuilder<T> transform(Function<String, T> transform) {
        this.baseTransform = transform;
        return this;
    }
    
    public TransformerBuilder<T> trim() {
        this.trimInput = true;
        return this;
    }
    
    public TransformerBuilder<T> caseInsensitive() {
        this.caseInsensitive = true;
        return this;
    }
    
    public TransformerBuilder<T> withDefault(T defaultValue) {
        this.defaultValue = defaultValue;
        return this;
    }
    
    public TransformerBuilder<T> validate(java.util.function.Predicate<T> validator, 
                                          String message) {
        this.validator = validator;
        this.validationMessage = message;
        return this;
    }
    
    public Transformer<T> build() {
        return (String arg) -> {
            if (arg == null || arg.isEmpty()) {
                return defaultValue;
            }
            
            String processed = arg;
            if (trimInput) {
                processed = processed.trim();
            }
            if (caseInsensitive) {
                processed = processed.toLowerCase();
            }
            
            T result = baseTransform.apply(processed);
            
            if (validator != null && result != null && !validator.test(result)) {
                throw new IllegalArgumentException(validationMessage);
            }
            
            return result;
        };
    }
}

// Usage
Transformer<Integer> transformer = new TransformerBuilder<Integer>()
    .transform(Integer::parseInt)
    .trim()
    .withDefault(0)
    .validate(i -> i > 0, "Must be positive")
    .build();

Performance Considerations

Transformation Cost:

  • Simple transformers (parse, trim): < 1µs
  • Complex transformers (validation, regex): 1-10µs
  • I/O-based transformers (database lookup): variable

Optimization Tips:

// GOOD: Reuse compiled patterns
public class PatternTransformer implements Transformer<String> {
    private final Pattern pattern = Pattern.compile("[a-z]+");
    
    @Override
    public String transform(String arg) {
        if (arg == null) return null;
        Matcher m = pattern.matcher(arg);
        return m.matches() ? arg : null;
    }
}

// BAD: Compile pattern on every transformation
Transformer<String> inefficient = (String arg) -> {
    Pattern pattern = Pattern.compile("[a-z]+");  // Compiled every time
    Matcher m = pattern.matcher(arg);
    return m.matches() ? arg : null;
};

// GOOD: Cache expensive lookups
Map<String, User> userCache = new ConcurrentHashMap<>();
Transformer<User> userTransformer = (String userId) -> {
    return userCache.computeIfAbsent(userId, id -> database.findUser(id));
};

// GOOD: Early return for null
Transformer<ComplexType> optimized = (String arg) -> {
    if (arg == null) return null;  // Fast path
    // Expensive computation only if non-null
    return ComplexType.parse(arg);
};

Best Practices

  1. Null Safety: Always handle null inputs gracefully
  2. Error Messages: Provide clear, actionable error messages
  3. Type Safety: Use generics appropriately for type-safe transformations
  4. Performance: Keep transformers lightweight (avoid I/O when possible)
  5. Single Responsibility: Each transformer should handle one transformation concern
  6. Validation: Validate inputs in transformers rather than later in code
  7. Immutability: Return immutable objects when possible
  8. Exception Handling: Throw specific exceptions with context
  9. Thread Safety: Ensure transformers are thread-safe if used concurrently
  10. Documentation: Document transformer behavior, especially edge cases

Related Documentation

  • Parameter Types - Use transformers in parameter types
  • Matching - Access transformed values from arguments
  • Expressions - Create expressions that use transformers
  • Exceptions - Handle transformation errors