Skip to content

[Feature] Dynamic response templating ({{path.id}}, {{body.field}}, etc.) #18

@ragilhadi

Description

@ragilhadi

Summary

Mock responses are currently fully static JSON blobs. Add lightweight response templating so mocks can echo back data from the incoming request — path parameters, query params, headers, or body fields — without writing custom code.

This dramatically reduces the number of mock files needed and makes mocks feel more like real APIs.

Motivation

Common use cases that are impossible today:

  • Echo back the ID from the URL: GET /users/42{ "id": 42 }
  • Return the submitted username in the response body
  • Include a correlation ID from the request header in the response
  • Build a Location header value that references the created resource

Proposed Template Syntax

Use {{ }} double-brace syntax inside any string value in the response JSON:

Template Source
{{path.id}} Named path parameter :id
{{query.page}} URL query parameter ?page=2
{{header.x-request-id}} Request header value
{{body.username}} Top-level JSON body field
{{body.user.email}} Nested JSON body field (dot notation)

Example

{
  "method": "POST",
  "path": "/users",
  "status": 201,
  "response": {
    "id": 99,
    "username": "{{body.username}}",
    "email": "{{body.email}}",
    "created_by": "{{header.x-actor}}",
    "self_url": "/users/99"
  }
}

Request:

curl -X POST http://localhost:8080/users \
  -H "X-Actor: admin" \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","email":"alice@example.com"}'

Response:

{
  "id": 99,
  "username": "alice",
  "email": "alice@example.com",
  "created_by": "admin",
  "self_url": "/users/99"
}

Implementation Plan

src/handler.rs

After matching a mock, before serializing the response, walk the serde_json::Value tree and interpolate:

fn render_template(value: &serde_json::Value, ctx: &RequestContext) -> serde_json::Value {
    match value {
        serde_json::Value::String(s) => {
            serde_json::Value::String(interpolate(s, ctx))
        }
        serde_json::Value::Object(map) => {
            serde_json::Value::Object(map.iter()
                .map(|(k, v)| (k.clone(), render_template(v, ctx)))
                .collect())
        }
        serde_json::Value::Array(arr) => {
            serde_json::Value::Array(arr.iter().map(|v| render_template(v, ctx)).collect())
        }
        other => other.clone(),
    }
}

interpolate replaces all {{source.key}} occurrences using a regex scan.

Dependency on Issue #4 (Path Parameters)

{{path.X}} requires path params to be captured and stored in RequestContext. The other sources (query, header, body) are already available.

Acceptance Criteria

  • {{query.X}}, {{header.X}}, {{body.X}} interpolation working
  • {{path.X}} working (depends on path param feature)
  • Unknown/missing template variables replaced with empty string (no panic)
  • Nested {{body.a.b.c}} dot-notation for JSON body fields
  • No-op when response contains no template expressions (zero overhead)
  • Unit tests covering each source type and missing key behavior
  • README updated with templating documentation and examples

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or requesthandlerRequest handling

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions