- Go installed on your system
- Your OpenAPI specification file (openapi.yaml)
github.com/getkin/kin-openapi
for parsing OpenAPI specsgithub.com/labstack/echo/v4
as a lightweight web framework- Standard Go libraries for file handling and HTTP operations
Let me create a complete Go program that will:
- Parse your OpenAPI YAML file
- Set up routes based on the paths defined in the spec
- Generate mock responses according to the schema definitions
- Handle different HTTP methods and status codes
-
Create a new Go module:
mkdir openapi-mock-server cd openapi-mock-server go mod init openapi-mock-server
-
Copy the code from above into a file named
main.go
-
Install the required dependencies:
go get github.com/getkin/kin-openapi/openapi3 go get github.com/labstack/echo/v4 go get github.com/labstack/echo/v4/middleware
-
Build and run the server, passing your OpenAPI YAML file as an argument:
go run main.go openapi.yaml
-
Test the API by sending requests to the endpoints defined in your spec. The server runs at http://localhost:8080 by default.
-
Parsing the OpenAPI Spec: The program reads your YAML file and parses it into a structured format using the kin-openapi library.
-
Dynamic Route Registration: For each path and method defined in your spec, the server registers a corresponding route in the Echo framework.
-
Mock Response Generation: When a request is received, the server looks at the schema for the corresponding response and generates mock data that matches the schema.
-
Content Negotiation: The server prioritizes JSON responses but can handle different content types as defined in your spec.
- Change the port: Modify the
port
variable in themain.go
file - Add authentication: Implement middleware for authentication if your API requires it
- Custom response generation: Enhance the
generateMockValue
function to generate more realistic data
Great question! Let's enhance the mock server to support different response status codes based on the client's request.
I'll modify the code to allow you to specify which status code you want as a response through a special HTTP header or query parameter. This way, you can easily test different scenarios with httpie.
Here's how to implement this and use it:
I've updated the mock server to support testing different response status codes. Now you can easily test the various response scenarios defined in your OpenAPI spec (200, 400, 401, 404) using HTTPie.
There are two ways to specify which status code response you want:
http GET http://localhost:8080/your/endpoint status==400
http GET http://localhost:8080/your/endpoint X-Mock-Status:400
Assuming your API has an endpoint /api/users
:
http GET http://localhost:8080/api/users
This will return the 200 OK response by default.
http GET http://localhost:8080/api/users status==400
http GET http://localhost:8080/api/users status==401
http GET http://localhost:8080/api/users status==404
If your API has endpoints that require a request body (like POST operations):
# POST with a JSON body and get a 200 response
http POST http://localhost:8080/api/users name="John Doe" email="[email protected]"
# POST with a JSON body and get a 400 response
http POST http://localhost:8080/api/users name="John Doe" email="[email protected]" status==400
# Add authorization header and get a 200 response
http GET http://localhost:8080/api/users Authorization:"Bearer token123"
# Add authorization header and get a 401 response
http GET http://localhost:8080/api/users Authorization:"Bearer token123" X-Mock-Status:401
The updated code now:
- Checks if you've specified a particular status code via query parameter or header
- Looks for that status code in the operation responses from your OpenAPI spec
- Generates a mock response using the schema for that status code
- Returns the response with the requested status code
This makes it easy to test all your different error scenarios and responses defined in the OpenAPI spec without modifying the server code.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// MockResponse generates a mock response based on the schema
func generateMockValue(schema *openapi3.Schema) interface{} {
if schema == nil {
return nil
}
switch schema.Type {
case "string":
if schema.Example != nil {
return schema.Example
}
if schema.Enum != nil && len(schema.Enum) > 0 {
return schema.Enum[0]
}
if schema.Format == "date-time" {
return "2023-01-01T00:00:00Z"
}
if schema.Format == "date" {
return "2023-01-01"
}
if schema.Format == "email" {
return "[email protected]"
}
if schema.Format == "uuid" {
return "00000000-0000-0000-0000-000000000000"
}
return "string"
case "number", "integer":
if schema.Example != nil {
return schema.Example
}
if schema.Enum != nil && len(schema.Enum) > 0 {
return schema.Enum[0]
}
return 0
case "boolean":
if schema.Example != nil {
return schema.Example
}
return false
case "array":
if schema.Items == nil {
return []interface{}{}
}
result := []interface{}{}
// Generate just one item for the array
item := generateMockValue(schema.Items.Value)
if item != nil {
result = append(result, item)
}
return result
case "object":
result := map[string]interface{}{}
if schema.Properties != nil {
for name, property := range schema.Properties {
if property.Value != nil {
result[name] = generateMockValue(property.Value)
}
}
}
return result
default:
if schema.Example != nil {
return schema.Example
}
return nil
}
}
func generateMockResponse(response *openapi3.Response) (int, interface{}) {
if response == nil || response.Content == nil {
return http.StatusOK, map[string]string{"message": "No content defined in schema"}
}
// Try to find JSON content type
for contentType, mediaType := range response.Content {
if strings.Contains(contentType, "json") && mediaType.Schema != nil {
return http.StatusOK, generateMockValue(mediaType.Schema.Value)
}
}
// If no JSON content type is found, return a default message
return http.StatusOK, map[string]string{"message": "Mock response (no JSON schema found)"}
}
func main() {
// Check command line arguments
if len(os.Args) < 2 {
log.Fatal("Please provide the path to your OpenAPI specification file")
}
specPath := os.Args[1]
// Load the OpenAPI spec
data, err := ioutil.ReadFile(specPath)
if err != nil {
log.Fatalf("Failed to read spec file: %v", err)
}
// Parse the spec
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(data)
if err != nil {
log.Fatalf("Failed to parse OpenAPI spec: %v", err)
}
// Create a new Echo instance
e := echo.New()
// Add middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Register routes based on the OpenAPI spec
for path, pathItem := range doc.Paths {
echoPath := convertPathToEcho(path)
// Handle different HTTP methods
registerHandler(e, "GET", echoPath, pathItem.Get)
registerHandler(e, "POST", echoPath, pathItem.Post)
registerHandler(e, "PUT", echoPath, pathItem.Put)
registerHandler(e, "DELETE", echoPath, pathItem.Delete)
registerHandler(e, "PATCH", echoPath, pathItem.Patch)
registerHandler(e, "HEAD", echoPath, pathItem.Head)
registerHandler(e, "OPTIONS", echoPath, pathItem.Options)
}
// Start the server
port := ":8080"
fmt.Printf("OpenAPI Mock Server started on http://localhost%s\n", port)
fmt.Println("Press Ctrl+C to stop the server")
e.Logger.Fatal(e.Start(port))
}
// Convert OpenAPI path parameters to Echo path parameters
func convertPathToEcho(path string) string {
// Replace {param} with :param
return strings.ReplaceAll(strings.ReplaceAll(path, "{", ":"), "}", "")
}
// Register a handler for a specific HTTP method
func registerHandler(e *echo.Echo, method string, path string, operation *openapi3.Operation) {
if operation == nil {
return
}
handler := func(c echo.Context) error {
// Check if a specific status code is requested
requestedStatus := c.QueryParam("status")
if requestedStatus == "" {
// Also check for X-Mock-Status header
requestedStatus = c.Request().Header.Get("X-Mock-Status")
}
var statusCode int = http.StatusOK
var mockResp interface{}
var responseFound bool = false
// If a specific status is requested and defined in the spec, use that
if requestedStatus != "" {
if response, ok := operation.Responses[requestedStatus]; ok {
code, err := strconv.Atoi(requestedStatus)
if err == nil {
responseFound = true
_, mockResp = generateMockResponse(response.Value)
// Override with the requested status code
statusCode = code
}
}
}
// If no specific status was requested or found, use the default behavior
if !responseFound {
// Find a successful response in the spec
for status, response := range operation.Responses {
// Prioritize 200 OK responses
if status == "200" || status == "201" || strings.HasPrefix(status, "2") {
code, err := strconv.Atoi(status)
if err == nil {
statusCode = code
_, mockResp = generateMockResponse(response.Value)
break
}
}
}
}
// If no response is defined, use a default
if mockResp == nil {
statusCode = http.StatusOK
mockResp = map[string]string{"message": "Default mock response"}
}
// Log information about the response
fmt.Printf("Serving %s %s with status %d\n",
c.Request().Method, c.Request().URL.Path, statusCode)
return c.JSON(statusCode, mockResp)
}
// Register the handler with Echo
switch method {
case "GET":
e.GET(path, handler)
fmt.Printf("Registered GET %s\n", path)
case "POST":
e.POST(path, handler)
fmt.Printf("Registered POST %s\n", path)
case "PUT":
e.PUT(path, handler)
fmt.Printf("Registered PUT %s\n", path)
case "DELETE":
e.DELETE(path, handler)
fmt.Printf("Registered DELETE %s\n", path)
case "PATCH":
e.PATCH(path, handler)
fmt.Printf("Registered PATCH %s\n", path)
case "HEAD":
e.HEAD(path, handler)
fmt.Printf("Registered HEAD %s\n", path)
case "OPTIONS":
e.OPTIONS(path, handler)
fmt.Printf("Registered OPTIONS %s\n", path)
}
}
I'd be happy to create an equivalent OpenAPI mock server in Rust. Below is a complete implementation that provides the same functionality as the Go version, including the ability to specify different status codes for testing various responses.
I've created a Rust implementation of the OpenAPI mock server with the same functionality as the Go version. This includes the ability to specify different status codes for testing various error responses.
-
Create a new Rust project:
cargo new openapi-mock-server cd openapi-mock-server
-
Replace the default
Cargo.toml
with the one provided above to include the necessary dependencies. -
Replace the content of
src/main.rs
with the provided code. -
Build and run the server:
cargo build cargo run -- path/to/your/openapi.yaml
-
The server will start on
http://localhost:8080
-
Path matching: The server matches request paths to patterns in your OpenAPI spec, including paths with parameters.
-
Mock response generation: Creates mock data based on the schema definitions in your OpenAPI spec.
-
Status code selection: Supports the same methods to specify which response you want to test:
- Query parameter:
?status=404
- Custom header:
X-Mock-Status: 404
- Query parameter:
-
Request logging: Shows which endpoints are being accessed and what status codes are being returned.
The usage with HTTPie is identical to the Go version:
http GET http://localhost:8080/api/users
http GET http://localhost:8080/api/users status==400
http GET http://localhost:8080/api/users status==401
http GET http://localhost:8080/api/users status==404
http POST http://localhost:8080/api/users name="John Doe" email="[email protected]"
http POST http://localhost:8080/api/users name="John Doe" email="[email protected]" status==400
- The Rust implementation uses the
actix-web
framework for HTTP handling. - OpenAPI schema parsing is done with the
openapiv3
crate. - The server handles path parameters and generates appropriate mock data.
- For simplicity, this implementation doesn't fully resolve schema references but provides placeholders.
This mock server should work with any standard OpenAPI 3.0 specification and allows you to easily test all the response types defined in your API.
Cargo.toml:
[package]
name = "openapi-mock-server"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
env_logger = "0.10.0"
openapiv3 = "1.0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
use std::collections::HashMap;
use std::env;
use std::fs;
use std::sync::Arc;
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use actix_web::http::StatusCode;
use actix_web::middleware::Logger;
use openapiv3::{OpenAPI, Operation, Parameter, PathItem, ReferenceOr, Response, Schema};
use serde_json::{json, Value};
// Struct to hold our app state
struct AppState {
spec: Arc<OpenAPI>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// Get the OpenAPI spec file path from command line arguments
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Please provide the path to your OpenAPI specification file");
std::process::exit(1);
}
let spec_path = &args[1];
// Read and parse the OpenAPI spec
let spec_content = match fs::read_to_string(spec_path) {
Ok(content) => content,
Err(e) => {
eprintln!("Failed to read spec file: {}", e);
std::process::exit(1);
}
};
let spec: OpenAPI = match serde_yaml::from_str(&spec_content) {
Ok(spec) => spec,
Err(e) => {
eprintln!("Failed to parse OpenAPI spec: {}", e);
std::process::exit(1);
}
};
let spec = Arc::new(spec);
let app_state = web::Data::new(AppState { spec });
println!("OpenAPI Mock Server started on http://localhost:8080");
println!("Press Ctrl+C to stop the server");
// Start the HTTP server
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(app_state.clone())
.default_service(web::route().to(handle_request))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
// Main request handler
async fn handle_request(
req: HttpRequest,
data: web::Data<AppState>,
body: Option<web::Bytes>,
) -> impl Responder {
let method = req.method().as_str();
let path = req.path();
// Find matching path in OpenAPI spec
let (path_pattern, path_item) = match find_matching_path(&data.spec.paths, path) {
Some(result) => result,
None => {
println!("No matching path found for {}", path);
return HttpResponse::NotFound().json(json!({
"message": "Endpoint not found in OpenAPI specification"
}));
}
};
// Get the operation based on HTTP method
let operation = match get_operation(&path_item, method) {
Some(op) => op,
None => {
println!("Method {} not supported for path {}", method, path);
return HttpResponse::MethodNotAllowed().json(json!({
"message": "Method not allowed for this endpoint"
}));
}
};
// Extract path parameters
let path_params = extract_path_params(path_pattern, path);
// Check if a specific status code is requested
let requested_status = req
.query_string()
.split('&')
.find_map(|param| {
let parts: Vec<&str> = param.split('=').collect();
if parts.len() == 2 && parts[0] == "status" {
parts[1].parse::<u16>().ok()
} else {
None
}
})
.or_else(|| {
req.headers()
.get("X-Mock-Status")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse::<u16>().ok())
});
// Generate mock response
let (status_code, mock_response) =
generate_response(operation, requested_status, &path_params, body.as_ref());
println!("Serving {} {} with status {}", method, path, status_code);
HttpResponse::build(StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK))
.json(mock_response)
}
// Find the matching path pattern in the OpenAPI spec
fn find_matching_path<'a>(
paths: &'a openapiv3::Paths,
request_path: &str,
) -> Option<(&'a str, &'a PathItem)> {
// First try exact match
if let Some(item) = paths.paths.get(request_path) {
return Some((request_path, item));
}
// Try to match with path parameters
for (pattern, item) in &paths.paths {
if path_matches(pattern, request_path) {
return Some((pattern, item));
}
}
None
}
// Check if a request path matches an OpenAPI path pattern
fn path_matches(pattern: &str, path: &str) -> bool {
let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if pattern_parts.len() != path_parts.len() {
return false;
}
for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
if !pattern_part.starts_with('{') || !pattern_part.ends_with('}') {
if pattern_part != path_part {
return false;
}
}
}
true
}
// Extract path parameters from the request path
fn extract_path_params(pattern: &str, path: &str) -> HashMap<String, String> {
let mut params = HashMap::new();
let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
if pattern_part.starts_with('{') && pattern_part.ends_with('}') {
let param_name = pattern_part
.trim_start_matches('{')
.trim_end_matches('}')
.to_string();
params.insert(param_name, path_part.to_string());
}
}
params
}
// Get the operation object based on HTTP method
fn get_operation<'a>(path_item: &'a PathItem, method: &str) -> Option<&'a Operation> {
match method {
"GET" => path_item.get.as_ref(),
"POST" => path_item.post.as_ref(),
"PUT" => path_item.put.as_ref(),
"DELETE" => path_item.delete.as_ref(),
"PATCH" => path_item.patch.as_ref(),
"HEAD" => path_item.head.as_ref(),
"OPTIONS" => path_item.options.as_ref(),
_ => None,
}
}
// Generate a mock response based on the OpenAPI operation
fn generate_response(
operation: &Operation,
requested_status: Option<u16>,
_path_params: &HashMap<String, String>,
_body: Option<&web::Bytes>,
) -> (u16, Value) {
let mut status_code = 200;
let mut response_body = json!({});
let mut response_found = false;
// If a specific status code is requested and defined in the spec, use that
if let Some(code) = requested_status {
if let Some(response) = operation.responses.responses.get(&code.to_string()) {
response_found = true;
status_code = code;
response_body = generate_mock_response(response);
}
}
// If no specific status was requested or found, use the default behavior
if !response_found {
// Find a successful response in the spec
for (status, response) in &operation.responses.responses {
if status.starts_with('2') {
if let Ok(code) = status.parse::<u16>() {
status_code = code;
response_body = generate_mock_response(response);
break;
}
}
}
}
// If no response was found or generated, use a default
if response_body == json!({}) {
response_body = json!({
"message": "Default mock response"
});
}
(status_code, response_body)
}
// Generate a mock response from an OpenAPI Response object
fn generate_mock_response(response: &ReferenceOr<Response>) -> Value {
let response = match response {
ReferenceOr::Reference { .. } => {
// For simplicity, we don't resolve references in this example
return json!({
"message": "Reference response (not resolved)"
});
}
ReferenceOr::Item(response) => response,
};
// Try to find a JSON schema to generate a response from
for (content_type, media_type) in &response.content {
if content_type.contains("json") {
if let Some(schema_or_ref) = &media_type.schema {
return generate_mock_value(schema_or_ref);
}
}
}
// Default response if no JSON schema is found
json!({
"message": "Mock response (no JSON schema found)"
})
}
// Generate a mock value based on an OpenAPI Schema
fn generate_mock_value(schema_or_ref: &ReferenceOr<Schema>) -> Value {
let schema = match schema_or_ref {
ReferenceOr::Reference { .. } => {
// For simplicity, we don't resolve references in this example
return json!("Reference type (not resolved)");
}
ReferenceOr::Item(schema) => schema,
};
match &schema.schema_kind {
openapiv3::SchemaKind::Type(schema_type) => match schema_type {
openapiv3::Type::String(string_type) => {
if let Some(example) = &schema.example {
return example.clone();
}
if let Some(enum_values) = &string_type.enumeration {
if !enum_values.is_empty() {
return enum_values[0].clone();
}
}
match string_type.format.as_deref() {
Some("date-time") => json!("2023-01-01T00:00:00Z"),
Some("date") => json!("2023-01-01"),
Some("email") => json!("[email protected]"),
Some("uuid") => json!("00000000-0000-0000-0000-000000000000"),
_ => json!("string"),
}
}
openapiv3::Type::Number(_) | openapiv3::Type::Integer(_) => {
if let Some(example) = &schema.example {
return example.clone();
}
json!(0)
}
openapiv3::Type::Boolean {} => {
if let Some(example) = &schema.example {
return example.clone();
}
json!(false)
}
openapiv3::Type::Array(array_type) => {
let mut result = Vec::new();
if let Some(items) = &array_type.items {
let item = generate_mock_value(items);
result.push(item);
}
json!(result)
}
openapiv3::Type::Object(object_type) => {
let mut result = serde_json::Map::new();
for (name, property) in &object_type.properties {
result.insert(name.clone(), generate_mock_value(property));
}
json!(result)
}
},
openapiv3::SchemaKind::OneOf { .. } |
openapiv3::SchemaKind::AllOf { .. } |
openapiv3::SchemaKind::AnyOf { .. } => {
// For simplicity, we return a placeholder for these complex schemas
json!({
"message": "Complex schema (oneOf/allOf/anyOf not fully supported)"
})
}
openapiv3::SchemaKind::Not { .. } => {
json!({
"message": "Not schema (not supported)"
})
}
openapiv3::SchemaKind::Any(any) => {
if let Some(example) = &schema.example {
return example.clone();
}
if any.properties.is_empty() {
json!({})
} else {
let mut result = serde_json::Map::new();
for (name, property) in &any.properties {
result.insert(name.clone(), generate_mock_value(property));
}
json!(result)
}
}
}
}