# spec-mock spec-mock is a pure Rust spec-driven mock runtime supporting HTTP REST (OpenAPI 3.0.x/3.1.x), WebSocket (AsyncAPI 2.x/3.x), and gRPC (Protobuf over HTTP/2). It provides both a standalone CLI for development and testing environments, and a native Rust SDK for embedding directly into `#[tokio::test]` integration tests. The runtime validates incoming requests against spec schemas, generates deterministic mock responses using a priority chain (example > examples[0] > default > schema faker), and returns RFC 7807 Problem Details JSON errors for validation failures. The project achieves feature parity with Stoplight Prism for OpenAPI mocking while adding unique capabilities: AsyncAPI WebSocket mocking, gRPC Protobuf mocking, multi-file `$ref` resolution, `Prefer` header support for response selection, content negotiation via `Accept` header, proxy mode with response validation, and a configurable request body size limit. The faker engine supports regex `pattern` generation, all standard JSON Schema `format` values, `discriminator` + `mapping` for polymorphic schemas, and `additionalProperties` generation. ## CLI: Start OpenAPI HTTP Mock Server Launch a mock server from an OpenAPI specification file. The server validates requests against the spec and generates mock responses based on schema definitions or examples. ```bash # Start mock server with OpenAPI spec cargo run -p spec-mock -- serve \ --openapi docs/specs/pets.openapi.yaml \ --http-addr 127.0.0.1:4010 \ --seed 42 # Test a valid request curl http://127.0.0.1:4010/pets/1 # Response: {"id": 42, "name": "bqxnvlms"} # Test an invalid request (non-integer ID) curl -i http://127.0.0.1:4010/pets/abc # HTTP/1.1 400 Bad Request # Content-Type: application/problem+json # { # "type": "about:blank", # "title": "Bad Request", # "status": 400, # "detail": "Request validation failed", # "errors": [ # { # "instance_pointer": "/id", # "schema_pointer": "/minimum", # "keyword": "minimum", # "message": "..." # } # ] # } ``` ## CLI: Prefer Header for Response Selection Use the `Prefer` header to select specific response status codes, named examples, or force dynamic faker-generated responses. ```bash # Select a specific HTTP status code (e.g., 404) curl -H "Prefer: code=404" http://127.0.0.1:4010/pets/1 # Select a named example from the OpenAPI spec curl -H "Prefer: example=whiskers" http://127.0.0.1:4010/pets/1 # Force dynamic (faker-generated) response even when static example exists curl -H "Prefer: dynamic=true" http://127.0.0.1:4010/pets/1 # Combine multiple preferences curl -H "Prefer: code=200, example=fluffy" http://127.0.0.1:4010/pets/1 ``` ## CLI: Start AsyncAPI WebSocket Mock Server Launch a WebSocket mock server from an AsyncAPI specification. Supports both AsyncAPI v2.x and v3.x with multi-path routing. ```bash # Start WebSocket mock server cargo run -p spec-mock -- serve \ --asyncapi docs/specs/chat.asyncapi.yaml \ --http-addr 127.0.0.1:4011 # Connect via WebSocket: ws://127.0.0.1:4011/ws # Send message with explicit channel envelope # {"channel": "chat.send", "payload": {"room": "general", "text": "Hello!"}} # Alternative alias envelope format # {"topic": "chat.send", "data": {"room": "general", "text": "Hello!"}} # Response (from subscribe message example): # {"ok": true, "event": "message.accepted"} ``` ## CLI: Start Protobuf gRPC Mock Server Launch a gRPC mock server from Protobuf definitions. Uses tonic for standard HTTP/2 transport with proper trailers, supporting unary and server-streaming RPCs. ```bash # Start gRPC mock server (also starts HTTP server) cargo run -p spec-mock -- serve \ --proto docs/specs/greeter.proto \ --grpc-addr 127.0.0.1:5010 \ --http-addr 127.0.0.1:4012 # Test with grpcurl grpcurl -plaintext \ -import-path docs/specs \ -proto greeter.proto \ -d '{"name": "alice"}' \ 127.0.0.1:5010 mock.Greeter/SayHello # Response: {"message": "bqxnvlms"} ``` ## CLI: Proxy Mode with Response Validation Run in proxy mode to forward requests to an upstream server while validating responses against the OpenAPI schema. Returns 502 with validation errors if the upstream response violates the spec. ```bash # Start in proxy mode cargo run -p spec-mock -- serve \ --openapi docs/specs/pets.openapi.yaml \ --mode proxy \ --upstream http://127.0.0.1:8080 \ --http-addr 127.0.0.1:4010 # Request is forwarded to upstream; response is validated against OpenAPI schema curl http://127.0.0.1:4010/pets/1 # If upstream returns invalid response (e.g., {"id": "string"} instead of integer): # HTTP/1.1 502 Bad Gateway # Content-Type: application/problem+json # {"type": "about:blank", "title": "Bad Gateway", "status": 502, ...} ``` ## Rust SDK: MockServer Embedded in Tests Embed the mock server directly in `#[tokio::test]` for integration testing. The server runs in-process with automatic port allocation. ```rust use specmock_sdk::MockServer; #[tokio::test] async fn test_pet_api() -> Result<(), Box> { // Start embedded mock server let server = MockServer::builder() .openapi("docs/specs/pets.openapi.yaml") .seed(42) // Deterministic responses .start() .await?; // Make HTTP request to mock server let response = hpx::get(format!("{}/pets/1", server.http_base_url())) .send() .await?; assert_eq!(response.status().as_u16(), 200); let body: serde_json::Value = response.json().await?; assert!(body["id"].is_i64()); assert!(body["name"].is_string()); // Graceful shutdown server.shutdown().await; Ok(()) } ``` ## Rust SDK: MockServerBuilder Configuration Configure the mock server with various options including spec paths, seed values, network addresses, runtime mode, and body size limits. ```rust use std::net::SocketAddr; use specmock_core::MockMode; use specmock_sdk::MockServer; #[tokio::test] async fn test_full_configuration() -> Result<(), Box> { let server = MockServer::builder() // Spec files (at least one required) .openapi("api.openapi.yaml") .asyncapi("events.asyncapi.yaml") .proto("service.proto") // Deterministic seed for reproducible responses .seed(12345) // Runtime mode: Mock (default) or Proxy .mode(MockMode::Mock) // Upstream URL (required for proxy mode) .upstream("http://localhost:8080") // Bind addresses (0 = auto-assign) .http_addr(SocketAddr::from(([127, 0, 0, 1], 0))) .grpc_addr(SocketAddr::from(([127, 0, 0, 1], 0))) // Max request body size (default: 10 MiB) .max_body_size(5 * 1024 * 1024) .start() .await?; // Access bound addresses println!("HTTP: {}", server.http_base_url()); // http://127.0.0.1:xxxxx println!("WebSocket: {}", server.ws_url()); // ws://127.0.0.1:xxxxx/ws if let Some(grpc) = server.grpc_addr() { println!("gRPC: {}", grpc); // 127.0.0.1:yyyyy } server.shutdown().await; Ok(()) } ``` ## Rust SDK: Process Mode Server Start the mock server as an external process instead of embedding it. Useful for testing against the compiled binary. ```rust use std::path::Path; use specmock_sdk::MockServer; #[tokio::main] async fn main() -> Result<(), Box> { // Start as external process let mut server = MockServer::builder() .openapi("docs/specs/pets.openapi.yaml") .seed(7) .start_process_with_bin(Path::new("target/debug/spec-mock")) .await?; let response = hpx::get(format!("{}/pets/1", server.http_base_url())) .send() .await?; assert_eq!(response.status().as_u16(), 200); // ProcessMockServer requires explicit shutdown (kills the process) server.shutdown()?; Ok(()) } ``` ## Core: JSON Schema Validation Validate JSON instances against JSON schemas with detailed error reporting including JSON pointers. ```rust use serde_json::json; use specmock_core::validate::validate_instance; fn main() -> Result<(), Box> { let schema = json!({ "type": "object", "required": ["id", "name"], "properties": { "id": {"type": "integer", "minimum": 1}, "name": {"type": "string", "minLength": 1} } }); // Valid instance let valid = json!({"id": 42, "name": "Fluffy"}); let issues = validate_instance(&schema, &valid)?; assert!(issues.is_empty()); // Invalid instance let invalid = json!({"id": -1, "name": ""}); let issues = validate_instance(&schema, &invalid)?; for issue in &issues { println!("Path: {}", issue.instance_pointer); // e.g., "/id" println!("Keyword: {}", issue.keyword); // e.g., "minimum" println!("Message: {}", issue.message); } // Output: // Path: /id // Keyword: minimum // Message: -1 is less than the minimum of 1 Ok(()) } ``` ## Core: Deterministic JSON Faker Generate deterministic mock JSON data from schemas. Supports the full JSON Schema specification including patterns, formats, discriminators, and additional properties. ```rust use serde_json::json; use specmock_core::faker::generate_json_value; fn main() -> Result<(), Box> { // Basic schema with example (highest priority) let schema = json!({ "type": "object", "example": {"id": 1, "name": "Whiskers"}, "properties": { "id": {"type": "integer"}, "name": {"type": "string"} } }); let value = generate_json_value(&schema, 42)?; // Returns: {"id": 1, "name": "Whiskers"} // Schema with regex pattern let pattern_schema = json!({ "type": "string", "pattern": "^[A-Z]{3}-\\d{4}$" }); let value = generate_json_value(&pattern_schema, 42)?; // Returns: "ABC-1234" (matches pattern) // Schema with format let format_schema = json!({ "type": "string", "format": "email" }); let value = generate_json_value(&format_schema, 42)?; // Returns: "mock@example.com" // Supported formats: date-time, date, time, duration, email, uuid, // uri, url, hostname, ipv4, ipv6, byte, binary, password, json-pointer // Polymorphic schema with discriminator let discriminator_schema = json!({ "discriminator": { "propertyName": "petType", "mapping": { "dog": "#/components/schemas/Dog", "cat": "#/components/schemas/Cat" } }, "oneOf": [ { "type": "object", "required": ["petType", "bark"], "properties": { "petType": {"type": "string"}, "bark": {"type": "boolean"} } } ] }); let value = generate_json_value(&discriminator_schema, 42)?; // Returns: {"petType": "dog", "bark": true} Ok(()) } ``` ## Core: RFC 7807 Problem Details Errors Create standardized error responses following the RFC 7807 Problem Details specification. ```rust use specmock_core::{ProblemDetails, ValidationIssue, PROBLEM_JSON_CONTENT_TYPE}; fn main() { // Validation error with issues let issues = vec![ ValidationIssue { instance_pointer: "/name".to_owned(), schema_pointer: "/properties/name/minLength".to_owned(), keyword: "minLength".to_owned(), message: "must be at least 1 character".to_owned(), } ]; let problem = ProblemDetails::validation_error(400, issues); let json = serde_json::to_string_pretty(&problem).unwrap(); println!("{}", json); // Output: // { // "type": "about:blank", // "title": "Bad Request", // "status": 400, // "detail": "Request validation failed", // "errors": [ // { // "instance_pointer": "/name", // "schema_pointer": "/properties/name/minLength", // "keyword": "minLength", // "message": "must be at least 1 character" // } // ] // } // Other error types let not_found = ProblemDetails::not_found("no such path: /pets/99"); let unsupported = ProblemDetails::unsupported_media_type("expected application/json"); let too_large = ProblemDetails::payload_too_large("body exceeds 10 MiB limit"); // Content-Type header value assert_eq!(PROBLEM_JSON_CONTENT_TYPE, "application/problem+json"); } ``` ## Core: $ref Resolution Resolve `$ref` pointers in OpenAPI/AsyncAPI documents including local refs, file-relative refs, and deeply nested references. ```rust use std::path::Path; use specmock_core::ref_resolver::RefResolver; fn main() -> Result<(), Box> { // Create resolver rooted at the spec directory let mut resolver = RefResolver::new(Path::new("./specs").to_path_buf()) .with_max_depth(64) // Cycle detection depth .with_external_cache_limit(128); // Max cached external docs // Resolve all $ref nodes in a document let resolved = resolver.resolve(Path::new("./specs/api.yaml"))?; // The resolved.root contains fully inlined JSON with no $ref nodes // Local refs like "#/components/schemas/Pet" are inlined // File refs like "./schemas/pet.yaml#/Pet" are loaded and inlined println!("{}", serde_json::to_string_pretty(&resolved.root)?); // Add additional allowed roots for external file refs let mut resolver = RefResolver::new(Path::new("./specs").to_path_buf()) .with_allowed_root(Path::new("./shared-schemas").to_path_buf()); Ok(()) } ``` ## Runtime: ServerConfig and start() Programmatically configure and start the runtime servers with full control over all options. ```rust use std::net::SocketAddr; use specmock_core::MockMode; use specmock_runtime::{ServerConfig, start}; #[tokio::main] async fn main() -> Result<(), Box> { let config = ServerConfig { openapi_spec: Some("api.yaml".into()), asyncapi_spec: Some("events.yaml".into()), proto_spec: Some("service.proto".into()), mode: MockMode::Mock, upstream: None, // Required if mode == Proxy seed: 42, http_addr: SocketAddr::from(([127, 0, 0, 1], 4010)), grpc_addr: SocketAddr::from(([127, 0, 0, 1], 5010)), ws_path: "/ws".to_owned(), max_body_size: 10 * 1024 * 1024, // 10 MiB }; // Validate configuration config.validate()?; // Start servers let running = start(config).await?; println!("HTTP bound to: {}", running.http_addr); if let Some(grpc) = running.grpc_addr { println!("gRPC bound to: {}", grpc); } // Wait for Ctrl+C tokio::signal::ctrl_c().await?; // Graceful shutdown running.shutdown().await; Ok(()) } ``` ## Integration Testing with Prism Comparison Run side-by-side comparison tests against Stoplight Prism to verify response equivalence. ```bash # Prerequisites npm install -g @stoplight/prism-cli # Run integration tests (uses Justfile) just integration-test # Custom configuration via environment variables SPECMOCK_FUZZ_SEED=123 \ SPECMOCK_FUZZ_ITERATIONS=20 \ SPECMOCK_PRISM_CMD=prism \ just integration-test # GitHub Actions example # - name: Install Prism # run: npm install -g @stoplight/prism-cli # - name: Run integration tests # run: just integration-test ``` ## Summary spec-mock is designed for API development workflows where contract-first design and reliable mocking are essential. It excels in integration testing scenarios where the Rust SDK can be embedded directly in `#[tokio::test]` functions, eliminating external dependencies and port management complexity. The deterministic seeding ensures reproducible test results across CI environments. For development, the CLI provides instant mock servers from OpenAPI, AsyncAPI, or Protobuf specs with full request validation and realistic response generation. The library integrates seamlessly with existing Rust test infrastructure through the `specmock_sdk` crate, while `specmock_core` provides standalone validation and faker utilities that can be used independently. The multi-protocol support (HTTP/WebSocket/gRPC) from a single runtime makes it suitable for microservices architectures. Proxy mode enables contract testing against real services, validating that upstream responses conform to the documented API specification.