Skip to content

Instantly share code, notes, and snippets.

@bparanj
Last active April 11, 2025 12:48
Show Gist options
  • Save bparanj/986d3451230436cbe6767a6f0ac1ba40 to your computer and use it in GitHub Desktop.
Save bparanj/986d3451230436cbe6767a6f0ac1ba40 to your computer and use it in GitHub Desktop.
Mock Server

Creating a Mock Server for OpenAPI in Go

Prerequisites

  • Go installed on your system
  • Your OpenAPI specification file (openapi.yaml)

What We'll Use

  • github.com/getkin/kin-openapi for parsing OpenAPI specs
  • github.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:

  1. Parse your OpenAPI YAML file
  2. Set up routes based on the paths defined in the spec
  3. Generate mock responses according to the schema definitions
  4. Handle different HTTP methods and status codes

How to Run the Mock Server

  1. Create a new Go module:

    mkdir openapi-mock-server
    cd openapi-mock-server
    go mod init openapi-mock-server
  2. Copy the code from above into a file named main.go

  3. 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
  4. Build and run the server, passing your OpenAPI YAML file as an argument:

    go run main.go openapi.yaml
  5. Test the API by sending requests to the endpoints defined in your spec. The server runs at http://localhost:8080 by default.

How It Works

  1. Parsing the OpenAPI Spec: The program reads your YAML file and parses it into a structured format using the kin-openapi library.

  2. Dynamic Route Registration: For each path and method defined in your spec, the server registers a corresponding route in the Echo framework.

  3. 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.

  4. Content Negotiation: The server prioritizes JSON responses but can handle different content types as defined in your spec.

Customization Options

  • Change the port: Modify the port variable in the main.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:

Testing Different Response Status Codes with HTTPie

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.

How to test different status codes

There are two ways to specify which status code response you want:

1. Using a query parameter:

http GET http://localhost:8080/your/endpoint status==400

2. Using a custom header:

http GET http://localhost:8080/your/endpoint X-Mock-Status:400

Example HTTPie commands for testing

Assuming your API has an endpoint /api/users:

1. Success (200 OK):

http GET http://localhost:8080/api/users

This will return the 200 OK response by default.

2. Bad Request (400):

http GET http://localhost:8080/api/users status==400

3. Unauthorized (401):

http GET http://localhost:8080/api/users status==401

4. Not Found (404):

http GET http://localhost:8080/api/users status==404

Testing with a request body (POST example)

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

HTTPie with headers example

# 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

How it works

The updated code now:

  1. Checks if you've specified a particular status code via query parameter or header
  2. Looks for that status code in the operation responses from your OpenAPI spec
  3. Generates a mock response using the schema for that status code
  4. 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.

OpenAPI Mock Server in Rust

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.

How to Set Up and Run

  1. Create a new Rust project:

    cargo new openapi-mock-server
    cd openapi-mock-server
  2. Replace the default Cargo.toml with the one provided above to include the necessary dependencies.

  3. Replace the content of src/main.rs with the provided code.

  4. Build and run the server:

    cargo build
    cargo run -- path/to/your/openapi.yaml
  5. The server will start on http://localhost:8080

Features of the Rust Implementation

  1. Path matching: The server matches request paths to patterns in your OpenAPI spec, including paths with parameters.

  2. Mock response generation: Creates mock data based on the schema definitions in your OpenAPI spec.

  3. 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
  4. Request logging: Shows which endpoints are being accessed and what status codes are being returned.

Using HTTPie with the Rust Server

The usage with HTTPie is identical to the Go version:

Success Response (200 OK)

http GET http://localhost:8080/api/users

Bad Request (400)

http GET http://localhost:8080/api/users status==400

Unauthorized (401)

http GET http://localhost:8080/api/users status==401

Not Found (404)

http GET http://localhost:8080/api/users status==404

Testing with a POST request

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

Implementation Notes

  • 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)
            }
        }
    }
}
@bparanj
Copy link
Author

bparanj commented Apr 11, 2025

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"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 {
		var statusCode int
		var mockResp interface{}

		// 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") {
				statusCode, mockResp = generateMockResponse(response.Value)
				break
			}
		}

		// If no successful response is defined, use a default
		if mockResp == nil {
			statusCode = http.StatusOK
			mockResp = map[string]string{"message": "Default mock response"}
		}

		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)
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment