docs: initialize stub-service repo with ADR-001
Architecture Decision Record for a Go-based HTTP stub/mock service (Mountebank alternative) with Web UI and LLM-powered config generation. Covers: component architecture, technology choices (Go + chi + Templ/htmx + SQLite), API design, data model, deployment model.
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
|||||||
|
# Binaries
|
||||||
|
stub-service
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
/bin/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Test
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Data
|
||||||
|
/data/
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# stub-service
|
||||||
|
|
||||||
|
Go-based HTTP stub/mock service with Web UI and LLM-powered configuration generation.
|
||||||
|
|
||||||
|
An alternative to [Mountebank](http://www.mbtest.org/) focused on simplicity, a modern web interface, and AI-assisted stub creation.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Planning phase** — see [ADR-001](docs/adr/adr-001.md) for the architecture decision record.
|
||||||
|
|
||||||
|
## Features (planned)
|
||||||
|
|
||||||
|
- HTTP/HTTPS stub server with rich request matching (equals, contains, regex, JSONPath, XPath)
|
||||||
|
- REST API for full CRUD management of imposters and stubs
|
||||||
|
- Web UI for browsing, creating, and editing mock configurations
|
||||||
|
- Real-time request log viewer
|
||||||
|
- LLM integration for natural-language stub generation
|
||||||
|
- Proxy mode with record/replay
|
||||||
|
- Response templating via Go templates
|
||||||
|
- File-based or embedded database persistence
|
||||||
|
- Docker-ready deployment
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Internal use.
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
# ADR-001: Architecture of stub-service — Go-based HTTP Stub/Mock Service
|
||||||
|
|
||||||
|
**Status:** Proposed
|
||||||
|
**Date:** 2026-05-07
|
||||||
|
**Authors:** backend-agent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Our development and QA workflows require a service-virtualization tool that can stand in for real HTTP services during integration testing, local development, and demo environments. [Mountebank](http://www.mbtest.org/) is the de-facto open-source option, but it presents several pain points for our team:
|
||||||
|
|
||||||
|
1. **Node.js runtime** — adds a runtime dependency our Go/Java-centric infrastructure doesn't otherwise need.
|
||||||
|
2. **No built-in Web UI** — managing imposters requires curl/Postman or third-party UIs.
|
||||||
|
3. **No AI-assisted authoring** — creating complex stubs is manual and error-prone.
|
||||||
|
4. **Limited extensibility** — behaviours and predicates are plugin-based in JavaScript, which is awkward to extend from our stack.
|
||||||
|
|
||||||
|
We need a lightweight, single-binary alternative written in Go that keeps Mountebank's core concepts (imposters, stubs, predicates, responses, proxies) while adding a web UI and LLM-powered stub generation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Build **stub-service** — a Go application that provides:
|
||||||
|
|
||||||
|
- An HTTP stub server with rich request matching
|
||||||
|
- A REST management API (CRUD for imposters/stubs)
|
||||||
|
- A web UI for visual management and real-time request logging
|
||||||
|
- An LLM adapter for natural-language stub authoring
|
||||||
|
- Proxy mode with record/replay capability
|
||||||
|
|
||||||
|
### High-Level Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ stub-service process │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||||
|
│ │ Management │ │ Web UI │ │ Stub Listeners │ │
|
||||||
|
│ │ REST API │ │ (embedded) │ │ (dynamic ports) │ │
|
||||||
|
│ │ :8080/api/v1 │ │ :8080/ui │ │ :N per imposter │ │
|
||||||
|
│ └──────┬───────┘ └──────┬───────┘ └───────────┬────────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Core Engine │ │
|
||||||
|
│ │ ┌─────────────┐ ┌──────────┐ ┌───────────┐ ┌────────┐ │ │
|
||||||
|
│ │ │ Imposter │ │ Matcher │ │ Response │ │ Proxy │ │ │
|
||||||
|
│ │ │ Manager │ │ Engine │ │ Renderer │ │ Module │ │ │
|
||||||
|
│ │ └─────────────┘ └──────────┘ └───────────┘ └────────┘ │ │
|
||||||
|
│ └──────────────────────────┬──────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────┼────────────────────┐ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ Stub Store │ │ Request Log │ │ LLM Adapter │ │
|
||||||
|
│ │ (SQLite / │ │ (ring buf) │ │ (Anthropic / │ │
|
||||||
|
│ │ bbolt) │ │ │ │ OpenAI) │ │
|
||||||
|
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Breakdown
|
||||||
|
|
||||||
|
#### 1. Management REST API (`internal/api`)
|
||||||
|
|
||||||
|
The control plane. All imposter/stub CRUD operations happen here.
|
||||||
|
|
||||||
|
- Listens on a single admin port (default `:8080`), path prefix `/api/v1`
|
||||||
|
- JSON request/response bodies
|
||||||
|
- Endpoints (see API Design section below)
|
||||||
|
- Provides SSE endpoint for real-time request log streaming
|
||||||
|
|
||||||
|
#### 2. Web UI (`internal/ui`)
|
||||||
|
|
||||||
|
An embedded single-page application served from the same admin port under `/ui`.
|
||||||
|
|
||||||
|
- **Technology:** [Templ](https://templ.guide/) templates + [htmx](https://htmx.org/) + minimal CSS (e.g., Pico CSS or similar classless framework)
|
||||||
|
- **Rationale:** No JavaScript build step, no npm dependency. Templ compiles to Go, htmx provides dynamic behavior via HTML attributes. The result is a server-rendered UI that feels interactive without a JS framework.
|
||||||
|
- **Pages:**
|
||||||
|
- Dashboard — list of all imposters with status, port, hit counts
|
||||||
|
- Imposter detail — list of stubs, predicates, responses, recent requests
|
||||||
|
- Stub editor — form-based creation/editing of stubs with predicate/response builders
|
||||||
|
- Request log — real-time log of incoming requests (SSE-powered)
|
||||||
|
- LLM assistant — text input for natural-language stub creation
|
||||||
|
|
||||||
|
#### 3. Stub Listeners (`internal/server`)
|
||||||
|
|
||||||
|
Each imposter binds to a dedicated port and handles incoming traffic.
|
||||||
|
|
||||||
|
- Dynamically started/stopped when imposters are created/deleted
|
||||||
|
- Each listener runs an HTTP handler that delegates to the Matcher Engine
|
||||||
|
- TLS support via optional per-imposter cert/key configuration
|
||||||
|
|
||||||
|
#### 4. Core Engine (`internal/engine`)
|
||||||
|
|
||||||
|
The domain logic of the application.
|
||||||
|
|
||||||
|
- **Imposter Manager** — lifecycle management of imposters (create, update, delete, start, stop). Coordinates listener creation and stub store persistence.
|
||||||
|
- **Matcher Engine** — evaluates incoming requests against stub predicates. Returns the first matching stub or a configurable default response. Supports predicate composition (`and`, `or`, `not`).
|
||||||
|
- **Response Renderer** — produces the HTTP response from a stub definition. Handles static bodies, Go template rendering (with request data as context), latency injection, and header manipulation.
|
||||||
|
- **Proxy Module** — forwards requests to a real upstream, optionally records the response as a new stub for future replay.
|
||||||
|
|
||||||
|
#### 5. Stub Store (`internal/store`)
|
||||||
|
|
||||||
|
Persistence layer for imposters, stubs, and request logs.
|
||||||
|
|
||||||
|
- **Primary:** SQLite via [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) (pure Go, no CGO) or [bbolt](https://github.com/etcd-io/bbolt) for a simpler key-value approach.
|
||||||
|
- **Decision:** SQLite preferred — it supports rich queries for filtering stubs and request logs, and the schema maps naturally to the relational data model.
|
||||||
|
- Migrations managed via embedded SQL files (`embed` package).
|
||||||
|
- Backup: the SQLite file can be copied while the service runs (WAL mode).
|
||||||
|
- Optional file-based export/import in JSON format for portability.
|
||||||
|
|
||||||
|
#### 6. LLM Adapter (`internal/llm`)
|
||||||
|
|
||||||
|
Translates natural-language descriptions into stub configurations.
|
||||||
|
|
||||||
|
- **Interface:** A `StubGenerator` interface that takes a text prompt and returns a structured stub definition.
|
||||||
|
- **Implementation:** HTTP client calling the Anthropic Messages API (Claude) or OpenAI Chat Completions API, configurable via environment variables.
|
||||||
|
- **Prompt strategy:** System prompt contains the stub JSON schema and examples. User message is the natural-language description. The LLM returns a JSON stub definition that is validated before being applied.
|
||||||
|
- **Flow:**
|
||||||
|
1. User types: *"Return 200 with a JSON user profile when GET /api/users/123 is called"*
|
||||||
|
2. LLM adapter sends the prompt with schema context
|
||||||
|
3. Response is parsed and validated against the stub schema
|
||||||
|
4. If valid, the stub is presented to the user for confirmation in the Web UI
|
||||||
|
5. On confirmation, the stub is saved via the Management API
|
||||||
|
|
||||||
|
#### 7. Request Log (`internal/log`)
|
||||||
|
|
||||||
|
In-memory ring buffer of recent requests per imposter, with optional persistence to SQLite.
|
||||||
|
|
||||||
|
- Stores: timestamp, method, path, headers, body (truncated), matched stub ID, response status, latency
|
||||||
|
- Queryable via Management API (filter by imposter, time range, match status)
|
||||||
|
- Streamed to the Web UI via SSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Technology Choices
|
||||||
|
|
||||||
|
| Concern | Choice | Rationale |
|
||||||
|
|---|---|---|
|
||||||
|
| Language | Go 1.23+ | Single binary, excellent HTTP stdlib, team expertise |
|
||||||
|
| HTTP framework | `net/http` (stdlib) + [chi](https://github.com/go-chi/chi) router | Minimal dependency, chi adds route params and middleware without abstracting stdlib |
|
||||||
|
| Web UI | Templ + htmx + Pico CSS | No JS build toolchain, server-rendered, interactive via htmx |
|
||||||
|
| Database | SQLite (modernc.org/sqlite) | Pure Go, zero CGO, rich SQL queries, single file |
|
||||||
|
| LLM client | Direct HTTP (Anthropic SDK or `net/http`) | Avoid heavy SDK deps; a thin wrapper suffices |
|
||||||
|
| Config | Environment variables + optional YAML file | 12-factor, simple for Docker |
|
||||||
|
| Logging | `log/slog` (stdlib) | Structured logging, zero deps |
|
||||||
|
| Testing | `testing` + [testify](https://github.com/stretchr/testify) | Assertions, standard tooling |
|
||||||
|
| TLS | `crypto/tls` (stdlib) | Per-imposter certs for HTTPS stubs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API Design Overview
|
||||||
|
|
||||||
|
Base URL: `http://localhost:8080/api/v1`
|
||||||
|
|
||||||
|
#### Imposters
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/imposters` | List all imposters |
|
||||||
|
| `POST` | `/imposters` | Create a new imposter |
|
||||||
|
| `GET` | `/imposters/{id}` | Get imposter details |
|
||||||
|
| `PUT` | `/imposters/{id}` | Update imposter |
|
||||||
|
| `DELETE` | `/imposters/{id}` | Delete imposter and stop listener |
|
||||||
|
| `DELETE` | `/imposters` | Delete all imposters |
|
||||||
|
| `POST` | `/imposters/{id}/start` | Start the stub listener |
|
||||||
|
| `POST` | `/imposters/{id}/stop` | Stop the stub listener |
|
||||||
|
|
||||||
|
#### Stubs (nested under imposter)
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/imposters/{id}/stubs` | List stubs for an imposter |
|
||||||
|
| `POST` | `/imposters/{id}/stubs` | Add a stub |
|
||||||
|
| `PUT` | `/imposters/{id}/stubs/{stubId}` | Update a stub |
|
||||||
|
| `DELETE` | `/imposters/{id}/stubs/{stubId}` | Delete a stub |
|
||||||
|
|
||||||
|
#### Request Log
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/imposters/{id}/requests` | Get request log (paginated) |
|
||||||
|
| `GET` | `/imposters/{id}/requests/stream` | SSE stream of live requests |
|
||||||
|
| `DELETE` | `/imposters/{id}/requests` | Clear request log |
|
||||||
|
|
||||||
|
#### LLM
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST` | `/generate` | Generate stub config from natural-language prompt |
|
||||||
|
|
||||||
|
#### System
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/health` | Health check |
|
||||||
|
| `GET` | `/config` | Current configuration (redacted) |
|
||||||
|
| `POST` | `/import` | Import imposters from JSON file |
|
||||||
|
| `GET` | `/export` | Export all imposters as JSON |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Data Model
|
||||||
|
|
||||||
|
#### Imposter
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "User Service Mock",
|
||||||
|
"protocol": "http",
|
||||||
|
"port": 4545,
|
||||||
|
"tls": {
|
||||||
|
"cert": "...",
|
||||||
|
"key": "..."
|
||||||
|
},
|
||||||
|
"default_response": {
|
||||||
|
"status": 404,
|
||||||
|
"headers": {"Content-Type": "application/json"},
|
||||||
|
"body": "{\"error\": \"no matching stub\"}"
|
||||||
|
},
|
||||||
|
"stubs": [ ... ],
|
||||||
|
"state": "running",
|
||||||
|
"created_at": "2026-05-07T10:00:00Z",
|
||||||
|
"request_count": 142
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stub
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"imposter_id": "uuid",
|
||||||
|
"priority": 1,
|
||||||
|
"predicates": [ ... ],
|
||||||
|
"response": { ... },
|
||||||
|
"hit_count": 37,
|
||||||
|
"created_at": "2026-05-07T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Predicate
|
||||||
|
|
||||||
|
Predicates are composable. Each predicate has a `type` and type-specific fields:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "equals",
|
||||||
|
"field": "path",
|
||||||
|
"value": "/api/users/123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "regex",
|
||||||
|
"field": "path",
|
||||||
|
"value": "^/api/users/\\d+$"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "jsonpath",
|
||||||
|
"expression": "$.order.items[?(@.quantity > 5)]",
|
||||||
|
"exists": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "and",
|
||||||
|
"predicates": [
|
||||||
|
{"type": "equals", "field": "method", "value": "POST"},
|
||||||
|
{"type": "contains", "field": "body", "value": "\"action\":\"create\""}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported predicate types:
|
||||||
|
- `equals` — exact match on method, path, header, query param, or body
|
||||||
|
- `contains` — substring match
|
||||||
|
- `regex` — regular expression match
|
||||||
|
- `jsonpath` — JSONPath expression evaluation
|
||||||
|
- `xpath` — XPath expression evaluation (for XML bodies)
|
||||||
|
- `and` — all child predicates must match
|
||||||
|
- `or` — at least one child predicate must match
|
||||||
|
- `not` — invert a child predicate
|
||||||
|
|
||||||
|
Fields available for matching: `method`, `path`, `query.<param>`, `header.<name>`, `body`
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"body": "{\"id\": 123, \"name\": \"Alice\"}",
|
||||||
|
"template": false,
|
||||||
|
"latency_ms": 0,
|
||||||
|
"proxy": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `template: true`, the `body` is rendered as a Go template with the request context:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"echo": "You requested {{.Request.Path}} with method {{.Request.Method}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Proxy Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"proxy": {
|
||||||
|
"to": "https://real-service.example.com",
|
||||||
|
"mode": "record",
|
||||||
|
"add_wait_behavior": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Proxy modes:
|
||||||
|
- `passthrough` — forward and return, do not record
|
||||||
|
- `record` — forward, return, and save as a new stub for future replay
|
||||||
|
- `replay` — use previously recorded response if available, otherwise forward
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SQLite Schema (initial)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE imposters (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
protocol TEXT NOT NULL DEFAULT 'http',
|
||||||
|
port INTEGER NOT NULL UNIQUE,
|
||||||
|
tls_cert TEXT,
|
||||||
|
tls_key TEXT,
|
||||||
|
default_response_json TEXT,
|
||||||
|
state TEXT NOT NULL DEFAULT 'stopped',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE stubs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
imposter_id TEXT NOT NULL REFERENCES imposters(id) ON DELETE CASCADE,
|
||||||
|
priority INTEGER NOT NULL DEFAULT 0,
|
||||||
|
predicates_json TEXT NOT NULL,
|
||||||
|
response_json TEXT NOT NULL,
|
||||||
|
hit_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_stubs_imposter ON stubs(imposter_id, priority);
|
||||||
|
|
||||||
|
CREATE TABLE request_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
imposter_id TEXT NOT NULL REFERENCES imposters(id) ON DELETE CASCADE,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
method TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
headers_json TEXT,
|
||||||
|
body TEXT,
|
||||||
|
matched_stub_id TEXT,
|
||||||
|
response_status INTEGER,
|
||||||
|
latency_ms INTEGER,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_request_log_imposter_ts ON request_log(imposter_id, timestamp DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Project Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
stub-service/
|
||||||
|
├── cmd/
|
||||||
|
│ └── stub-service/
|
||||||
|
│ └── main.go # Entry point, config loading, wiring
|
||||||
|
├── internal/
|
||||||
|
│ ├── api/ # Management REST API handlers
|
||||||
|
│ │ ├── handler.go
|
||||||
|
│ │ ├── imposter.go
|
||||||
|
│ │ ├── stub.go
|
||||||
|
│ │ ├── requestlog.go
|
||||||
|
│ │ ├── generate.go # LLM generation endpoint
|
||||||
|
│ │ └── middleware.go
|
||||||
|
│ ├── engine/ # Core domain logic
|
||||||
|
│ │ ├── imposter.go # Imposter manager
|
||||||
|
│ │ ├── matcher.go # Predicate matching engine
|
||||||
|
│ │ ├── renderer.go # Response rendering (static, template)
|
||||||
|
│ │ └── proxy.go # Proxy/record/replay
|
||||||
|
│ ├── model/ # Domain types
|
||||||
|
│ │ ├── imposter.go
|
||||||
|
│ │ ├── stub.go
|
||||||
|
│ │ ├── predicate.go
|
||||||
|
│ │ └── response.go
|
||||||
|
│ ├── store/ # Persistence
|
||||||
|
│ │ ├── store.go # Store interface
|
||||||
|
│ │ ├── sqlite.go # SQLite implementation
|
||||||
|
│ │ └── migrations/ # Embedded SQL migrations
|
||||||
|
│ │ └── 001_init.sql
|
||||||
|
│ ├── server/ # Dynamic stub HTTP listeners
|
||||||
|
│ │ └── listener.go
|
||||||
|
│ ├── llm/ # LLM integration
|
||||||
|
│ │ ├── generator.go # StubGenerator interface
|
||||||
|
│ │ ├── anthropic.go # Anthropic/Claude implementation
|
||||||
|
│ │ └── openai.go # OpenAI implementation
|
||||||
|
│ ├── ui/ # Web UI
|
||||||
|
│ │ ├── templates/ # Templ files
|
||||||
|
│ │ ├── static/ # CSS, minimal JS (htmx)
|
||||||
|
│ │ └── handler.go # UI route handlers
|
||||||
|
│ └── config/ # Configuration loading
|
||||||
|
│ └── config.go
|
||||||
|
├── docs/
|
||||||
|
│ └── adr/
|
||||||
|
│ └── adr-001.md # This document
|
||||||
|
├── go.mod
|
||||||
|
├── go.sum
|
||||||
|
├── Dockerfile
|
||||||
|
├── docker-compose.yaml
|
||||||
|
├── Makefile
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Deployment Model
|
||||||
|
|
||||||
|
**Docker (primary):**
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Build stage
|
||||||
|
FROM golang:1.23-alpine AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -o /stub-service ./cmd/stub-service
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
COPY --from=build /stub-service /usr/local/bin/stub-service
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME /data
|
||||||
|
ENTRYPOINT ["stub-service"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**docker-compose.yaml (local dev):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
stub-service:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080" # Admin API + Web UI
|
||||||
|
- "4545-4600:4545-4600" # Range for stub listeners
|
||||||
|
volumes:
|
||||||
|
- stub-data:/data
|
||||||
|
environment:
|
||||||
|
STUB_ADMIN_PORT: "8080"
|
||||||
|
STUB_DATA_DIR: "/data"
|
||||||
|
STUB_LOG_LEVEL: "debug"
|
||||||
|
LLM_PROVIDER: "anthropic"
|
||||||
|
LLM_API_KEY: "${LLM_API_KEY}"
|
||||||
|
LLM_MODEL: "claude-sonnet-4-6"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
stub-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration (environment variables):**
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `STUB_ADMIN_PORT` | `8080` | Admin API and Web UI port |
|
||||||
|
| `STUB_DATA_DIR` | `./data` | SQLite database directory |
|
||||||
|
| `STUB_LOG_LEVEL` | `info` | Log level (debug, info, warn, error) |
|
||||||
|
| `STUB_PORT_RANGE_START` | `4545` | Start of port range for stub listeners |
|
||||||
|
| `STUB_PORT_RANGE_END` | `4600` | End of port range for stub listeners |
|
||||||
|
| `LLM_PROVIDER` | (none) | LLM provider: `anthropic` or `openai` |
|
||||||
|
| `LLM_API_KEY` | (none) | LLM API key |
|
||||||
|
| `LLM_MODEL` | `claude-sonnet-4-6` | Model to use for stub generation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- **Single binary** — no runtime dependencies beyond the OS; trivial to deploy.
|
||||||
|
- **Embedded Web UI** — no separate frontend build/deploy pipeline; Templ + htmx keeps it all in Go.
|
||||||
|
- **SQLite** — zero operational overhead, no external database to manage, easy backup (copy a file).
|
||||||
|
- **LLM integration** — significantly lowers the barrier for creating complex stubs, especially for QA engineers unfamiliar with JSONPath/regex predicates.
|
||||||
|
- **Mountebank-compatible concepts** — teams familiar with Mountebank can transfer their mental model (imposters, stubs, predicates).
|
||||||
|
- **Port-per-imposter isolation** — each mock service gets its own port, matching how real services are addressed.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- **Port range management** — dynamic port allocation requires firewall/Docker port-range configuration.
|
||||||
|
- **SQLite single-writer** — under very heavy concurrent management API load, SQLite's single-writer lock could be a bottleneck. Acceptable for a testing/dev tool.
|
||||||
|
- **LLM dependency** — the generation feature requires external API access and incurs cost. It is optional — the service works fully without it.
|
||||||
|
- **No multi-protocol support initially** — only HTTP/HTTPS. TCP/SMTP stubs (which Mountebank supports) are deferred to a future iteration.
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
- **Templ maturity** — Templ is relatively new. Mitigation: the UI is simple enough that switching to `html/template` is feasible with moderate effort.
|
||||||
|
- **JSONPath/XPath complexity** — Go's ecosystem for JSONPath and XPath is less mature than Node.js. Mitigation: use well-maintained libraries ([github.com/PaesslerAG/jsonpath](https://github.com/PaesslerAG/jsonpath), [github.com/antchfx/xmlquery](https://github.com/antchfx/xmlquery)) and test thoroughly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Mountebank](http://www.mbtest.org/) — the inspiration
|
||||||
|
- [chi router](https://github.com/go-chi/chi)
|
||||||
|
- [Templ](https://templ.guide/) — Go HTML templating
|
||||||
|
- [htmx](https://htmx.org/) — HTML-driven interactivity
|
||||||
|
- [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) — pure Go SQLite
|
||||||
Reference in New Issue
Block a user