alef 0.22.19

Opinionated polyglot binding generator for Rust libraries
Documentation
#!/usr/bin/env ruby
{# Ruby app harness for server-pattern e2e tests

   This harness script is spawned as a subprocess by spec_helper.rb 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 Ruby codegen):
   - imports: list of module names to import (e.g., ["my_pkg"])
   - app_class: class name for SUT app (e.g., "MyPkg::App")
   - method_enum_module: module path for Method enum (e.g., "MyPkg::Method")
   - register_route_method: app method to register routes (e.g., "register_route")
   - run_method: serve entrypoint (e.g., "run")
   - host: binding host (e.g., "127.0.0.1")
   - port: binding port (e.g., 8002)
   - fixtures_json: raw JSON string with all fixtures (auto-serialized)
#}
# frozen_string_literal: true

require "json"
require "socket"
require "{{ imports[0] }}"

module AppHarness
  # Load fixtures from the JSON payload.
  FIXTURES_JSON = {{ fixtures_json }}
  FIXTURES = JSON.parse(FIXTURES_JSON, symbolize_names: true)

  # Create and configure the app. The leading `::` anchors the lookup at the
  # top-level constant namespace so the surrounding `module AppHarness` block
  # does not shadow a configured `App` symbol that lives outside the module.
  APP = ::{{ app_class }}.new

  # Register a handler for each fixture.
  FIXTURES.each do |fixture_id, fixture|
    http = fixture[:http]
    next unless http

    handler_config = http[:handler] || {}
    route = handler_config[:route] || "/"
    method_str = (handler_config[:method] || "GET").upcase
    body_schema = handler_config[:body_schema]
    expected = http[:expected_response] || {}

    expected_status = expected[:status_code] || 200
    expected_body = expected[:body]
    expected_headers = expected[:headers] || {}

    # Build the handler closure that returns the expected response.
    handler_fn = lambda do |*_args, **_kwargs|
      # Return the expected response wrapped in the framework's response shape.
      # The body field name ({{ response_body_field }}) is configurable via
      # `[crates.e2e.harness].response_body_field` so the wrapper matches the
      # SUT's Response deserialization layout.
      {
        status_code: expected_status,
        "{{ response_body_field }}": expected_body,
        headers: expected_headers
      }
    end

    # Register the handler at /fixtures/<fixture_id>{route}
    full_route = "/fixtures/#{fixture_id}#{route}"

    # Normalize the HTTP method to PascalCase for RouteBuilder (accepts strings via TryConvert).
    method_str_upper = method_str.upcase
    method_val = method_str_upper.capitalize
    next if method_val.empty?

    # Build the RouteBuilder with the method and path.
    builder = ::{{ route_builder_class }}.new(method_val, full_route)

    # If there's a body schema, attach it to the builder.
    if body_schema
      builder = builder.{{ route_builder_schema_setter }}(JSON.dump(body_schema))
    end

    # Thread handler middleware through to the RouteBuilder.
    middleware_config = handler_config[:middleware] || {}
    cors_config_class = Object.const_get("{{ server_config_class }}".sub("ServerConfig", "CorsConfig"))
    middleware_dispatch = {
      "cors" => ->(cfg) { builder.cors(cors_config_class.from_json(cfg.to_json)) }
    }
    middleware_config.each do |mw_name, mw_cfg|
      next unless mw_cfg
      applicator = middleware_dispatch[mw_name.to_s]
      next unless applicator
      builder = applicator.call(mw_cfg)
    end

    # Register the route with the handler.
    # Ruby uses block form for route registration
    APP.{{ register_route_method }}(builder, &handler_fn)
  end

  # Configure and start the server with retry on bind failure.
  # The probe-close window may leave the port in TIME_WAIT, causing EADDRINUSE.
  # Retry with fresh random ports from the dynamic range (40000-60000).
  max_attempts = 20
  attempt = 0
  loop do
    attempt += 1
    port = rand(40000..60000)

    begin
      APP.config(::{{ server_config_class }}.new(host: "{{ host }}", port: port))
      puts "HARNESS_PORT=#{port}"
      STDOUT.flush
      APP.{{ run_method }}
      break  # Success: run returned normally
    rescue Errno::EADDRINUSE, RuntimeError => e
      # Bind failed; raise if we've exhausted retries
      if attempt >= max_attempts
        raise "Failed to bind after #{max_attempts} attempts: #{e.message}"
      end
      # Try again with a different port
      next
    end
  end
end