Dynamic Templates v3.4.0
Dynamic templates allow you to use request data in your stub responses at runtime. This feature enables creating more realistic and flexible mock responses that adapt based on the incoming request.
Overview
Dynamic templates use Go's text/template
syntax to process request data and generate responses. Templates are processed at runtime, not at load time, allowing for real-time data substitution.
Basic Syntax
Request Data Access
Use
to access request data:{{.Request.field}}
- service: example.Service
method: GetUser
input:
matches:
id: "\\d+"
output:
data:
id: "{{.Request.id}}"
name: "User {{.Request.id}}"
email: "user{{.Request.id}}@example.com"
Header Access
Use
to access request headers:{{.Headers.field}}
- service: example.Service
method: GetUser
input:
equals:
id: "admin"
output:
headers:
x-user-role: "{{.Headers.authorization | split \" \" | index 1 | upper}}"
data:
id: "{{.Request.id}}"
role: "admin"
Template Functions
GripMock provides several built-in template functions:
String Functions
upper(s)
: Convert to uppercaselower(s)
: Convert to lowercasetitle(s)
: Convert to title casesplit(s, sep)
: Split string by separatorjoin(slice, sep)
: Join string slice with separatorindex(slice, i)
: Get element at index from slice
Math Functions
len(slice)
: Get length of slicemul(a, b)
: Multiply two numbersadd(a, b)
: Add two numberssub(a, b)
: Subtract two numbersdiv(a, b)
: Divide two numbers (returns 0 for division by zero)
Slice Functions
sum(slice)
: Sum all values in a slicemul(slice)
: Multiply all values in a sliceavg(slice)
: Calculate average of all values in a slicemin(slice)
: Find minimum value in a slicemax(slice)
: Find maximum value in a slice
Time Functions
now()
: Get current time (changes for each message)requestTime()
: Get atomic request time (same for all templates in one request)unix(t)
: Convert time to Unix timestampformat(t, layout)
: Format time with layout
Utility Functions
json(v)
: Convert value to JSON string
State Management
You can access and modify request state directly using .State
:
: Get value from request state{{.State.key}}
: Set value in request state (returns empty string){{setState "key" "value"}}
State is isolated per request and can be used to track calculations across multiple template evaluations.
Technical Parameters
Dynamic templates provide access to essential technical parameters:
Core Parameters
: Current message index (0-based) for streaming{{.MessageIndex}}
: Atomic request time for consistent timestamps{{.RequestTime}}
: Request-scoped state for tracking calculations across templates{{.State}}
Streaming Context
: Slice of all non-empty client messages for client streaming{{.Requests}}
- Use
to get the count of messages{{len .Requests}}
- Use
to access a specific message{{(index .Requests 0).field}}
Streaming Support
Unary Requests
Templates are processed once per request with full access to request data.
Server Streaming
Templates are processed once before streaming starts. The same processed data is used for all stream messages.
Client Streaming
Templates are processed after all client messages are received. You have access to:
: All received non-empty messages{{.Requests}}
: Total number of messages{{len .Requests}}
: Access message by index, then field via{{(index .Requests N)}}
{{(index .Requests 0).value}}
- The last message is used as primary
.Request
Bidirectional Streaming
Templates are processed for each message with:
: Current message index (0-based){{.MessageIndex}}
- Current message data as primary request data
Examples
See the complete ecommerce and calculator examples in the examples/projects/
directory for full demonstrations of dynamic templates with all streaming types.
E-commerce Product Lookup
- service: ecommerce.EcommerceService
method: GetProduct
input:
matches:
product_id: "PROD_\\d+"
user_id: "USER_\\d+"
output:
data:
product_id: "{{.Request.product_id}}"
name: "Product {{.Request.product_id}}"
description: "Dynamic product for user {{.Request.user_id}}"
user_discount: "{{.Request.user_id | split \"_\" | index 1 | title}}"
Order Creation with Dynamic ID
- service: ecommerce.EcommerceService
method: CreateOrder
input:
equals:
user_id: "USER_123"
output:
data:
order_id: "ORDER_{{.Request.user_id | split \"_\" | index 1}}_{{now | unix}}"
user_id: "{{.Request.user_id}}"
total_amount: "{{.Request.items | len | mul 25.50}}"
status: "processing"
Customer Support Chat
- service: ecommerce.EcommerceService
method: CustomerSupportChat
input:
equals:
user_id: "USER_789"
output:
stream:
- message_id: "MSG_{{.MessageIndex}}_SUPPORT"
user_id: "SUPPORT_001"
content: "Hello! I'm support agent for message {{.MessageIndex}}. How can I help you with: {{.Request.content}}"
timestamp: "{{now | format \"2006-01-02T15:04:05Z\"}}"
sender_type: "support"
Mathematical Calculator with Real Calculations
- service: calculator.CalculatorService
method: CalculateAverage
inputs:
- matches:
value: "\\d+(\\.\\d+)?"
- matches:
value: "\\d+(\\.\\d+)?"
- matches:
value: "\\d+(\\.\\d+)?"
output:
data:
result: "{{avg (extract .Requests `value`)}}"
count: "{{len .Requests}}"
sum: "{{sum (extract .Requests `value`)}}"
- service: calculator.CalculatorService
method: DivideNumbers
inputs:
- equals:
value: 100.0
- equals:
value: 2.0
output:
data:
result: "{{div (index (extract .Requests `value`) 0) (index (extract .Requests `value`) 1)}}"
count: "{{len .Requests}}"
Advanced Usage
Conditional Responses
You can create different responses based on request data:
# Different responses for different users
- service: example.Service
method: GetUser
input:
equals:
user_id: "USER_789"
output:
data:
user_id: "SUPPORT_001"
content: "Hello! I'm support agent for message {{.MessageIndex}}"
- service: example.Service
method: GetUser
input:
equals:
user_id: "USER_999"
output:
data:
user_id: "SUPPORT_SPECIAL"
content: "Special support for user 999, message {{.MessageIndex}}"
Complex Calculations
- service: example.Service
method: CalculateTotal
input:
equals:
user_id: "USER_123"
output:
data:
total: "{{.Request.items | len | mul 25.50}}"
discount: "{{.Request.user_tier | mul 0.1}}"
final_total: "{{.Request.total | mul 0.9}}"
Error Handling with Dynamic Messages
- service: ecommerce.EcommerceService
method: GetProduct
input:
equals:
product_id: "INVALID_PROD"
user_id: "USER_ERROR"
output:
error: "Product {{.Request.product_id}} not found for user {{.Request.user_id}}. Please check your request."
code: 5
State Management Example
- service: example.Service
method: ProcessOrder
input:
equals:
user_id: "USER_123"
output:
data:
order_id: "ORDER_{{.Request.user_id | split \"_\" | index 1}}_{{now | unix}}"
processing_step: "{{if .State.step}}{{.State.step}}{{else}}1{{end}}"
message: "{{setState \"step\" (add (.State.step | default 0) 1)}}Processing step {{.State.step}}"
Implementation Details
Template Processing Flow
- Detection: Templates containing
,{{.Request.}}
,{{.Headers.}}
,{{.MessageIndex}}
, or{{.Requests.}}
are identified as dynamic{{.State}}
- Processing: Dynamic templates are processed at runtime, not at load time
- Execution: Go's
text/template
engine processes templates with custom functions - Integration: Processed data is integrated into gRPC responses
YAML Processing
- Dynamic templates are detected and preserved during YAML → JSON conversion
- Static templates (no
.Request/.Headers/.MessageIndex/.Requests/.State
) are processed at load time - Dynamic evaluation happens only at runtime
Backward Compatibility
Dynamic templates are fully backward compatible:
- Static templates (without
or{{.Request.}}
) continue to work unchanged{{.Headers.}}
- No migration required for existing stubs
- Dynamic templates are opt-in only
Performance Considerations
- Template processing happens at runtime, so there's a small performance impact
- Complex template functions may impact performance
- Template errors return gRPC internal errors
- Consider caching for high-throughput scenarios
Thread Safety and Atomicity
Atomic Functions
All template functions are designed to be thread-safe and atomic:
- Mathematical functions (
add
,mul
,sub
,div
,len
): Pure functions, no side effects - String functions (
upper
,lower
,split
,join
): Pure functions, no side effects - Time functions:
now()
returns current time for each message,requestTime()
uses atomic time within a single request
Race Condition Prevention
- Each request gets its own
TemplateData
instance - Time functions use atomic timestamps within a single request
- No shared state between concurrent requests
- Template processing is isolated per request
Error Handling
Template errors are handled gracefully:
- Invalid template syntax returns gRPC internal errors
- Missing request fields are treated as empty strings
- Template processing errors are logged for debugging
- Division by zero returns 0 instead of causing errors
- For server streaming with
output.stream
andoutput.error
/output.code
set: stream messages are sent first, then the error is returned. Ifoutput.stream
is empty, the error is returned immediately
Best Practices
- Use meaningful field names: Make templates readable and maintainable
- Test thoroughly: Verify templates work with different request data
- Keep templates simple: Avoid overly complex template logic
- Use appropriate functions: Choose the right template function for your use case
- Consider performance: Be mindful of template complexity in high-throughput scenarios
- Handle edge cases: Consider division by zero, missing fields, etc.
- Use state management: Leverage state functions for complex calculations
Limitations
- Template errors cause gRPC internal errors
- State is request-scoped and not persisted between requests
Migration Guide
From Static to Dynamic Templates
Before (Static):
output:
data:
id: "123"
name: "User 123"
After (Dynamic):
output:
data:
id: "{{.Request.id}}"
name: "User {{.Request.id}}"
Testing Dynamic Templates
# Start server
go run main.go examples/projects/calculator/service.proto --stub examples/projects/calculator
# Run tests
grpctestify examples/projects/calculator
Important Notes
- Do not use dynamic templates inside
input.equals
,input.contains
, orinput.matches
. Matching expressions must be static (plain strings, numbers, or regex strings). Use dynamic templates only in theoutput
section
Additional Functions
Along with the functions listed above, the following helpers are available:
extract(messages, field)
→ returns a slice withfield
extracted from each message (e.g.,extract .Requests "value"
)sprintf
,str
,int
,int64
,float
,round
,floor
,ceil