alef 0.22.16

Opinionated polyglot binding generator for Rust libraries
Documentation
{#- Go app harness for server-pattern e2e tests

   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")
   - run_method: serve entrypoint (e.g., "Run")
   - port: binding port (e.g., 8012)
   - fixtures_json: raw JSON string with all fixtures (auto-serialized)
#}
package main

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

{%- 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"`
}

// 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
		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.
		if err := app.{{ register_route_method }}(handler, builder); err != nil {
			log.Fatalf("register route %s: %v", fullRoute, err)
		}
		builder.Free()
	}

	// Spawn the server in a goroutine and poll for port readiness.
	runErr := make(chan error, 1)
	go func() {
		runErr <- app.{{ run_method }}()
	}()

	// Poll the bind port with 50ms intervals, capped at 500ms total.
	addr := fmt.Sprintf("%s:%d", host, port)
	readyDeadline := time.Now().Add(500 * time.Millisecond)
	for {
		if time.Now().After(readyDeadline) {
			log.Fatalf("server did not bind to %s within 500ms", addr)
		}
		conn, err := net.DialTimeout("tcp", addr, 50*time.Millisecond)
		if err == nil {
			conn.Close()
			break
		}
		time.Sleep(50 * time.Millisecond)
	}

	// 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)
	go func() {
		<-sigChan
		os.Exit(0)
	}()

	// Wait for the server to finish (should not return unless error).
	if err := <-runErr; err != nil {
		log.Fatalf("run: %v", err)
	}
}

// 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) {
		// Wrap the response in the framework's Response struct.
		resp := map[string]interface{}{
			"status_code": statusCode,
			"content":     body,
			"headers":     headers,
		}
		respBody, _ := json.Marshal(resp)
		return respBody, nil
	}
}