alef 0.25.50

Opinionated polyglot binding generator for Rust libraries
Documentation
{#- Go app harness for server-pattern e2e tests
   <!-- go-ffi-nonblock -->

   This harness program is spawned as a subprocess by main_test.go and runs the
   SUT app, registering handlers per fixture. It loads all fixtures, creates
   handlers that return expected responses, and serves on a configured port.

   Context variables (passed from Go codegen):
   - imports: list of import paths to include (typically the package being tested)
   - app_class: class name for SUT app (e.g., "App")
   - route_builder_class: class name for route builder (e.g., "RouteBuilder")
   - method_enum_import: import path for Method (e.g., "my_app")
   - method_enum_class: class name for method enum (e.g., "GET", "POST")
   - register_route_method: app method to register routes (e.g., "RegisterRoute")
   - start_background_method: non-blocking serve entrypoint (e.g., "StartBackground")
   - port: binding port (e.g., 8012)
   - fixtures_json: raw JSON string with all fixtures (auto-serialized)
#}
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strings"
	"syscall"

{%- for import_name in imports %}
	{{ import_alias }} "{{ import_name }}"
{%- endfor %}
)

// Fixture describes an HTTP test fixture with its handler route and expected response.
type Fixture struct {
	HTTP *HTTPFixture `json:"http"`
}

// HTTPFixture contains handler and expected response details.
type HTTPFixture struct {
	Handler          HandlerDetails `json:"handler"`
	ExpectedResponse ResponseDetails `json:"expected_response"`
}

// HandlerDetails specifies the route and HTTP method for the handler.
type HandlerDetails struct {
	Route      string                 `json:"route"`
	Method     string                 `json:"method"`
	BodySchema interface{}             `json:"body_schema"`
	Middleware map[string]interface{}  `json:"middleware"`
}

// ResponseDetails defines the expected status, body, and headers.
type ResponseDetails struct {
	StatusCode uint16            `json:"status_code"`
	Body       interface{}       `json:"body"`
	Headers    map[string]string `json:"headers"`
}

const port = {{ port }}
const host = "127.0.0.1"

// Load fixtures from the JSON payload embedded at codegen time.
// Use a quoted string with escaped newlines to avoid backtick conflicts.
var fixturesJSON = "{{ fixtures_json }}"

