Skip to content

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 {{.Request.field}} to access request data:

yaml
- 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 {{.Headers.field}} to access request headers. Keys with a dash — most gRPC metadata — are only reachable as {{index .Headers "x-user"}}:

yaml
- 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"

Templates render leaf values only: range inside a template cannot add or remove array elements. To make an array — or the number of stream messages — follow the request, use Response Templates.

A key whose value is null is dropped from the response v3.22.0, so an optional field can be left out and a oneof can be selected by writing the branches you do not want as null:

yaml
output:
  data:
    success:
      taskId: "T-1"
    error: null

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 case
  • split(s, sep): split a string into a slice
  • join(slice, sep): join a slice into a string
  • sprintf(format, args...), str(v): format a value

Math Functions ​

  • add, sub, div, mod: two numbers. div returns 0 for division by zero
  • sum, 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 conversion
  • eq, gt, gte, lt, lte: comparison

Time Functions ​

  • now(): current time, re-evaluated per message
  • duration(n, unit?): number to duration string, ms by default
  • regressive(attempt, start, step), backoff(attempt, base, cap?), jitter(min, max): ready-made delay curves
  • unix(t): time to Unix timestamp
  • format(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 string
  • extract(messages, field): pull field out of each message — {{extract .Requests "value"}}
  • uuid, uuid2base64, uuid2bytes, uuid2int64, string2base64, bytes2base64, bytes: identifier and encoding conversion
  • faker.*: see the Faker reference
  • where, page, countBy, dict, list, seq: build and query collections v3.22.0 — see Response Templates

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:

yaml
output:
  data:
    hash: "{{.Request.data | sha256}}"
    checksum: "{{.Request.data | crc32}}"

Example with math plugin:

yaml
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:

yaml
- 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 ​

  • {{.MessageIndex}}: Current message index (0-based) for streaming
  • {{.RequestTime}}: Request time, identical for every template in one request (alias: {{.Timestamp}})
  • {{.StubID}}: UUID of the stub that matched (alias: {{.RequestID}})
  • {{.AttemptNumber}}: Matches of this stub in the session, 1-based (alias {{.AttemptIndex}})
  • {{.MaxAttempts}}: options.times, 0 when unlimited (alias {{.TotalAttempts}})

Streaming Context ​

  • {{.Requests}}: Slice of all non-empty client messages for client streaming
  • Use {{len .Requests}} to get the count of messages
  • Use {{(index .Requests 0).field}} to access a specific message

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:

  • {{.Requests}}: All received non-empty messages
  • {{len .Requests}}: Total number of messages
  • {{(index .Requests N)}}: Access message by index, then field via {{(index .Requests 0).value}}
  • The last message is used as primary .Request

Bidirectional Streaming ​

Templates are processed for each message with:

  • {{.MessageIndex}}: Current message index (0-based)
  • 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 ​

yaml
- 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 ​

yaml
- 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 ​

yaml
- 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 ​

yaml
- 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:

yaml
# 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 ​

yaml
- 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 ​

yaml
- 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

Implementation Details ​

Template Processing Flow ​

  1. Detection: Templates containing {{.Request.}}, {{.Headers.}}, {{.MessageIndex}}, {{.Requests.}}, or {{.State}} are identified as dynamic
  2. Processing: Dynamic templates are processed at runtime, not at load time
  3. Execution: Go's text/template engine processes templates with custom functions
  4. 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 {{.Request.}} or {{.Headers.}}) continue to work unchanged
  • 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.stream and output.error/output.code set: stream messages are sent first, then the error is returned. If output.stream is empty, the error is returned immediately

Migration Guide ​

From Static to Dynamic Templates ​

Before (Static):

yaml
output:
  data:
    id: "123"
    name: "User 123"

After (Dynamic):

yaml
output:
  data:
    id: "{{.Request.id}}"
    name: "User {{.Request.id}}"

Testing Dynamic Templates ​

bash
# Start server
go run main.go examples/projects/calculator/service.proto --stub examples/projects/calculator

# Run tests
grpctestify examples/projects/calculator

Important 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.