Dynamic Templates v3.4.0
A stub response can read the request that triggered it. Templates use Go's text/template syntax and are evaluated per request, not at load time.
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. Keys with a dash — most gRPC metadata — are only reachable as {{.Headers.field}}:{{index .Headers "x-user"}}
- 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
Go's own template builtins (len, index, if, range, …) are available. GripMock adds these:
String Functions
upper(s),lower(s),title(s): change casesplit(s, sep): split a string into a slicejoin(slice, sep): join a slice into a stringsprintf(format, args...),str(v): format a value
Math Functions
add,sub,div,mod: two numbers.divreturns 0 for division by zerosum,mul,avg,min,max: variadic — either spread numbers ({{sum 1 2 3}}) or one slice ({{sum .Request.items}})int,int64,float,round,floor,ceil,decimal: numeric conversioneq,gt,gte,lt,lte: comparison
Time Functions
now(): current time, re-evaluated per messageduration(n, unit?): number to duration string,msby defaultregressive(attempt, start, step),backoff(attempt, base, cap?),jitter(min, max): ready-made delay curvesunix(t): time to Unix timestampformat(t, layout): format a time
For a timestamp that stays identical across every template in one request, use the .RequestTime field rather than now().
Utility Functions
json(v): value to JSON stringextract(messages, field): pullfieldout of each message —{{extract .Requests "value"}}uuid,uuid2base64,uuid2bytes,uuid2int64,string2base64,bytes2base64,bytes: identifier and encoding conversionfaker.*: see the Faker reference
Plugin Functions v3.5.0
Custom functions provided by plugins are also available in templates. Load plugins using the --plugins flag and use their functions just like built-in functions.
Example with hash plugin:
output:
data:
hash: "{{.Request.data | sha256}}"
checksum: "{{.Request.data | crc32}}"Example with math plugin:
output:
data:
result: "{{pow .Request.base .Request.exponent}}"
sqrt: "{{sqrt .Request.value}}"See Plugins for more information on creating and using custom plugin functions.
Built-in Faker Object v3.10.0
GripMock ships with a built-in faker object for realistic dynamic values.
Available semantic groups include:
faker.Person(name, age, gender, ...)faker.Contact(email, phone, username, url)faker.Geo(country, city, latitude, longitude, ...)faker.Network(ip, domain, user-agent, http status/method)faker.Company,faker.Commerce,faker.Text,faker.DateTime,faker.Identity
See full key-by-key reference in Faker Reference.
Example:
- service: example.UserService
method: GetProfile
input:
matches:
id: "\\d+"
output:
data:
id: "{{.Request.id}}"
first_name: "{{faker.Person.FirstName}}"
last_name: "{{faker.Person.LastName}}"
email: "{{faker.Contact.Email}}"
city: "{{faker.Geo.City}}"
lat: "{{faker.Geo.Latitude}}"
lon: "{{faker.Geo.Longitude}}"
ip: "{{faker.Network.IPv4}}"
user_agent: "{{faker.Network.UserAgent}}"
account_id: "{{faker.Identity.UUID}}"Technical Parameters
Core Parameters
: Current message index (0-based) for streaming{{.MessageIndex}}: Request time, identical for every template in one request (alias:{{.RequestTime}}){{.Timestamp}}: UUID of the stub that matched (alias:{{.StubID}}){{.RequestID}}: Matches of this stub in the session, 1-based (alias{{.AttemptNumber}}){{.AttemptIndex}}:{{.MaxAttempts}}options.times, 0 when unlimited (alias){{.TotalAttempts}}
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: 5Implementation 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/templateengine 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
Thread Safety
Every request builds its own template data. Nothing is shared between concurrent requests, and the functions themselves are pure. now() re-reads the clock per message; .RequestTime is fixed for the whole 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.streamandoutput.error/output.codeset: stream messages are sent first, then the error is returned. Ifoutput.streamis empty, the error is returned immediately
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/calculatorImportant Notes
Templates belong in output only. Every matcher — input.equals, input.contains, input.matches, input.glob, input.anyOf, and their headers counterparts — must be static: plain strings, numbers, regex or glob patterns.