On this page
- The Multi-Protocol Challenge
- Protocol Abstraction: The Core Architecture
- GraphQL Implementation: Schema-Based Mocking
- Automatic Resolver Generation
- GraphQL Query Execution
- WebSocket Implementation: Connection Management
- WebSocket Message Replay
- Protocol Abstraction Interface
- Shared Infrastructure: Data Generation and State
- GraphQL Playground Integration
- WebSocket Templating
- Performance Considerations
- The Bottom Line
🔗The Multi-Protocol Challenge
Modern applications don't use a single protocol. They use REST APIs, GraphQL queries, WebSocket connections, gRPC services, and raw TCP. Each protocol has different semantics, different connection models, and different mocking requirements.
The challenge isn't just supporting multiple protocols—it's supporting them in a unified way that doesn't duplicate logic. You don't want separate code paths for HTTP mocking, GraphQL mocking, and WebSocket mocking. You want a unified architecture where protocol-specific handlers share common infrastructure.
MockForge v0.1.3 introduced GraphQL and WebSocket support, building on the existing HTTP and gRPC infrastructure. This post covers the technical implementation of how protocol abstraction enables multi-protocol mocking without code duplication.
🔗Protocol Abstraction: The Core Architecture
The protocol abstraction layer is the key to supporting multiple protocols:
graph TD
A[Protocol Abstraction] -->|HTTP| B[HTTP Handler]
A -->|GraphQL| C[GraphQL Handler]
A -->|WebSocket| D[WebSocket Handler]
A -->|gRPC| E[gRPC Handler]
A -->|TCP| F[TCP Handler]
G[Data Generation] -->|Schema| A
H[Response Selection] -->|Examples| A
I[State Management] -->|Shared State| A
The abstraction layer defines a unified interface that all protocols implement. This means data generation, response selection, and state management work the same way regardless of protocol.
🔗GraphQL Implementation: Schema-Based Mocking
GraphQL mocking is fundamentally different from REST. Instead of matching URLs and HTTP methods, you match GraphQL queries and mutations. The implementation is built around GraphQL schemas:
pub struct GraphQLHandler {
schema: GraphQLSchema,
resolvers: ResolverRegistry,
executor: GraphQLExecutor,
}
impl GraphQLHandler {
pub async fn handle_query(&self, query: &str, variables: &Value) -> Result<Value> {
// Parse and validate query
let document = parse_query(query)?;
validate_query(&document, &self.schema)?;
// Execute query with mock resolvers
let result = self.executor.execute(
&document,
&variables,
&self.schema,
&self.resolvers,
).await?;
Ok(result)
}
}
The GraphQL handler:
- Parses the incoming query
- Validates it against the schema
- Executes it using mock resolvers
- Returns the result
🔗Automatic Resolver Generation
GraphQL resolvers are generated automatically from the schema. The generator analyzes field types and generates appropriate mock data:
pub struct ResolverGenerator;
impl ResolverGenerator {
fn generate_resolver(field: &Field, schema: &GraphQLSchema) -> Resolver {
match field.type_.as_str() {
"ID" => Resolver::scalar(|_| generate_uuid()),
"String" => Resolver::scalar(|_| generate_string()),
"Int" => Resolver::scalar(|_| generate_int()),
"Boolean" => Resolver::scalar(|_| generate_bool()),
"User" => Resolver::object(|_| {
generate_user_object(schema)
}),
"[User]" => Resolver::list(|_| {
(0..10).map(|_| generate_user_object(schema)).collect()
}),
_ => Resolver::default(),
}
}
}
The resolver generator:
- Analyzes field types from the schema
- Generates appropriate mock data for each type
- Maintains referential integrity (e.g.,
user.postsreferences the same user) - Supports nested queries and relationships
🔗GraphQL Query Execution
The query executor processes GraphQL queries and resolves fields:
pub struct GraphQLExecutor {
schema: GraphQLSchema,
resolvers: ResolverRegistry,
}
impl GraphQLExecutor {
pub async fn execute(
&self,
query: &Document,
variables: &Value,
root: &Value,
) -> Result<Value> {
// Extract operation (query or mutation)
let operation = query.operation()?;
// Execute selection set
let result = self.execute_selection_set(
&operation.selection_set,
&root,
variables,
).await?;
Ok(result)
}
async fn execute_selection_set(
&self,
selection_set: &SelectionSet,
parent: &Value,
variables: &Value,
) -> Result<Value> {
let mut result = json!({});
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => {
let field_name = &field.name;
let resolver = self.resolvers.get(field_name)?;
// Resolve field value
let value = resolver.resolve(parent, variables).await?;
// Handle nested selections
if let Some(nested_selection) = &field.selection_set {
let nested_value = self.execute_selection_set(
nested_selection,
&value,
variables,
).await?;
result[field_name] = nested_value;
} else {
result[field_name] = value;
}
}
Selection::FragmentSpread(_) => {
// Handle fragment spreads
}
Selection::InlineFragment(_) => {
// Handle inline fragments
}
}
}
Ok(result)
}
}
The executor handles:
- Operation parsing (queries, mutations)
- Field resolution with mock resolvers
- Nested selections (relationships)
- Fragment spreads and inline fragments
- Variable substitution
🔗WebSocket Implementation: Connection Management
WebSocket mocking is different from HTTP. It's not request/response—it's a persistent connection with bidirectional message flow. The implementation handles connection lifecycle and message routing:
pub struct WebSocketHandler {
connections: Arc<DashMap<String, WebSocketConnection>>,
message_handlers: HandlerRegistry,
}
impl WebSocketHandler {
pub async fn handle_connection(
&self,
stream: TcpStream,
addr: SocketAddr,
) -> Result<()> {
// Upgrade HTTP connection to WebSocket
let ws_stream = accept_async(stream).await?;
let (tx, rx) = ws_stream.split();
let connection_id = generate_connection_id();
let connection = WebSocketConnection {
id: connection_id.clone(),
addr,
tx,
rx,
state: Arc::new(RwLock::new(ConnectionState::default())),
};
// Store connection
self.connections.insert(connection_id.clone(), connection.clone());
// Handle messages
self.handle_messages(connection).await?;
Ok(())
}
async fn handle_messages(&self, connection: WebSocketConnection) -> Result<()> {
while let Some(msg) = connection.rx.next().await {
match msg {
Ok(Message::Text(text)) => {
// Parse message
let message: WebSocketMessage = serde_json::from_str(&text)?;
// Route to handler
let handler = self.message_handlers.get(&message.type_)?;
let response = handler.handle(&message, &connection.state).await?;
// Send response
connection.tx.send(Message::Text(
serde_json::to_string(&response)?
)).await?;
}
Ok(Message::Close(_)) => {
// Connection closed
break;
}
Err(e) => {
error!("WebSocket error: {}", e);
break;
}
_ => {}
}
}
// Clean up connection
self.connections.remove(&connection.id);
Ok(())
}
}
The WebSocket handler:
- Manages connection lifecycle (connect, message, disconnect)
- Routes messages to appropriate handlers
- Maintains connection state
- Handles connection cleanup
🔗WebSocket Message Replay
WebSocket connections can be recorded and replayed, just like HTTP requests:
pub struct WebSocketRecorder {
database: Arc<RecorderDatabase>,
}
impl WebSocketRecorder {
pub async fn record_message(
&self,
connection_id: &str,
message: &WebSocketMessage,
response: &WebSocketMessage,
) -> Result<()> {
// Store message exchange
self.database.record_websocket_exchange(
connection_id,
message,
response,
).await?;
Ok(())
}
pub async fn replay_connection(
&self,
connection_id: &str,
) -> Result<Vec<WebSocketMessage>> {
// Load recorded messages for this connection
let exchanges = self.database.get_websocket_exchanges(connection_id).await?;
// Replay messages in order
let messages: Vec<WebSocketMessage> = exchanges
.into_iter()
.map(|exchange| exchange.response)
.collect();
Ok(messages)
}
}
This enables deterministic testing of WebSocket-based features by replaying recorded message sequences.
🔗Protocol Abstraction Interface
The protocol abstraction defines a unified interface:
pub trait ProtocolHandler {
type Request;
type Response;
async fn handle(&self, request: Self::Request) -> Result<Self::Response>;
fn get_schema(&self) -> &dyn ProtocolSchema;
fn supports_feature(&self, feature: ProtocolFeature) -> bool;
}
// HTTP implementation
impl ProtocolHandler for HttpHandler {
type Request = http::Request<Body>;
type Response = http::Response<Body>;
async fn handle(&self, request: Self::Request) -> Result<Self::Response> {
// HTTP-specific handling
}
}
// GraphQL implementation
impl ProtocolHandler for GraphQLHandler {
type Request = GraphQLRequest;
type Response = GraphQLResponse;
async fn handle(&self, request: Self::Request) -> Result<Self::Response> {
// GraphQL-specific handling
}
}
// WebSocket implementation
impl ProtocolHandler for WebSocketHandler {
type Request = WebSocketMessage;
type Response = WebSocketMessage;
async fn handle(&self, request: Self::Request) -> Result<Self::Response> {
// WebSocket-specific handling
}
}
This unified interface means:
- Data generation works across all protocols
- Response selection works the same way
- State management is shared
- Observability is consistent
🔗Shared Infrastructure: Data Generation and State
Protocol-specific handlers share common infrastructure:
pub struct MockServer {
// Shared across all protocols
data_generator: Arc<DataGenerator>,
state_manager: Arc<StateManager>,
response_selector: Arc<ResponseSelector>,
// Protocol-specific handlers
http_handler: HttpHandler,
graphql_handler: GraphQLHandler,
websocket_handler: WebSocketHandler,
}
impl MockServer {
pub async fn handle_http(&self, req: http::Request<Body>) -> http::Response<Body> {
// Use shared data generator
let mock_data = self.data_generator.generate(&req.schema).await?;
// ...
}
pub async fn handle_graphql(&self, req: GraphQLRequest) -> GraphQLResponse {
// Use shared data generator
let mock_data = self.data_generator.generate(&req.schema).await?;
// ...
}
pub async fn handle_websocket(&self, msg: WebSocketMessage) -> WebSocketMessage {
// Use shared data generator
let mock_data = self.data_generator.generate(&msg.schema).await?;
// ...
}
}
This shared infrastructure means:
- LLM-powered data generation works for all protocols
- State management is consistent across protocols
- Response selection logic is shared
- Observability is unified
🔗GraphQL Playground Integration
GraphQL Playground provides an interactive UI for testing GraphQL queries:
pub fn create_graphql_router() -> Router {
Router::new()
.route("/graphql", post(graphql_handler))
.route("/playground", get(playground_handler))
.route("/graphql/ws", get(graphql_subscription_handler))
}
The playground handler serves the GraphQL Playground UI, which provides:
- Schema explorer
- Query editor with auto-complete
- Response viewer
- Request history
This makes it easy to test GraphQL queries interactively without writing client code.
🔗WebSocket Templating
WebSocket messages can use templates, just like HTTP responses:
pub struct WebSocketTemplateEngine;
impl WebSocketTemplateEngine {
fn render_template(
&self,
template: &str,
context: &TemplateContext,
) -> Result<String> {
// Support for template variables
// {{timestamp}} - Current timestamp
// {{uuid}} - Generate UUID
// {{faker.email}} - Generate fake email
// {{state.connection_id}} - Connection state
// ...
}
}
This enables dynamic WebSocket responses that can simulate realistic server behavior.
🔗Performance Considerations
Multi-protocol support doesn't mean sacrificing performance. The architecture is designed for efficiency:
- Protocol-specific handlers: Each protocol has optimized handlers
- Shared infrastructure: Common code is shared, not duplicated
- Async I/O: All protocols use async/await for non-blocking I/O
- Connection pooling: WebSocket connections are efficiently managed
The result is a mock server that can handle multiple protocols simultaneously without performance degradation.
🔗The Bottom Line
GraphQL and WebSocket support in MockForge v0.1.3 demonstrate the power of protocol abstraction. By building on a unified architecture, adding new protocols doesn't require duplicating infrastructure—it just requires implementing the protocol-specific handler interface.
The architecture enables:
- Multi-protocol mocking in a single server
- Shared data generation and state management
- Consistent observability across protocols
- Easy addition of new protocols
For teams building modern applications that use multiple protocols, this architecture means you can mock your entire stack—REST APIs, GraphQL queries, WebSocket connections, gRPC services—with a single tool. That's the value of protocol abstraction.