func main() {
	var fixtures map[string]Fixture
	if err := json.Unmarshal([]byte(fixturesJSON), &fixtures); err != nil {
		log.Fatalf("unmarshal fixtures: %v", err)
	}

	app, err := {{ import_alias }}.NewApp()
	if err != nil {
		log.Fatalf("new app: %v", err)
	}

	// Map fixture method names (UPPERCASE) to framework method enum constants (PascalCase).
	methodMap := map[string]{{ import_alias }}.Method{
		"GET":     {{ import_alias }}.MethodGet,
		"POST":    {{ import_alias }}.MethodPost,
		"PUT":     {{ import_alias }}.MethodPut,
		"PATCH":   {{ import_alias }}.MethodPatch,
		"DELETE":  {{ import_alias }}.MethodDelete,
		"HEAD":    {{ import_alias }}.MethodHead,
		"OPTIONS": {{ import_alias }}.MethodOptions,
		"CONNECT": {{ import_alias }}.MethodConnect,
		"TRACE":   {{ import_alias }}.MethodTrace,
	}

	// Register a handler for each fixture.
	for fixtureID, fixture := range fixtures {
		if fixture.HTTP == nil {
			continue
		}

		http := fixture.HTTP
		route := http.Handler.Route
		method := http.Handler.Method
		bodySchema := http.Handler.BodySchema
		middleware := http.Handler.Middleware
		expectedStatus := http.ExpectedResponse.StatusCode
		expectedBody := http.ExpectedResponse.Body
		expectedHeaders := http.ExpectedResponse.Headers

		// Build a closure that captures the expected response for this fixture.
		// The handler ignores request parameters and returns the recorded response.
		handler := makeHandler(expectedStatus, expectedBody, expectedHeaders)

		// Register the route with the full fixture-namespaced path: /fixtures/<fixture_id>{route}
		fullRoute := fmt.Sprintf("/fixtures/%s%s", fixtureID, route)

		// Convert method string (GET, POST, etc.) to the framework's method enum.
		m, ok := methodMap[strings.ToUpper(method)]
		if !ok {
			log.Printf("skipping fixture %s: unknown method %s", fixtureID, method)
			continue
		}
		builder, err := {{ import_alias }}.RouteBuilderNew(m, fullRoute)
		if err != nil {
			log.Fatalf("create route builder: %v", err)
		}

		// If there's a body schema, attach it to the builder.
		if bodySchema != nil {
			bodySchemaJSON, _ := json.Marshal(bodySchema)
			builder = builder.RequestSchemaJSON(json.RawMessage(bodySchemaJSON))
		}

		// Register the route with the handler.
		// Note: The builder is consumed by RegisterRoute (ownership transferred to Rust).
		// Do NOT call builder.Free() — the Rust FFI layer already owns and frees it.
		if err := app.{{ register_route_method }}(handler, builder); err != nil {
			log.Fatalf("register route %s: %v", fullRoute, err)
		}

		// If the fixture has CORS middleware configured, register an OPTIONS preflight handler.
		if middleware != nil {
			if corsConfig, ok := middleware["cors"].(map[string]interface{}); ok {
				corsHandler := makeCorsPreflightHandler(corsConfig)
				optionsMethod := methodMap["OPTIONS"]
				optionsBuilder, err := {{ import_alias }}.RouteBuilderNew(optionsMethod, fullRoute)
				if err != nil {
					log.Fatalf("create OPTIONS route builder: %v", err)
				}
				if err := app.{{ register_route_method }}(corsHandler, optionsBuilder); err != nil {
					log.Fatalf("register OPTIONS route %s: %v", fullRoute, err)
				}
			}
		}
	}

	// Start the server on a background OS thread.  StartBackground blocks until
	// the TCP socket is bound, so the server is guaranteed to be accepting
	// connections when this call returns.  This avoids the goroutine + polling
	// pattern that previously exhausted cgo OS threads.
	serverHandle, err := app.{{ start_background_method }}(host, port)
	if err != nil {
		log.Fatalf("start background server on %s:%d: %v", host, port, err)
	}

	// Signal readiness to the parent test process.
	fmt.Printf("Harness listening on %s:%d\n", host, port)
	os.Stdout.Sync()

	// Graceful shutdown on interrupt.
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
	<-sigChan
	serverHandle.Stop()
}

