blob: 725108a43fb94089c96858ab96c951034184a400 [file] [edit]
// Copyright (C) 2025 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
#if ENABLE_CXX_INTEROP
import Foundation
private import TestWebKitAPILibrary.Helpers.cocoa.HTTPServer
import struct Swift.String
/// A description of an HTTP server route with a path and a response.
///
/// Typically, a simple route can be created using a path and some response data:
///
/// ```swift
/// Route("/hello") {
/// "Hello World!"
/// }
/// ```
///
/// A route can also be used to express a group of routes that share a common path prefix:
///
/// ```swift
/// Route("/parent") {
/// Route("/a") {
/// "My path is /parent/a"
/// }
///
/// Route("/b") {
/// "My path is /parent/b"
/// }
/// }
/// ```
public struct Route: Sendable {
fileprivate struct Storage: Sendable {
let pathComponents: [String]
let response: String
}
fileprivate let children: [Storage]
fileprivate init(children: [Storage]) {
self.children = children
}
fileprivate init(path: String, response: String) {
self.children = [Storage(pathComponents: [path], response: response)]
}
/// Creates a Route from a group of child Routes.
///
/// - Parameters:
/// - path: The path of this route. If this value is non-empty, it must start with `/`.
/// - route: The children of this route; each child has it's full path prepended by the path of this route.
public init(_ path: String, @RouteBuilder _ route: () -> Route) {
self.children = route().children
.map {
Storage(pathComponents: [path] + $0.pathComponents, response: $0.response)
}
}
/// Creates a Route whose response data is a String.
///
/// - Parameters:
/// - path: The path of this route. If this value is non-empty, it must start with `/`.
/// - response: The response to be used.
public init(_ path: String, _ response: () -> String) {
self.init(path: path, response: response())
}
}
/// A result builder used to create a ``Route``.
@resultBuilder
public struct RouteBuilder {
/// Create a ``Route`` from a group of Routes.
public static func buildBlock(_ components: Route...) -> Route {
.init(children: components.flatMap(\.children))
}
}
/// A type used to simulate an HTTP server with a set of predefined responses for different types of requests.
@MainActor
@safe
public struct HTTPServer: ~Copyable {
/// A protocol describing how an HTTP connection handles requests.
public enum `Protocol`: Sendable {
/// The HTTP protocol.
case http
/// The HTTPS protocol.
case https
/// The HTTPS protocol, using a legacy version of TLS.
case httpsWithLegacyTLS
/// The HTTP2 protocol.
case http2
/// The HTTPS proxy protocol.
case httpsProxy
/// The HTTPS proxy protocol with authentication.
case httpsProxyWithAuthentication
}
private var storage: TestWebKitAPI.__CxxHTTPServer
/// Create a server from a group of routes.
///
/// For example, a server which loads `/webkit/success` after loading `/example` can be created using two routes:
///
/// ```swift
/// func doSomethingCool() async throws {
/// let server = HTTPServer(protocol: .httpsProxy) {
/// Route("/example") {
/// "<script>w = window.open('https://webkit.org/webkit/success')</script>"
/// }
///
/// Route("/webkit/success") {
/// "Loaded!"
/// }
/// }
///
/// let page = WebPage()
///
/// let result = try await server.run {
/// let destination = address.appendingPathComponent("example")
///
/// try await page.load(destination).wait()
///
/// return page.url
/// }
///
/// // `result` is now a `URL` with a value like `http://127.0.0.1:8080/webkit/success`.
/// }
/// ```
///
/// - Parameters:
/// - protocol: The HTTP protocol to use for this server.
/// - route: A group of routes that correspond to a mapping of request paths to responses.
public init(protocol: `Protocol`, @RouteBuilder _ route: () -> Route) {
var entries = unsafe TestWebKitAPI.__CxxHTTPServer.ResponseMap()
let routes = route().children
for child in routes {
let path = child.pathComponents.joined()
let response = unsafe TestWebKitAPI.HTTPResponse(WTF.String(child.response))
unsafe hashMapSet(&entries, consuming: .init(path), consuming: response)
}
unsafe self.storage = .init(consuming: entries, .init(`protocol`), consuming: .init(), nil, .init(), .Yes)
}
/// Calls the given closure after starting the server, and then closes the server once finished.
///
/// - Parameter body: A closure that will run while this server is active.
/// - Returns: The return value, if any, of the `body` closure parameter.
/// - Throws: Any error thrown by `body`.
public mutating func run<Result, E>(
_ body: () async throws(E) -> sending Result
) async throws(E) -> sending Result where E: Error, Result: ~Copyable {
await withCheckedContinuation { continuation in
unsafe self.storage.startListening(
consuming: .init(
{
continuation.resume()
},
WTF.ThreadLikeAssertion(WTF.CurrentThreadLike())
)
)
}
let result = try await body()
await withCheckedContinuation { continuation in
unsafe self.storage.cancel(
consuming: .init(
{
continuation.resume()
},
WTF.ThreadLikeAssertion(WTF.CurrentThreadLike())
)
)
}
return result
}
}
extension TestWebKitAPI.__CxxHTTPServer.`Protocol` {
fileprivate init(_ protocol: HTTPServer.`Protocol`) {
self =
switch `protocol` {
case .http: .Http
case .https: .Https
case .httpsWithLegacyTLS: .HttpsWithLegacyTLS
case .http2: .Http2
case .httpsProxy: .HttpsProxy
case .httpsProxyWithAuthentication: .HttpsProxyWithAuthentication
}
}
}
#endif // ENABLE_CXX_INTEROP