Matchers and Resolvers Guide
This guide explains how to use StuntDouble’s powerful input matchers and dynamic value resolvers to create flexible, realistic mock responses.
Overview
StuntDouble provides two core components for advanced mocking:
Input Matchers — Match tool inputs using MongoDB-style operators (
$gt,$in,$regex, etc.)Value Resolvers — Generate dynamic outputs with placeholders (
{{now}},{{uuid}},{{input.field}})
┌─────────────────────────────────────────────────────────────────────────────┐
│ Mock Resolution Pipeline │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Tool Input ──▶ InputMatcher ──▶ Select Mock Case ──▶ ValueResolver ──▶ Output
│ │
│ {"amount": 1500} $gt: 1000? Case 2 {{now}} "2025-01-05"
│ ▼ output {{uuid}} "a1b2c3..."
│ YES │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Input Matchers
Basic Usage
The InputMatcher class matches tool input parameters against patterns using operators.
from stuntdouble.matching import InputMatcher, matches
matcher = InputMatcher()
# Exact match
matcher.matches({"status": "active"}, {"status": "active"}) # True
matcher.matches({"status": "active"}, {"status": "inactive"}) # False
# Operator match
matcher.matches({"amount": {"$gt": 1000}}, {"amount": 1500}) # True
matcher.matches({"amount": {"$gt": 1000}}, {"amount": 500}) # False
# Convenience function
matches({"status": "active"}, {"status": "active"}) # True
Supported Operators
Operator |
Description |
Example Pattern |
Matches |
|---|---|---|---|
|
Exact equality (default) |
|
|
|
Not equal |
|
|
|
Greater than |
|
|
|
Greater than or equal |
|
|
|
Less than |
|
|
|
Less than or equal |
|
|
|
Value in list |
|
|
|
Not in list |
|
|
|
String contains |
|
|
|
Regex match |
|
|
|
Key exists |
|
|
Multiple Conditions
Combine multiple operators with AND logic:
# All conditions must match
pattern = {
"amount": {"$gt": 100, "$lt": 1000}, # 100 < amount < 1000
"status": "active"
}
matches(pattern, {"amount": 500, "status": "active"}) # True
matches(pattern, {"amount": 500, "status": "pending"}) # False (status mismatch)
matches(pattern, {"amount": 50, "status": "active"}) # False (amount too low)
Catch-All Patterns
Use None or empty dict for catch-all matching:
# None matches anything
matches(None, {"any": "input", "goes": "here"}) # True
# Empty dict also matches anything
matches({}, {"any": "input"}) # True
Real-World Examples
Example 1: Customer Tier Logic
# Via scenario_metadata (LangGraph)
scenario_metadata = {
"mocks": {
"get_customer": [
{"input": {"customer_id": {"$regex": "^VIP-"}}, "output": {"tier": "platinum", "discount": 0.25}},
{"input": {"customer_id": {"$contains": "CORP"}}, "output": {"tier": "enterprise", "discount": 0.15}},
{"input": {"total_purchases": {"$gte": 10000}}, "output": {"tier": "gold", "discount": 0.10}},
{"output": {"tier": "standard", "discount": 0}}
]
}
}
Example 2: Bill Filtering
# Via scenario_metadata (LangGraph)
scenario_metadata = {
"mocks": {
"list_bills": [
{"input": {"status": "overdue", "amount": {"$gt": 5000}}, "output": {"priority": "URGENT", "bills": [...]}},
{"input": {"status": "overdue"}, "output": {"priority": "HIGH", "bills": [...]}},
{"input": {"status": {"$in": ["paid", "pending"]}}, "output": {"priority": "LOW", "bills": [...]}},
{"output": {"priority": "NORMAL", "bills": []}}
]
}
}
Value Resolvers
Basic Usage
The ValueResolver class resolves dynamic placeholders in mock outputs.
from stuntdouble.resolving import ValueResolver, ResolverContext, resolve_output
resolver = ValueResolver()
ctx = ResolverContext(input_data={"customer_id": "CUST-123"})
# Simple placeholder
resolver.resolve_dynamic_values("{{uuid}}", ctx) # "a1b2c3d4-e5f6-7890-..."
# Input reference
resolver.resolve_dynamic_values("{{input.customer_id}}", ctx) # "CUST-123"
# Nested structure
resolver.resolve_dynamic_values({
"id": "{{uuid}}",
"created_at": "{{now}}",
"customer": "{{input.customer_id}}"
}, ctx)
# {'id': 'a1b2...', 'created_at': '2025-01-05T...', 'customer': 'CUST-123'}
# Convenience function
resolve_output({"id": "{{uuid}}"}, input_data={"customer_id": "123"})
Timestamp Placeholders
Placeholder |
Description |
Example Output |
|---|---|---|
|
Current datetime (ISO format) |
|
|
Current date |
|
|
7 days from now |
|
|
30 days ago |
|
|
1 week from today |
|
|
2 hours from now |
|
|
Start of today |
|
|
End of today |
|
|
Monday of current week |
|
|
Sunday of current week |
|
|
First day of month |
|
|
Last day of month |
|
|
First day of year |
|
|
Last day of year |
|
Time units:
h— hoursd— daysw— weeksm— minutesM— months (approximate, 30 days)y— years (approximate, 365 days)
Input Reference Placeholders
Placeholder |
Description |
Example |
|---|---|---|
|
Value from tool input |
|
|
With default if missing |
|
ctx = ResolverContext(input_data={"customer_id": "CUST-123", "amount": 500})
resolver.resolve_dynamic_values("Customer: {{input.customer_id}}", ctx) # "Customer: CUST-123"
resolver.resolve_dynamic_values("{{input.email | default('none')}}", ctx) # "none" (field missing)
Generator Placeholders
Placeholder |
Description |
Example Output |
|---|---|---|
|
Random UUID |
|
|
Random integer in range |
|
|
Random float (2 decimals) |
|
|
Random choice from list |
|
|
Sequential ID with prefix |
|
|
Random alphanumeric string |
|
Real-World Examples
Example 1: Invoice Creation
mock("create_invoice", {
"id": "{{sequence('INV')}}",
"created_at": "{{now}}",
"due_date": "{{now + 30d}}",
"customer_id": "{{input.customer_id}}",
"amount": "{{input.amount}}",
"status": "pending",
"reference": "{{uuid}}"
})
Example 2: User Profile
mock("get_user", {
"id": "{{input.user_id}}",
"email": "{{input.user_id}}@example.com",
"created_at": "{{now - 90d}}",
"last_login": "{{now - 2h}}",
"session_token": "{{random_string(32)}}",
"loyalty_points": "{{random_int(100, 5000)}}"
})
Example 3: Billing Period
# Via scenario_metadata (LangGraph)
scenario_metadata = {
"mocks": {
"get_billing_period": [{
"output": {
"period_start": "{{start_of_month}}",
"period_end": "{{end_of_month}}",
"invoice_due": "{{end_of_month + 15d}}",
"customer": "{{input.customer_id}}",
"status": "{{choice('pending', 'processing', 'complete')}}"
}
}]
}
}
Combining Matchers and Resolvers
The real power comes from combining input matching with dynamic outputs:
# Via scenario_metadata (LangGraph)
scenario_metadata = {
"mocks": {
"process_payment": [
{
"input": {"amount": {"$gt": 10000}},
"output": {
"transaction_id": "{{uuid}}",
"status": "pending_review",
"review_deadline": "{{now + 24h}}",
"amount": "{{input.amount}}",
"requires_approval": True
}
},
{
"input": {"amount": {"$gt": 0}},
"output": {
"transaction_id": "{{uuid}}",
"status": "completed",
"processed_at": "{{now}}",
"amount": "{{input.amount}}",
"requires_approval": False
}
},
{
"output": {
"transaction_id": None,
"status": "invalid",
"error": "Invalid payment amount"
}
}
]
}
}
Using with LangGraph
Matchers and resolvers work seamlessly with the LangGraph approach via scenario_metadata:
from stuntdouble import inject_scenario_metadata
# Pass mock data with operators and placeholders in scenario_metadata
config = inject_scenario_metadata({}, {
"mocks": {
"get_customer": [
# Match VIP customers
{
"input": {"customer_id": {"$regex": "^VIP-"}},
"output": {
"id": "{{input.customer_id}}",
"tier": "platinum",
"since": "{{now - 365d}}"
}
},
# Catch-all
{
"output": {
"id": "{{input.customer_id}}",
"tier": "standard",
"since": "{{now}}"
}
}
]
}
})
result = await graph.ainvoke(state, config=config)
Best Practices
1. Order Patterns from Specific to General
# Most specific first, catch-all last
scenario_metadata = {
"mocks": {
"process": [
{"input": {"type": "premium", "amount": {"$gt": 1000}}, "output": {...}},
{"input": {"type": "premium"}, "output": {...}},
{"input": {"amount": {"$gt": 1000}}, "output": {...}},
{"output": {...}} # Catch-all
]
}
}
2. Use Defaults for Missing Fields
# Good: Handle missing input gracefully
{"email": "{{input.email | default('unknown@example.com')}}"}
# Risky: May produce None if field missing
{"email": "{{input.email}}"}
4. Combine Operators for Range Matching
# Match amounts between 100 and 1000
({"amount": {"$gte": 100, "$lte": 1000}}, {...})
5. Use $exists for Optional Fields
# Only match if email is provided
({"email": {"$exists": true}}, {"verified": True})
# Match when email is NOT provided
({"email": {"$exists": false}}, {"verified": False})
Debugging
Check if a Pattern Matches
from stuntdouble.matching import matches
pattern = {"amount": {"$gt": 100}}
actual = {"amount": 50}
if not matches(pattern, actual):
print(f"Pattern {pattern} does not match {actual}")
Check for Placeholders
from stuntdouble.resolving import has_placeholders
output = {"id": "{{uuid}}", "name": "static"}
print(has_placeholders(output)) # True
output = {"id": "123", "name": "static"}
print(has_placeholders(output)) # False
Enable Debug Logging
import logging
logging.getLogger("stuntdouble.matching").setLevel(logging.DEBUG)
logging.getLogger("stuntdouble.resolving").setLevel(logging.DEBUG)
API Reference
Matchers
Function/Class |
Description |
|---|---|
|
Class for pattern matching with operators |
|
Check if actual matches pattern |
|
Convenience function using singleton matcher |
Resolvers
Function/Class |
Description |
|---|---|
|
Class for placeholder resolution |
|
Resolve placeholders in value |
|
Context with input data and state |
|
Convenience function for resolution |
|
Check if value contains placeholders |
Next Steps
Topic |
Guide |
|---|---|
LangGraph integration |
|
Custom mocked tools |
|
Mock format reference |
|
Evaluation workflows |