// makeCorsPreflightHandler builds a CORS preflight handler that validates the request
// and returns either a 403 (Forbidden) if validation fails or 204 (No Content) if allowed.
func makeCorsPreflightHandler(corsConfig map[string]interface{}) func([]byte) ([]byte, error) {
	return func(reqBody []byte) ([]byte, error) {
		// Parse the request to extract CORS-related headers.
		// The request body contains the full RequestData with headers as a map.
		var requestData map[string]interface{}
		if err := json.Unmarshal(reqBody, &requestData); err != nil {
			// If we can't parse, return 403.
			return jsonResponse(403, nil, map[string]string{}), nil
		}

		// Extract headers from request data (case-insensitive lookup helper).
		headersRaw, ok := requestData["headers"].(map[string]interface{})
		if !ok {
			headersRaw = make(map[string]interface{})
		}
		origin := getHeaderValue(headersRaw, "origin")
		requestMethod := getHeaderValue(headersRaw, "access-control-request-method")
		requestHeaders := getHeaderValue(headersRaw, "access-control-request-headers")

		// Extract CORS config.
		allowedOrigins := getStringSlice(corsConfig, "allow_origins")
		allowedMethods := getStringSlice(corsConfig, "allow_methods")
		allowedHeaders := getStringSlice(corsConfig, "allow_headers")
		maxAge := getInt64(corsConfig, "max_age")

		// Validate CORS preflight request.
		isOriginAllowed := contains(allowedOrigins, origin)
		// Compare methods case-insensitively (both sides uppercased).
		isMethodAllowed := requestMethod == "" || containsCaseInsensitive(allowedMethods, requestMethod)
		areHeadersAllowed := true
		if requestHeaders != "" {
			requestHeadersList := splitAndTrim(requestHeaders, ",")
			for _, rh := range requestHeadersList {
				if !containsCaseInsensitive(allowedHeaders, rh) {
					areHeadersAllowed = false
					break
				}
			}
		}

		// If any check fails, return 403.
		if !isOriginAllowed || !isMethodAllowed || !areHeadersAllowed {
			return jsonResponse(403, nil, map[string]string{}), nil
		}

		// Build CORS response headers.
		corsHeaders := map[string]string{
			"Access-Control-Allow-Origin":  origin,
			"Access-Control-Allow-Methods": requestMethod,
			"Access-Control-Allow-Headers": requestHeaders,
		}
		if maxAge > 0 {
			corsHeaders["Access-Control-Max-Age"] = fmt.Sprintf("%d", maxAge)
		}

		return jsonResponse(204, nil, corsHeaders), nil
	}
}

// Helper functions for CORS validation.

// getHeaderValue extracts a header value case-insensitively.
func getHeaderValue(headers map[string]interface{}, name string) string {
	if val, ok := headers[name].(string); ok {
		return val
	}
	// Try case-insensitive lookup.
	lowerName := strings.ToLower(name)
	for k, v := range headers {
		if strings.ToLower(k) == lowerName {
			if str, ok := v.(string); ok {
				return str
			}
		}
	}
	return ""
}

// getStringSlice extracts a slice of strings from a map.
func getStringSlice(m map[string]interface{}, key string) []string {
	val, ok := m[key]
	if !ok {
		return []string{}
	}
	slice, ok := val.([]interface{})
	if !ok {
		return []string{}
	}
	result := make([]string, 0, len(slice))
	for _, item := range slice {
		if str, ok := item.(string); ok {
			result = append(result, str)
		}
	}
	return result
}

// getInt64 extracts an int64 from a map.
func getInt64(m map[string]interface{}, key string) int64 {
	val, ok := m[key]
	if !ok {
		return 0
	}
	switch v := val.(type) {
	case float64:
		return int64(v)
	case int64:
		return v
	default:
		return 0
	}
}

// contains checks if a string is in a slice (case-sensitive).
func contains(slice []string, s string) bool {
	for _, item := range slice {
		if item == s {
			return true
		}
	}
	return false
}

// containsCaseInsensitive checks if a string is in a slice (case-insensitive).
func containsCaseInsensitive(slice []string, s string) bool {
	lower := strings.ToLower(s)
	for _, item := range slice {
		if strings.ToLower(item) == lower {
			return true
		}
	}
	return false
}

// splitAndTrim splits a string by a delimiter and trims whitespace.
func splitAndTrim(s, delimiter string) []string {
	parts := strings.Split(s, delimiter)
	result := make([]string, len(parts))
	for i, part := range parts {
		result[i] = strings.TrimSpace(part)
	}
	return result
}

// jsonResponse builds a JSON response wrapper.
func jsonResponse(statusCode uint16, body interface{}, headers map[string]string) []byte {
	resp := map[string]interface{}{
		"status_code": statusCode,
		"content":     body,
		"headers":     headers,
	}
	respBody, _ := json.Marshal(resp)
	return respBody
}

// makeHandler returns a handler function that always returns the expected response.
func makeHandler(statusCode uint16, body interface{}, headers map[string]string) func([]byte) ([]byte, error) {
	return func(reqBody []byte) ([]byte, error) {
		return jsonResponse(statusCode, body, headers), nil
	}
}