diff --git a/Embedded/Package.swift b/Embedded/Package.swift index 33f3de62..617d4945 100644 --- a/Embedded/Package.swift +++ b/Embedded/Package.swift @@ -23,6 +23,11 @@ var pkgDependencies:[Package.Dependency] = [ // Metrics //.package(url: "https://github.com/apple/swift-metrics", from: "2.5.1"), + .package( + url: "https://github.com/RandomHashTags/swift-compression", + branch: "refactor" + ), + // Unlock more performance .package( url: "https://github.com/RandomHashTags/swift-unwrap-arithmetic-operators", @@ -338,6 +343,10 @@ let traits:Set = [ description: "Enables the design protocols." ), + .trait( + name: "Compression", + description: "Enables compression support (using swift-compression)." + ), .trait( name: "Epoll", description: "Enables Epoll functionality (Linux only)." @@ -380,6 +389,7 @@ let package = Package( .product(name: "MediaTypes", package: "swift-media-types", condition: .when(traits: ["MediaTypes"])), .product(name: "UnwrapArithmeticOperators", package: "swift-unwrap-arithmetic-operators"), .product(name: "VariableLengthArray", package: "swift-variablelengtharray"), + .product(name: "SwiftCompressionUtilities", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), @@ -410,7 +420,8 @@ let package = Package( .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftDiagnostics", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacros", package: "swift-syntax") + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompression", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), diff --git a/Package.swift b/Package.swift index 3c0f774c..2c270e7f 100644 --- a/Package.swift +++ b/Package.swift @@ -16,13 +16,18 @@ var pkgDependencies:[Package.Dependency] = [ // Media types .package( url: "https://github.com/RandomHashTags/swift-media-types", - from: "0.1.0", + exact: "0.1.0", traits: ["MediaTypes", "RawValues", "FileExtensionInits", "MediaTypeParsable"] ), // Metrics //.package(url: "https://github.com/apple/swift-metrics", from: "2.5.1"), + .package( + url: "https://github.com/RandomHashTags/swift-compression", + branch: "refactor" + ), + // Unlock more performance .package( url: "https://github.com/RandomHashTags/swift-unwrap-arithmetic-operators", @@ -87,6 +92,7 @@ defaultTraits.formUnion([ "UnwrapArithmetic", "Protocols", + "Compression", "Logging", "OpenAPI" ]) @@ -365,6 +371,10 @@ let traits:Set = [ description: "Enables the design protocols." ), + .trait( + name: "Compression", + description: "Enables compression support (using swift-compression)." + ), .trait( name: "Epoll", description: "Enables Epoll functionality (Linux only)." @@ -403,6 +413,7 @@ var targets = [ .product(name: "MediaTypes", package: "swift-media-types", condition: .when(traits: ["MediaTypes"])), .product(name: "UnwrapArithmeticOperators", package: "swift-unwrap-arithmetic-operators"), .product(name: "VariableLengthArray", package: "swift-variablelengtharray"), + .product(name: "SwiftCompressionUtilities", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), @@ -433,7 +444,8 @@ var targets = [ .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftDiagnostics", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftSyntaxMacros", package: "swift-syntax") + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompression", package: "swift-compression", condition: .when(traits: ["Compression"])) ] ), diff --git a/README.md b/README.md index 0fe188bc..38c996d3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Features like TLS/SSL, Web Sockets and embedded support are coming soon. - [x] Cookies (Aug 9, 2025) - [x] Header parsing (Sep 9, 2025) - [x] Request body streaming (Sep 10, 2025) +- [x] Compression support (Apr 8, 2026) ### WIP @@ -63,7 +64,6 @@ Features like TLS/SSL, Web Sockets and embedded support are coming soon. - [ ] Cache Middleware - [ ] Data Validation (form, POST, etc) - [ ] Authentication -- [ ] Compression support - [ ] OpenAPI support - [ ] Tracing support - [ ] TLS/SSL diff --git a/Sources/Destiny/extensions/Int32Extensions.swift b/Sources/Destiny/extensions/Int32Extensions.swift index c2bb6a6e..e4eab869 100644 --- a/Sources/Destiny/extensions/Int32Extensions.swift +++ b/Sources/Destiny/extensions/Int32Extensions.swift @@ -73,6 +73,20 @@ extension Int32 { } } + public func writeBuffers4( + _ b1: iovec, + _ b2: iovec, + _ b3: iovec, + _ b4: iovec + ) throws(DestinyError) { + let result = withUnsafePointer(to: (b1, b2, b3, b4)) { + writev(fileDescriptor, UnsafePointer(OpaquePointer($0)), 4) + } + if result <= 0 { + throw .socketWriteFailed(errno) + } + } + public func writeBuffers4( _ b1: iovec, _ b2: UnsafeBufferPointer, diff --git a/Sources/Destiny/extensions/SwiftCompressionExtensions.swift b/Sources/Destiny/extensions/SwiftCompressionExtensions.swift index 365552b1..ee415ae3 100644 --- a/Sources/Destiny/extensions/SwiftCompressionExtensions.swift +++ b/Sources/Destiny/extensions/SwiftCompressionExtensions.swift @@ -1,7 +1,7 @@ -/* -import SwiftCompression -import SwiftSyntax +#if Compression + +import SwiftCompressionUtilities extension CompressionAlgorithm { public var acceptEncodingName: String { @@ -10,100 +10,12 @@ extension CompressionAlgorithm { case .huffmanCoding: "huffman" case .lzw: "compress" + case .gzip: "gzip" + case ._7z: "7z" default: rawValue } } } -#if canImport(SwiftSyntax) -// MARK: SwiftSyntax -extension CompressionAlgorithm { - public static func parse(_ expr: some ExprSyntaxProtocol) -> Self? { - let key:String - guard let function = expr.functionCall else { return nil } - if let string = function.calledExpression.memberAccess?.declName.baseName.text { - key = string - } else { - return nil - } - let arguments = function.arguments - switch key { - /*case "aac": self = .aac - case "mp3": self = .mp3 - - case "arithmetic": self = .arithmetic - case "brotli": self = .brotli - - case "bwt": self = .bwt - case "deflate": self = .deflate - case "huffmanCoding": self = .huffman(rootNode: nil) - case "json": self = .json - case "lz4": self = .lz4*/ - case "lz77": - var windowSize:Int = 0, bufferSize:Int = 0, offsetBitWidth:Int = 0 - for child in arguments { - switch child.label?.text { - case "windowSize": windowSize = Int(child.expression.integerLiteral!.literal.text)! - case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text)! - case "offsetBitWidth": offsetBitWidth = Int(child.expression.integerLiteral!.literal.text)! - default: break - } - } - return .lz77(windowSize: windowSize, bufferSize: bufferSize, offsetBitWidth: offsetBitWidth) - /*case "lz78": self = .lz78 - case "lzw": self = .lzw - case "mtf": self = .mtf*/ - case "runLengthEncoding": - var minRun:Int = 0, alwaysIncludeRunCount:Bool = false - for child in arguments { - switch child.label?.text { - case "minRun": minRun = Int(child.expression.integerLiteral!.literal.text)! - case "alwaysIncludeRunCount": alwaysIncludeRunCount = child.expression.booleanLiteral!.isTrue - default: break - } - } - return .runLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) - case "snappy": return CompressionAlgorithm.snappy(windowSize: 32_000) - /*case "snappyFramed": self = .snappyFramed - case "zstd": self = .zstd - - case "_7z": self = ._7z - case "bzip2": self = .bzip2 - case "gzip": self = .gzip - case "rar": self = .rar - - case "h264": self = .h264 - case "h265": self = .h265 - case "jpeg": self = .jpeg - case "jpeg2000": self = .jpeg2000 - - case "eliasDelta": self = .eliasDelta - case "eliasGamma": self = .eliasGamma - case "eliasOmega": self = .eliasOmega - case "fibonacci": self = .fibonacci*/ - - case "dnaBinaryEncoding": - var baseBits:[UInt8:[Bool]] = [:] - for child in arguments { - switch child.label?.text { - case "baseBits": - child.expression.dictionary?.content.as(DictionaryElementListSyntax.self)!.forEach({ - baseBits[UInt8($0.key.integerLiteral!.literal.text)!] = $0.value.array!.elements.map({ $0.expression.booleanLiteral!.isTrue }) - }) - default: break - } - } - return .dnaBinaryEncoding(baseBits: baseBits) - /*case "dnaSingleBlockEncoding": self = .dnaSingleBlockEncoding - - case "boringSSL": self = .boringSSL - - case "av1": self = .av1 - case "dirac": self = .dirac - case "mpeg": self = .mpeg*/ - default: return nil - } - } -} -#endif*/ \ No newline at end of file +#endif \ No newline at end of file diff --git a/Sources/Destiny/http/HTTPResponseMessage.swift b/Sources/Destiny/http/HTTPResponseMessage.swift index 13bb1ba4..9e8212e0 100644 --- a/Sources/Destiny/http/HTTPResponseMessage.swift +++ b/Sources/Destiny/http/HTTPResponseMessage.swift @@ -113,6 +113,7 @@ extension HTTPResponseMessage { status: head.status, headers: head.headers, body: bodyString, + contentLength: body?.count ?? 0, contentType: contentType, charset: charset ) @@ -238,11 +239,21 @@ extension HTTPResponseMessage { status: HTTPResponseStatus.Code, headers: HTTPHeaders, body: String?, + contentLength: Int, contentType: String?, charset: Charset? ) -> String { let suffix = escapeLineBreak ? "\\r\\n" : "\r\n" - return create(suffix: suffix, version: version, status: status, headers: Self.headers(suffix: suffix, headers: headers), body: body, contentType: contentType, charset: charset) + return create( + suffix: suffix, + version: version, + status: status, + headers: Self.headers(suffix: suffix, headers: headers), + body: body, + contentLength: contentLength, + contentType: contentType, + charset: charset + ) } public static func create( @@ -251,12 +262,12 @@ extension HTTPResponseMessage { status: HTTPResponseStatus.Code, headers: String, body: String?, + contentLength: Int, contentType: String?, charset: Charset? ) -> String { var string = "\(version.string) \(status)\(suffix)\(headers)" if let body { - let contentLength = body.utf8Span.count if let contentType { string += "content-type: \(contentType)\((charset != nil ? "; charset=" + charset!.rawName : ""))\(suffix)" } diff --git a/Sources/Destiny/http/HTTPSocket.swift b/Sources/Destiny/http/HTTPSocket.swift index 3e77f72a..3b3d84ba 100644 --- a/Sources/Destiny/http/HTTPSocket.swift +++ b/Sources/Destiny/http/HTTPSocket.swift @@ -128,6 +128,15 @@ extension HTTPSocket { try fileDescriptor.writeBuffers3(b1, b2, b3) } + public func writeBuffers4( + _ b1: iovec, + _ b2: iovec, + _ b3: iovec, + _ b4: iovec + ) throws(DestinyError) { + try fileDescriptor.writeBuffers4(b1, b2, b3, b4) + } + public func writeBuffers4( _ b1: iovec, _ b2: UnsafeBufferPointer, diff --git a/Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift b/Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift new file mode 100644 index 00000000..4e4a57bf --- /dev/null +++ b/Sources/Destiny/responders/noncopyable/NonCopyableCompressedBody.swift @@ -0,0 +1,28 @@ + +#if canImport(Android) +import Android +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Windows) +import Windows +#elseif canImport(WinSDK) +import WinSDK +#endif + +public struct NonCopyableCompressedBody: Sendable { + let bytes:[count of UInt8] + + public func iovec(_ closure: (iovec) -> Void) { + bytes.span.withUnsafeBufferPointer { + closure(.init(iov_base: .init(mutating: $0.baseAddress), iov_len: $0.count)) + } + } +} \ No newline at end of file diff --git a/Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift b/Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift new file mode 100644 index 00000000..95c51bb0 --- /dev/null +++ b/Sources/Destiny/responders/noncopyable/NonCopyableDateHeaderPayloadWithBody.swift @@ -0,0 +1,71 @@ + +#if NonCopyableDateHeaderPayload + +#if canImport(Android) +import Android +#elseif canImport(Bionic) +import Bionic +#elseif canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(WASILibc) +import WASILibc +#elseif canImport(Windows) +import Windows +#elseif canImport(WinSDK) +import WinSDK +#endif + +/// Default storage to efficiently handle the `date` header payload for responders. +public struct NonCopyableDateHeaderPayloadWithBody: @unchecked Sendable, ~Copyable { + @usableFromInline let preDatePointer:UnsafePointer + @usableFromInline let postDatePointer:UnsafePointer + @usableFromInline let preDateIovec:iovec + @usableFromInline let postDateIovec:iovec + @usableFromInline let body:[count of UInt8] + + public init( + preDate: StaticString, + postDate: StaticString, + body: [count of UInt8] + ) { + self.preDatePointer = preDate.utf8Start + self.postDatePointer = postDate.utf8Start + self.preDateIovec = .init(iov_base: .init(mutating: preDate.utf8Start), iov_len: preDate.utf8CodeUnitCount) + self.postDateIovec = .init(iov_base: .init(mutating: postDate.utf8Start), iov_len: postDate.utf8CodeUnitCount) + self.body = body + } + + package init( + _ payload: borrowing Self + ) { + self.preDatePointer = payload.preDatePointer + self.postDatePointer = payload.postDatePointer + self.preDateIovec = payload.preDateIovec + self.postDateIovec = payload.postDateIovec + self.body = payload.body + } + + /// Efficiently writes the `preDate` value, `date` header and `postDate` value to a file descriptor. + /// + /// - Throws: `DestinyError` + public func write(to socket: some FileDescriptor) throws(DestinyError) { + do { // TODO: fix + try body.span.withUnsafeBufferPointer { + try socket.writeBuffers4( + preDateIovec, + HTTPDateFormat.nowIovec, + postDateIovec, + .init(iov_base: .init(mutating: $0.baseAddress), iov_len: $0.count) + ) + } + } catch { + throw .custom("\(error)") + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift b/Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift new file mode 100644 index 00000000..abe52a0d --- /dev/null +++ b/Sources/Destiny/responders/noncopyable/NonCopyableStaticStringWithDateHeaderAndCompressedBody.swift @@ -0,0 +1,64 @@ + +#if NonCopyableStaticStringWithDateHeader + +import UnwrapArithmeticOperators + +public struct NonCopyableStaticStringWithDateHeaderAndCompressedBody: Sendable, ~Copyable { + public let payload:NonCopyableDateHeaderPayloadWithBody + + public init( + preDateValue: StaticString, + postDateValue: StaticString, + body: [count of UInt8] + ) { + payload = .init( + preDate: preDateValue, + postDate: postDateValue, + body: body + ) + } + + public var count: Int { + payload.preDateIovec.iov_len +! HTTPDateFormat.InlineArrayResult.count +! payload.postDateIovec.iov_len + count + } + + public func string() -> String { + "\(String(cString: payload.preDatePointer))\(HTTPDateFormat.placeholder)\(String(cString: payload.postDatePointer))" + } + + public var hasDateHeader: Bool { + true + } +} + +// MARK: Write to buffer +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody { + public func write(to buffer: UnsafeMutableBufferPointer, at index: inout Int) { + index = 0 + buffer.copyBuffer(baseAddress: payload.preDatePointer, count: payload.preDateIovec.iov_len, at: &index) + buffer.copyBuffer(baseAddress: HTTPDateFormat.nowUnsafeBufferPointer.baseAddress!, count: HTTPDateFormat.count, at: &index) + buffer.copyBuffer(baseAddress: payload.postDatePointer, count: payload.postDateIovec.iov_len, at: &index) + } +} + +// MARK: Respond +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody { + public func respond( + provider: some SocketProvider, + router: borrowing some NonCopyableHTTPRouterProtocol & ~Copyable, + request: inout HTTPRequest + ) throws(DestinyError) { + try payload.write(to: request.fileDescriptor) + request.fileDescriptor.flush(provider: provider) + } +} + +#if Protocols + +// MARK: Conformances +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody: ResponseBodyProtocol {} +extension NonCopyableStaticStringWithDateHeaderAndCompressedBody: NonCopyableRouteResponderProtocol {} + +#endif + +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/CompressionSettings.swift b/Sources/Destiny/util/CompressionSettings.swift new file mode 100644 index 00000000..fa61fc2b --- /dev/null +++ b/Sources/Destiny/util/CompressionSettings.swift @@ -0,0 +1,71 @@ + +#if Compression + +import SwiftCompressionUtilities + +public struct CompressionSettings: Sendable { + package var flags:Flags.RawValue + public package(set) var supportedCompressionAlgorithms:[CompressionAlgorithm:CompressorSettings] + + public init( + enabled: Bool = true, + compressOnlyIfResultIsSmaller: Bool = true, + supportedCompressionAlgorithms: [CompressionAlgorithm:CompressorSettings] = [ + .brotli( + quality: 11, // BROTLI_DEFAULT_QUALITY + windowSize: 22, // BROTLI_DEFAULT_WINDOW + mode: 0 // BROTLI_MODE_GENERIC + ): .init(contentTypePrefixWhitelist: "text/"), + .gzip( + bufferSize: 1024, + level: -1, // Z_DEFAULT_COMPRESSION + memLevel: 8, + strategy: 0 // Z_DEFAULT_STRATEGY + ): .init(contentTypePrefixWhitelist: "text/") + ] + ) { + flags = Flags.pack( + enabled: enabled, + compressOnlyIfResultIsSmaller: compressOnlyIfResultIsSmaller + ) + self.supportedCompressionAlgorithms = supportedCompressionAlgorithms + } + + public var isEnabled: Bool { + isFlag(.enabled) + } + public var compressOnlyIfResultIsSmaller: Bool { + isFlag(.compressOnlyIfResultIsSmaller) + } + + func isFlag(_ flag: Flags) -> Bool { + flags & flag.rawValue != 0 + } + + mutating func setFlag(_ flag: Flags, _ value: Bool) { + if value { + flags |= flag.rawValue + } else { + flags &= ~flag.rawValue + } + } +} + +// MARK: Flags +extension CompressionSettings { + package enum Flags: UInt8 { + case enabled = 1 + case compressOnlyIfResultIsSmaller = 2 + } +} +extension CompressionSettings.Flags { + package static func pack( + enabled: Bool, + compressOnlyIfResultIsSmaller: Bool + ) -> RawValue { + (enabled ? Self.enabled.rawValue : 0) + | (compressOnlyIfResultIsSmaller ? Self.compressOnlyIfResultIsSmaller.rawValue : 0) + } +} + +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/CompressorSettings.swift b/Sources/Destiny/util/CompressorSettings.swift new file mode 100644 index 00000000..3b9b90e9 --- /dev/null +++ b/Sources/Destiny/util/CompressorSettings.swift @@ -0,0 +1,26 @@ + +#if Compression + +public struct CompressorSettings: Sendable { + public package(set) var contentLengthThreshold:Int? = nil + public package(set) var contentTypePrefixWhitelist:String? + public package(set) var contentTypePrefixBlacklist:String? + public package(set) var contentTypeWhitelist:Set + public package(set) var contentTypeBlacklist:Set + + public init( + contentLengthThreshold: Int? = nil, + contentTypePrefixWhitelist: String? = nil, + contentTypeWhitelist: Set = [], + contentTypePrefixBlacklist: String? = nil, + contentTypeBlacklist: Set = [] + ) { + self.contentLengthThreshold = contentLengthThreshold + self.contentTypePrefixWhitelist = contentTypePrefixWhitelist + self.contentTypeWhitelist = contentTypeWhitelist + self.contentTypePrefixBlacklist = contentTypePrefixBlacklist + self.contentTypeBlacklist = contentTypeBlacklist + } +} + +#endif \ No newline at end of file diff --git a/Sources/Destiny/util/RouterSettings.swift b/Sources/Destiny/util/RouterSettings.swift index dee8329a..e1bc8280 100644 --- a/Sources/Destiny/util/RouterSettings.swift +++ b/Sources/Destiny/util/RouterSettings.swift @@ -12,25 +12,10 @@ public struct RouterSettings: Sendable { /// Access control for the router. public var visibility:RouterVisibility - public init( - mutable: Bool = false, - dynamicResponsesAreGeneric: Bool = true, - respondersAreComputedProperties: Bool = false, - protocolConformances: Bool = true, - logging: Bool = true, - visibility: RouterVisibility = .internal, - name: String = "CompiledHTTPRouter" - ) { - self.visibility = visibility - self.name = name - flags = Flags.pack( - mutable: mutable, - dynamicResponsesAreGeneric: dynamicResponsesAreGeneric, - respondersAreComputedProperties: respondersAreComputedProperties, - protocolConformances: protocolConformances, - logging: logging - ) - } + #if Compression + package var compression:CompressionSettings + #endif + /// Whether or not this router is mutable. /// @@ -76,6 +61,53 @@ public struct RouterSettings: Sendable { } } +// MARK: Init +extension RouterSettings { + #if Compression + public init( + mutable: Bool = false, + dynamicResponsesAreGeneric: Bool = true, + respondersAreComputedProperties: Bool = false, + protocolConformances: Bool = true, + logging: Bool = true, + visibility: RouterVisibility = .internal, + name: String = "CompiledHTTPRouter", + compression: CompressionSettings = .init() + ) { + self.visibility = visibility + self.name = name + self.compression = compression + flags = Flags.pack( + mutable: mutable, + dynamicResponsesAreGeneric: dynamicResponsesAreGeneric, + respondersAreComputedProperties: respondersAreComputedProperties, + protocolConformances: protocolConformances, + logging: logging + ) + } + #else + public init( + mutable: Bool = false, + dynamicResponsesAreGeneric: Bool = true, + respondersAreComputedProperties: Bool = false, + protocolConformances: Bool = true, + logging: Bool = true, + visibility: RouterVisibility = .internal, + name: String = "CompiledHTTPRouter" + ) { + self.visibility = visibility + self.name = name + flags = Flags.pack( + mutable: mutable, + dynamicResponsesAreGeneric: dynamicResponsesAreGeneric, + respondersAreComputedProperties: respondersAreComputedProperties, + protocolConformances: protocolConformances, + logging: logging + ) + } + #endif +} + // MARK: Flags extension RouterSettings { @usableFromInline diff --git a/Sources/Destiny/util/protocols/FileDescriptor.swift b/Sources/Destiny/util/protocols/FileDescriptor.swift index 284b1842..0b2eb960 100644 --- a/Sources/Destiny/util/protocols/FileDescriptor.swift +++ b/Sources/Destiny/util/protocols/FileDescriptor.swift @@ -62,6 +62,16 @@ public protocol FileDescriptor: NetworkAddressable, ~Copyable { _ b3: iovec ) throws(DestinyError) + /// Efficiently writes 4 buffers to the file descriptor. + /// + /// - Throws: `DestinyError` + func writeBuffers4( + _ b1: iovec, + _ b2: iovec, + _ b3: iovec, + _ b4: iovec + ) throws(DestinyError) + /// Efficiently writes 4 buffers to the file descriptor. /// /// - Throws: `DestinyError` diff --git a/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift b/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift new file mode 100644 index 00000000..06fc8190 --- /dev/null +++ b/Sources/DestinyMacros/extensions/CompressionAlgorithm+Compress.swift @@ -0,0 +1,62 @@ + +#if Compression + +import SwiftCompression + +extension CompressionAlgorithm { + func compress(span: Span) -> [UInt8]? { + switch self { + + case .brotli(let quality, let windowSize, let mode): + #if canImport(Brotli) + return Brotli(quality: quality, windowSize: windowSize, mode: mode) + .compress(span, configuration: .default) + #else + return nil + #endif + + case .deflate(let bufferSize, let level): + #if canImport(Zlib) + return Deflate(bufferSize: bufferSize, level: level).compress(span, configuration: .default) + #else + return nil + #endif + + case .lz77(let searchBufferSize, let lookaheadBufferSize, let offsetBitWidth): + #if canImport(CompressionLZ) + switch offsetBitWidth { + case 8: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 16: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 32: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 64: + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + case 128: + if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { + return LZ77(searchBufferSize: searchBufferSize, lookaheadBufferSize: lookaheadBufferSize).compress(span, configuration: .default) + } + return nil + default: + return nil + } + #else + return nil + #endif + + case .gzip(let bufferSize, let level, let memLevel, let strategy): + #if canImport(Zlib) + return Gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) + .compress(span, configuration: .default) + #else + return nil + #endif + + default: + return nil + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift b/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift index a03229c5..c11a9a52 100644 --- a/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift +++ b/Sources/DestinyMacros/extensions/HTTPResponseMessageExtensions.swift @@ -3,7 +3,10 @@ import Destiny // MARK: HTTPResponseMessage extension HTTPResponseMessage { - public func intermediateString(escapeLineBreak: Bool) -> String { + public func headString( + escapeLineBreak: Bool, + contentLength: Int + ) -> String { let suffix = escapeLineBreak ? "\\r\\n" : "\r\n" var string = head.string(suffix: suffix) if let body { @@ -11,7 +14,7 @@ extension HTTPResponseMessage { string += "content-type: \(contentType)\((charset != nil ? "; charset=\(charset!.rawName)" : ""))\(suffix)" } if body.hasContentLength { - string += "content-length: \(body.count)\(suffix)\(suffix)" + string += "content-length: \(contentLength)\(suffix)\(suffix)" } } return string diff --git a/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift new file mode 100644 index 00000000..82e6f03d --- /dev/null +++ b/Sources/DestinyMacros/parse/CompressionAlgorithm+Parse.swift @@ -0,0 +1,134 @@ + +#if Compression + +import BrotliShim +import SwiftCompressionUtilities +import SwiftSyntax +import ZlibShim + +// MARK: SwiftSyntax +extension CompressionAlgorithm { + public static func parse( + _ expr: some ExprSyntaxProtocol + ) -> Self? { + let key:String + guard let function = expr.functionCall else { return nil } + if let string = function.calledExpression.memberAccess?.declName.baseName.text { + key = string + } else { + return nil + } + let arguments = function.arguments + switch key { + /*case "aac": self = .aac + case "mp3": self = .mp3 + + case "arithmetic": self = .arithmetic + + case "bwt": self = .bwt + case "deflate": self = .deflate + case "huffmanCoding": self = .huffman(rootNode: nil) + case "json": self = .json + case "lz4": self = .lz4*/ + case "brotli": + var quality:Int32 = BROTLI_DEFAULT_QUALITY + var windowSize:Int32 = BROTLI_DEFAULT_WINDOW + var mode:UInt32 = BROTLI_MODE_GENERIC.rawValue + for child in arguments { + switch child.label?.text { + case "quality": quality = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "windowSize": windowSize = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "mode": mode = UInt32(child.expression.integerLiteral!.literal.text) ?? 0 + default: break + } + } + return .brotli(quality: quality, windowSize: windowSize, mode: mode) + case "lz77": + var windowSize = 0 + var bufferSize = 0 + var offsetBitWidth = 0 + for child in arguments { + switch child.label?.text { + case "windowSize": windowSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "offsetBitWidth": offsetBitWidth = Int(child.expression.integerLiteral!.literal.text) ?? 0 + default: break + } + } + return .lz77(windowSize: windowSize, bufferSize: bufferSize, offsetBitWidth: offsetBitWidth) + /*case "lz78": self = .lz78 + case "lzw": self = .lzw + case "mtf": self = .mtf*/ + + case "gzip": + var bufferSize = 1024 + var level = Z_DEFAULT_COMPRESSION + var memLevel:Int32 = 8 + var strategy = Z_DEFAULT_STRATEGY + for child in arguments { + switch child.label?.text { + case "bufferSize": bufferSize = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "level": level = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "memLevel": memLevel = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + case "strategy": strategy = Int32(child.expression.integerLiteral!.literal.text) ?? 0 + default: + break + } + } + return .gzip(bufferSize: bufferSize, level: level, memLevel: memLevel, strategy: strategy) + + case "runLengthEncoding": + var minRun = 0 + var alwaysIncludeRunCount:Bool = false + for child in arguments { + switch child.label?.text { + case "minRun": minRun = Int(child.expression.integerLiteral!.literal.text) ?? 0 + case "alwaysIncludeRunCount": alwaysIncludeRunCount = child.expression.booleanIsTrue + default: break + } + } + return .runLengthEncoding(minRun: minRun, alwaysIncludeRunCount: alwaysIncludeRunCount) + case "snappy": return CompressionAlgorithm.snappy + /*case "snappyFramed": self = .snappyFramed + case "zstd": self = .zstd + + case "_7z": self = ._7z + case "bzip2": self = .bzip2 + case "gzip": self = .gzip + case "rar": self = .rar + + case "h264": self = .h264 + case "h265": self = .h265 + case "jpeg": self = .jpeg + case "jpeg2000": self = .jpeg2000 + + case "eliasDelta": self = .eliasDelta + case "eliasGamma": self = .eliasGamma + case "eliasOmega": self = .eliasOmega + case "fibonacci": self = .fibonacci*/ + + case "dnaBinaryEncoding": + var baseBits:[UInt8:UInt8] = [:] + for child in arguments { + switch child.label?.text { + case "baseBits": + child.expression.dictionary?.content.as(DictionaryElementListSyntax.self)!.forEach({ + baseBits[UInt8($0.key.integerLiteral!.literal.text)!] = UInt8($0.value.integerLiteral!.literal.text) + }) + default: break + } + } + return .dnaBinaryEncoding(baseBits: baseBits) + /*case "dnaSingleBlockEncoding": self = .dnaSingleBlockEncoding + + case "boringSSL": self = .boringSSL + + case "av1": self = .av1 + case "dirac": self = .dirac + case "mpeg": self = .mpeg*/ + default: return nil + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift new file mode 100644 index 00000000..9a5a9512 --- /dev/null +++ b/Sources/DestinyMacros/parse/CompressionSettings+Parse.swift @@ -0,0 +1,47 @@ + +#if Compression + +import Destiny +import SwiftCompressionUtilities +import SwiftSyntax +import SwiftSyntaxMacros + +extension CompressionSettings { + public static func parse( + context: some MacroExpansionContext, + expr: some ExprSyntaxProtocol + ) -> Self { + guard let function = expr.functionCall else { + return Self(enabled: false, compressOnlyIfResultIsSmaller: false, supportedCompressionAlgorithms: [:]) + } + var settings = Self() + var enabled = true + var compressOnlyIfResultIsSmaller = true + for arg in function.arguments { + switch arg.label?.text { + case "enabled": + enabled = arg.expression.booleanIsTrue + case "compressOnlyIfResultIsSmaller": + compressOnlyIfResultIsSmaller = arg.expression.booleanIsTrue + case "supportedCompressionAlgorithms": + settings.supportedCompressionAlgorithms = [:] + guard let dict = arg.expression.dictionary else { continue } + switch dict.content { + case .elements(let elements): + for e in elements { + guard let algorithm = CompressionAlgorithm.parse(e.key) else { continue } + settings.supportedCompressionAlgorithms[algorithm] = CompressorSettings.parse(context: context, expr: e.value) + } + default: + break + } + default: + break + } + } + settings.flags = Self.Flags.pack(enabled: enabled, compressOnlyIfResultIsSmaller: compressOnlyIfResultIsSmaller) + return settings + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift b/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift new file mode 100644 index 00000000..2c749500 --- /dev/null +++ b/Sources/DestinyMacros/parse/CompressorSettings+Parse.swift @@ -0,0 +1,37 @@ + +#if Compression + +import Destiny +import SwiftSyntax +import SwiftSyntaxMacros + +extension CompressorSettings { + public static func parse( + context: some MacroExpansionContext, + expr: some ExprSyntaxProtocol + ) -> Self { + var settings = Self() + guard let function = expr.functionCall else { return settings } + for arg in function.arguments { + switch arg.label?.text { + case "contentLengthThreshold": + settings.contentLengthThreshold = Int(arg.expression.integerLiteral!.literal.text) ?? 0 + case "contentTypePrefixWhitelist": + settings.contentTypePrefixWhitelist = arg.expression.stringLiteralString(context: context) + case "contentTypeWhitelist": + guard let array = arg.expression.arrayElements(context: context)?.compactMap({ $0.expression.stringLiteralString(context: context) }) else { continue } + settings.contentTypeWhitelist = Set(array) + case "contentTypePrefixBlacklist": + settings.contentTypePrefixBlacklist = arg.expression.stringLiteralString(context: context) + case "contentTypeBlacklist": + guard let array = arg.expression.arrayElements(context: context)?.compactMap({ $0.expression.stringLiteralString(context: context) }) else { continue } + settings.contentTypeBlacklist = Set(array) + default: + break + } + } + return settings + } +} + +#endif \ No newline at end of file diff --git a/Sources/DestinyMacros/parse/Route+Parse.swift b/Sources/DestinyMacros/parse/Route+Parse.swift index 1e33cfa0..01a68406 100644 --- a/Sources/DestinyMacros/parse/Route+Parse.swift +++ b/Sources/DestinyMacros/parse/Route+Parse.swift @@ -212,8 +212,10 @@ extension Route { if withDateHeader { // auto-upgrade switch body?.type { - case .string: body!.type = .stringWithDateHeader - case .staticString: body!.type = .staticStringWithDateHeader + case .string(let isNonCopyable, false, false, let withCompressedBody): + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: false, withDateHeader: true, withCompressedBody: withCompressedBody) + case .string(let isNonCopyable, true, false, let withCompressedBody): + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: true, withDateHeader: true, withCompressedBody: withCompressedBody) default: break } } diff --git a/Sources/DestinyMacros/parse/RouterSettings+Parse.swift b/Sources/DestinyMacros/parse/RouterSettings+Parse.swift index f6666343..16192973 100644 --- a/Sources/DestinyMacros/parse/RouterSettings+Parse.swift +++ b/Sources/DestinyMacros/parse/RouterSettings+Parse.swift @@ -30,6 +30,12 @@ extension RouterSettings { } case "visibility": settings.visibility = .init(rawValue: arg.expression.memberAccess?.declName.baseName.text ?? "internal") ?? .internal + + #if Compression + case "compression": + settings.compression = .parse(context: context, expr: arg.expression) + #endif + default: context.diagnose(DiagnosticMsg.unhandled(node: arg)) } diff --git a/Sources/DestinyMacros/router/Router+Compute.swift b/Sources/DestinyMacros/router/Router+Compute.swift index 8fa301ed..734d53a3 100644 --- a/Sources/DestinyMacros/router/Router+Compute.swift +++ b/Sources/DestinyMacros/router/Router+Compute.swift @@ -309,7 +309,10 @@ extension Router { let contentType = "text/plain" let charset = Charset.utf8 let stringLiteral = StringLiteralExprSyntax(content: body) - let intermediateBody = IntermediateResponseBody(type: .staticStringWithDateHeader, .init(stringLiteral)) + let intermediateBody = IntermediateResponseBody( + type: .string(isNonCopyable: false, isStatic: true, withDateHeader: true, withCompressedBody: false), + .init(stringLiteral) + ) #if hasFeature(Embedded) || EMBEDDED let response:HTTPResponseMessage diff --git a/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift b/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift index 255a018e..32720c31 100644 --- a/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift +++ b/Sources/DestinyMacros/router/Router+Routes+PerfectHash.swift @@ -83,7 +83,7 @@ extension RouterStorage { routePaths.reserveCapacity(reservedCapacity) routeResponders.reserveCapacity(reservedCapacity) - appendStaticRoutes( + let appendedStaticRoutes = appendStaticRoutes( context: context, isCaseSensitive: isCaseSensitive, isCopyable: isCopyable, @@ -105,6 +105,7 @@ extension RouterStorage { staticConstants( isCaseSensitive: isCaseSensitive, isCopyable: isCopyable, + routes: appendedStaticRoutes, routePaths: routePaths, members: &members, routeResponders: routeResponders @@ -134,6 +135,7 @@ extension RouterStorage { private func staticConstants( isCaseSensitive: Bool, isCopyable: Bool, + routes: StaticAppendedRoutes, routePaths: [String], members: inout MemberBlockItemListSyntax, routeResponders: [String] diff --git a/Sources/DestinyMacros/router/Router+Routes+Static.swift b/Sources/DestinyMacros/router/Router+Routes+Static.swift index df2add98..459a188e 100644 --- a/Sources/DestinyMacros/router/Router+Routes+Static.swift +++ b/Sources/DestinyMacros/router/Router+Routes+Static.swift @@ -150,7 +150,7 @@ extension RouterStorage { routes: [(StaticRoute, FunctionCallExprSyntax)], routePaths: inout [String], routeResponders: inout [String] - ) { + ) -> StaticAppendedRoutes { appendStaticRoutes( context: context, isCaseSensitive: isCaseSensitive, @@ -161,6 +161,8 @@ extension RouterStorage { routeResponders: &routeResponders ) } + + @discardableResult mutating func appendStaticRoutes( context: some MacroExpansionContext, isCaseSensitive: Bool, @@ -169,16 +171,18 @@ extension RouterStorage { routes: [(StaticRoute, FunctionCallExprSyntax)], routePaths: inout [String], routeResponders: inout [String] - ) { + ) -> StaticAppendedRoutes { let getResponderValue:(RouterStorage.Route) -> String = { "// \($0.startLine)\nCompiledStaticResponderStorageRoute(\npath: \($0.buffer),\nresponder: \($0.responder)\n)" } + var appended = StaticAppendedRoutes() if !isCopyable { // always make redirects noncopyable for optimal performance #if StaticRedirectionRoute appendStaticRedirects( context: context, isCaseSensitive: isCaseSensitive, isCopyable: isCopyable, + appended: &appended, routePaths: &routePaths, routeResponders: &routeResponders, data: data, @@ -186,10 +190,10 @@ extension RouterStorage { ) #endif } - for (route, function) in routes { + for (var route, function) in routes { let startLine = data.routeStartLine(route) #if StaticMiddleware - let httpResponse = route.response(context: context, function: function, middleware: staticMiddleware) + let httpResponse = route.response(context: context, function: function, routerStorage: self, middleware: staticMiddleware) #else let httpResponse = route.response(context: context, function: function) #endif @@ -211,6 +215,7 @@ extension RouterStorage { routePaths.append(startLine) routeResponders.append(responder) staticRouteStorage.remove(isCaseSensitive: isCaseSensitive, path: route.path, function: function) + appended.normal.append(route) } else { guard !registeredPaths.contains(startLine) else { Router.routePathAlreadyRegistered(context: context, node: function, startLine) @@ -229,6 +234,7 @@ extension RouterStorage { )*/ } } + return appended } } @@ -240,6 +246,7 @@ extension RouterStorage { context: some MacroExpansionContext, isCaseSensitive: Bool, isCopyable: Bool, + appended: inout StaticAppendedRoutes, routePaths: inout [String], routeResponders: inout [String], data: borrowing SharedStaticRouteResponderData, @@ -263,10 +270,11 @@ extension RouterStorage { let stringLiteral = StringLiteralExprSyntax(content: "") let responder = IntermediateResponseBody( - type: .staticStringWithDateHeader, + type: .string(isNonCopyable: false, isStatic: true, withDateHeader: true, withCompressedBody: false), .init(stringLiteral) ).responderDebugDescription(context: context, isCopyable: isCopyable, response: route.response()) routeResponders.append(responder) + appended.redirects.append(redirect.0) } for i in removedRedirects.reversed() { staticRedirects.remove(at: i) @@ -274,4 +282,15 @@ extension RouterStorage { } } -#endif \ No newline at end of file +#endif + +// MARK: Appended routes +extension RouterStorage { + struct StaticAppendedRoutes: Sendable { + #if StaticRedirectionRoute + var redirects = [StaticRedirectionRoute]() + #endif + + var normal = [StaticRoute]() + } +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift new file mode 100644 index 00000000..1f4a1a37 --- /dev/null +++ b/Sources/DestinyMacros/util/IntermediateResponseBody+ResponderDebugDescription.swift @@ -0,0 +1,173 @@ + +import Destiny +import SwiftSyntaxMacros + +extension IntermediateResponseBody { + private func preDateAndPostDateValues(_ string: String) -> (preDate: Substring, postDate: Substring) { + let preDate = string[string.startIndex.. String { + let prefix = isCopyable ? "" : "NonCopyable" + switch type { + case .bytes: + return "\(prefix)Bytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .inlineBytes: + return "\(prefix)InlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .macroExpansion: + responseString.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count + return "RouteResponses.\(prefix)MacroExpansion(\"\(responseString)\", body: \(value))" + case .macroExpansionWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues(responseString) + postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count + return "\(prefix)MacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" + case .streamWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues(responseString) + postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" + return "\(prefix)StreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" + + case .nonCopyableBytes: + return "NonCopyableBytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .nonCopyableInlineBytes: + return "NonCopyableInlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" + case .nonCopyableMacroExpansionWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues("\(responseString)") + postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count + return "NonCopyableMacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" + case .nonCopyableStreamWithDateHeader: + var (preDate, postDate) = preDateAndPostDateValues(responseString) + postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" + return "NonCopyableStreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" + + case .string(let isNonCopyable, let isStatic, let withDateHeader, let withCompressedBody): + let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" + var (preDate, postDate):(Substring, Substring) + var trailingSuffix:Substring = "" + var targetType:String + if isStatic || interpolation == 0 { + targetType = "StaticString" + (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") + } else { + targetType = "String" + (preDate, postDate) = preDateAndPostDateValues(responseString) + } + if withDateHeader { + targetType += "WithDateHeader" + } + if !postDate.hasSuffix(delimiter) { + postDate += delimiter + } + if withCompressedBody { + targetType += "AndCompressedBody" + (preDate, postDate) = preDateAndPostDateValues(responseString) + trailingSuffix = ", body: \(rawValue ?? [])" + } + return "\(isNonCopyable ? "NonCopyable" : prefix)\(targetType)(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter)\(trailingSuffix))" + } + } + private func escapedValue() -> String { + if let rawValue { + var s = "" + for b in rawValue { + let hex = Self.byteToHex(b) + s.append("\\u{\(hex.high)\(hex.low)}") // TODO: fix | bytes > 127 get encoded as two bytes + } + return s + } + var string = value + guard valueExpr.stringLiteral?.openingPounds == nil else { + // don't escape the string if it uses pound delimiters + return string + } + string.replace("\"", with: "\\\"") + return string + } + + private func bytesPayload( + context: some MacroExpansionContext, + responseString: inout String + ) -> [UInt8] { + var payload = [UInt8]() + payload.reserveCapacity(responseString.count) + responseString.withUTF8 { + payload.append(contentsOf: $0) + } + if let elements = valueExpr.array?.elements { + for element in elements { + if let s = element.expression.integerLiteral?.literal.text, let byte = UInt8(s) { + payload.append(byte) + } else if let s = element.expression.memberAccess?.declName.baseName.text, let byte = UInt8(convenientName: s) { + payload.append(byte) + } else { + context.diagnose(DiagnosticMsg.unhandled(node: element.expression)) + } + } + } else { + context.diagnose(DiagnosticMsg.unhandled(node: valueExpr)) + } + return payload + } +} + +extension IntermediateResponseBody { + #if hasFeature(Embedded) || EMBEDDED + public func responderDebugDescription( + context: some MacroExpansionContext, + isCopyable: Bool, + response: HTTPResponseMessage + ) -> String { + let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) + var responseString = response.intermediateString( + escapeLineBreak: escapeLineBreak, + contentLength: count + ) + return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) + } + #else + public func responderDebugDescription( + context: some MacroExpansionContext, + isCopyable: Bool, + response: HTTPResponseMessage + ) -> String { + let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) + var responseString = response.headString( + escapeLineBreak: escapeLineBreak, + contentLength: count + ) + return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) + } + #endif +} + +// MARK: Byte to hex +extension IntermediateResponseBody { + private static let hexDigits:[16 of Character] = [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F" + ] + private static func byteToHex(_ byte: UInt8) -> (high: Character, low: Character) { + let high = hexDigits[unchecked: Int(byte >> 4)] + let low = hexDigits[unchecked: Int(byte & 0x0F)] + return (high, low) + } +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/IntermediateResponseBody.swift b/Sources/DestinyMacros/util/IntermediateResponseBody.swift index 4a284c04..2f06f10b 100644 --- a/Sources/DestinyMacros/util/IntermediateResponseBody.swift +++ b/Sources/DestinyMacros/util/IntermediateResponseBody.swift @@ -8,8 +8,13 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { public let valueExpr:ExprSyntax public var type:IntermediateResponseBodyType let value:String - public let count:Int - private var interpolation = 0 + public private(set) var count:Int + var interpolation = 0 + var rawValue:[UInt8]? = nil { + didSet { + count = rawValue?.count ?? count + } + } public init( type: IntermediateResponseBodyType, @@ -29,18 +34,20 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { self.value = valueString self.count = valueString.count - count } - private init( + init( valueExpr: ExprSyntax, type: IntermediateResponseBodyType, value: String, count: Int, - interpolation: Int + interpolation: Int, + rawValue: [UInt8]? = nil ) { self.valueExpr = valueExpr self.type = type self.value = value - self.count = count + self.count = rawValue?.count ?? count self.interpolation = interpolation + self.rawValue = rawValue } private static func upgradeSegments(_ list: StringLiteralSegmentListSyntax) -> (String, Int) { @@ -91,28 +98,22 @@ public struct IntermediateResponseBody: ResponseBodyProtocol { public func write(to buffer: UnsafeMutableBufferPointer, at index: inout Int) { } - private func preDateAndPostDateValues(_ string: String) -> (preDate: Substring, postDate: Substring) { - let preDate = string[string.startIndex.. String { - let prefix = isCopyable ? "" : "NonCopyable" - switch type { - case .bytes: - return "\(prefix)Bytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .inlineBytes: - return "\(prefix)InlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .macroExpansion: - responseString.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count - return "RouteResponses.\(prefix)MacroExpansion(\"\(responseString)\", body: \(value))" - case .macroExpansionWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues(responseString) - postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count - return "\(prefix)MacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" - case .streamWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues(responseString) - postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" - return "\(prefix)StreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" - case .stringWithDateHeader: - if interpolation == 0 { - // upgrade - return IntermediateResponseBody( - valueExpr: valueExpr, - type: .staticStringWithDateHeader, - value: escapedValue(), - count: count, - interpolation: interpolation - ).responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues(responseString) - return "StringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter), value: \(delimiter)\"\(escapedValue())\"\(delimiter))" - case .staticString: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - return "StaticString(\(delimiter)\"\(responseString)\(escapedValue())\"\(delimiter))" - case .staticStringWithDateHeader: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") - return "\(prefix)StaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" - - case .string: - var s = responseString + value - if s.first != "\"" { - s.insert("\"", at: s.startIndex) - } - if s.last != "\"" { - s.append("\"") - } - if let stringLiteral = valueExpr.stringLiteral, let openingPounds = stringLiteral.openingPounds, let closingPounds = stringLiteral.closingPounds { - s = openingPounds.text + s + closingPounds.text - } - return s - - case .nonCopyableBytes: - return "NonCopyableBytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .nonCopyableInlineBytes: - return "NonCopyableInlineBytes(\(bytesPayload(context: context, responseString: &responseString)))" - case .nonCopyableMacroExpansionWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues("\(responseString)") - postDate.removeLast(8 + String(value.count).count) // "#\r\n\r\n".count - return "NonCopyableMacroExpansionWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\", body: \(value))" - case .nonCopyableStreamWithDateHeader: - var (preDate, postDate) = preDateAndPostDateValues(responseString) - postDate = "\\r\\nTransfer-Encoding: chunked\(postDate)" - return "NonCopyableStreamWithDateHeader(preDateValue: \"\(preDate)\", postDateValue: \"\(postDate)\\r\\n\", body: \(value))" - case .nonCopyableStaticStringWithDateHeader: - let delimiter = valueExpr.stringLiteral?.openingPounds?.text ?? "" - let (preDate, postDate) = preDateAndPostDateValues("\(responseString)\(escapedValue())") - return "NonCopyableStaticStringWithDateHeader(preDateValue: \(delimiter)\"\(preDate)\"\(delimiter), postDateValue: \(delimiter)\"\(postDate)\"\(delimiter))" - } - } - func escapedValue() -> String { - var string = value - guard valueExpr.stringLiteral?.openingPounds == nil else { - // don't escape the string if it uses pound delimiters - return string - } - string.replace("\"", with: "\\\"") - return string - } - - private func bytesPayload( - context: some MacroExpansionContext, - responseString: inout String - ) -> [UInt8] { - var payload = [UInt8]() - payload.reserveCapacity(responseString.count) - responseString.withUTF8 { - payload.append(contentsOf: $0) - } - if let elements = valueExpr.array?.elements { - for element in elements { - if let s = element.expression.integerLiteral?.literal.text, let byte = UInt8(s) { - payload.append(byte) - } else if let s = element.expression.memberAccess?.declName.baseName.text, let byte = UInt8(convenientName: s) { - payload.append(byte) - } else { - context.diagnose(DiagnosticMsg.unhandled(node: element.expression)) - } - } - } else { - context.diagnose(DiagnosticMsg.unhandled(node: valueExpr)) - } - return payload - } -} - -extension IntermediateResponseBody { - #if hasFeature(Embedded) || EMBEDDED - public func responderDebugDescription( - context: some MacroExpansionContext, - isCopyable: Bool, - response: HTTPResponseMessage - ) -> String { - let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) - var responseString = response.intermediateString(escapeLineBreak: escapeLineBreak) - return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - #else - public func responderDebugDescription( - context: some MacroExpansionContext, - isCopyable: Bool, - response: HTTPResponseMessage - ) -> String { - let escapeLineBreak = !(type == .bytes || type == .nonCopyableBytes || type == .inlineBytes || type == .nonCopyableInlineBytes) - var responseString = response.intermediateString(escapeLineBreak: escapeLineBreak) - return responderDebugDescription(context: context, isCopyable: isCopyable, responseString: &responseString) - } - #endif -} - -// MARK: IntermediateResponseBodyType -public enum IntermediateResponseBodyType: String, Sendable { - case bytes - case inlineBytes = "inlinebytes" - case macroExpansion = "macroexpansion" - case macroExpansionWithDateHeader = "macroexpansionwithdateheader" - case streamWithDateHeader = "streamwithdateheader" - case staticString = "staticstring" - case staticStringWithDateHeader = "staticstringwithdateheader" - case stringWithDateHeader = "stringwithdateheader" - - case string - - case nonCopyableBytes = "noncopyablebytes" - case nonCopyableInlineBytes = "noncopyableinlinebytes" - case nonCopyableMacroExpansionWithDateHeader = "noncopyablemacroexpansionwithdateheader" - case nonCopyableStreamWithDateHeader = "noncopyablestreamwithdateheader" - case nonCopyableStaticStringWithDateHeader = "noncopyablestaticstringwithdateheader" -} - -extension IntermediateResponseBodyType { - public var isEnabled: Bool? { - switch self { - case .bytes: - #if CopyableBytes - true - #else - false - #endif - case .inlineBytes: - #if CopyableInlineBytes - true - #else - false - #endif - case .macroExpansion: - #if CopyableMacroExpansion - true - #else - false - #endif - case .macroExpansionWithDateHeader: - #if CopyableMacroExpansionWithDateHeader - true - #else - false - #endif - case .streamWithDateHeader: - #if CopyableStreamWithDateHeader - true - #else - false - #endif - case .staticString: - true - case .staticStringWithDateHeader: - #if CopyableStaticStringWithDateHeader - true - #else - false - #endif - case .stringWithDateHeader: - #if CopyableStringWithDateHeader - true - #else - false - #endif - - case .string: - #if StringRouteResponder - true - #else - false - #endif - - case .nonCopyableBytes: - #if NonCopyableBytes - true - #else - false - #endif - case .nonCopyableInlineBytes: - #if NonCopyableInlineBytes - true - #else - false - #endif - case .nonCopyableMacroExpansionWithDateHeader: - #if NonCopyableMacroExpansionWithDateHeader - true - #else - false - #endif - case .nonCopyableStreamWithDateHeader: - #if NonCopyableStreamWithDateHeader - true - #else - false - #endif - case .nonCopyableStaticStringWithDateHeader: - #if NonCopyableStaticStringWithDateHeader - true - #else - false - #endif - } - } -} - // MARK: UInt8 init extension UInt8 { public init?(convenientName: String) { diff --git a/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift b/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift new file mode 100644 index 00000000..08dae528 --- /dev/null +++ b/Sources/DestinyMacros/util/IntermediateResponseBodyType.swift @@ -0,0 +1,150 @@ + +import SwiftSyntax +import SwiftSyntaxMacros + +public enum IntermediateResponseBodyType: Equatable, Sendable { + case bytes + case inlineBytes + case macroExpansion + case macroExpansionWithDateHeader + case streamWithDateHeader + + case string(isNonCopyable: Bool, isStatic: Bool, withDateHeader: Bool, withCompressedBody: Bool) + + case nonCopyableBytes + case nonCopyableInlineBytes + case nonCopyableMacroExpansionWithDateHeader + case nonCopyableStreamWithDateHeader +} + +// MARK: Is enabled +extension IntermediateResponseBodyType { + public var isEnabled: Bool { + switch self { + case .bytes: + #if CopyableBytes + return true + #else + return false + #endif + case .inlineBytes: + #if CopyableInlineBytes + return true + #else + return false + #endif + case .macroExpansion: + #if CopyableMacroExpansion + return true + #else + return false + #endif + case .macroExpansionWithDateHeader: + #if CopyableMacroExpansionWithDateHeader + return true + #else + return false + #endif + case .streamWithDateHeader: + #if CopyableStreamWithDateHeader + return true + #else + return false + #endif + + case .string(let isNonCopyable, let isStatic, let withDateHeader, let withCompressedBody): + if isNonCopyable { + if isStatic { + if withDateHeader { + if withCompressedBody { + #if NonCopyableStaticStringWithDateHeader + return true + #else + return false + #endif + } + #if NonCopyableStaticStringWithDateHeader + return true + #else + return false + #endif + } + } + } + // copyable + if isStatic { + if withDateHeader { + if withCompressedBody { + #if CopyableStaticStringWithDateHeader + return true + #else + return false + #endif + } + #if CopyableStringWithDateHeader + return true + #else + return false + #endif + } + return true + } + // copyable, not static + if withDateHeader { + #if CopyableStringWithDateHeader + return true + #else + return false + #endif + } + #if StringRouteResponder + return true + #else + return false + #endif + + case .nonCopyableBytes: + #if NonCopyableBytes + return true + #else + return false + #endif + case .nonCopyableInlineBytes: + #if NonCopyableInlineBytes + return true + #else + return false + #endif + case .nonCopyableMacroExpansionWithDateHeader: + #if NonCopyableMacroExpansionWithDateHeader + return true + #else + return false + #endif + case .nonCopyableStreamWithDateHeader: + #if NonCopyableStreamWithDateHeader + return true + #else + return false + #endif + } + } +} + +// MARK: Parse +extension IntermediateResponseBodyType { + public static func parse(key: String, args: LabeledExprListSyntax) -> Self? { + switch key { + case "bytes": .bytes + case "inlinebytes": .inlineBytes + case "macroexpansion": .macroExpansion + case "macroexpansionwithdateheader": .macroExpansionWithDateHeader + case "streamWithDateHeader": .streamWithDateHeader + case "noncopyablebytes": .nonCopyableBytes + case "noncopyableinlinebytes": .nonCopyableInlineBytes + case "noncopyablemacroexpansionwithdateheader": .nonCopyableMacroExpansionWithDateHeader + case "noncopyablestreamwithdateheader": .nonCopyableStreamWithDateHeader + default: nil + } + } +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute+Response.swift b/Sources/DestinyMacros/util/StaticRoute+Response.swift new file mode 100644 index 00000000..f375347d --- /dev/null +++ b/Sources/DestinyMacros/util/StaticRoute+Response.swift @@ -0,0 +1,194 @@ + +import Destiny +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +#if Compression +import SwiftCompression +#endif + +extension StaticRoute { + /// Builds the HTTP Message for this route. + /// + /// - Parameters: + /// - context: Macro expansion context where it was called. + /// - function: `FunctionCallExprSyntax` that represents this route. + /// - middleware: Static middleware this route will handle. + #if StaticMiddleware + public mutating func response( + context: some MacroExpansionContext, + function: FunctionCallExprSyntax, + routerStorage: RouterStorage, + middleware: [StaticMiddleware] + ) -> HTTPResponseMessage { + let result = response(routerStorage: routerStorage, middleware: middleware) + if result.statusCode() == 501 { // not implemented + Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) + } + return result + } + #else + public mutating func response( + context: some MacroExpansionContext, + function: FunctionCallExprSyntax + ) -> HTTPResponseMessage { + let result = response() + if result.statusCode() == 501 { // not implemented + Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) + } + return result + } + #endif +} + +extension StaticRoute { + #if StaticMiddleware + public mutating func response( + routerStorage: RouterStorage, + middleware: [StaticMiddleware] + ) -> HTTPResponseMessage { + var version = version + let path = path.joined(separator: "/") + var status = status + var contentType = contentType + var headers = HTTPHeaders() + if body?.hasDateHeader ?? false { + headers["date"] = HTTPDateFormat.placeholder + } + + #if HTTPCookie + var cookies = [HTTPCookie]() + #endif + + middleware.forEach { middleware in + if middleware.handles(version: version, path: path, method: method, contentType: contentType, status: status) { + #if HTTPCookie + middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers, cookies: &cookies) + #else + middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers) + #endif + } + } + headers["content-type"] = nil + headers["content-length"] = nil + + #if HTTPCookie + return Self.response( + routerStorage: routerStorage, + version: version, + status: status, + headers: &headers, + cookies: cookies, + body: &body, + contentType: contentType, + charset: charset + ) + #else + return Self.response( + version: version, + status: status, + headers: &headers, + body: body, + contentType: contentType, + charset: charset + ) + #endif + } + #else + public mutating func response() -> HTTPResponseMessage { + var headers = HTTPHeaders() + if body?.hasDateHeader ?? false { + headers["date"] = HTTPDateFormat.placeholder + } + headers["content-type"] = nil + headers["content-length"] = nil + #if HTTPCooke + return Self.response(version: version, status: status, headers: &headers, cookies: [], body: body, contentType: contentType, charset: charset) + #else + return Self.response(version: version, status: status, headers: &headers, body: body, contentType: contentType, charset: charset) + #endif + } + #endif +} + +// MARK: Static +extension StaticRoute { + #if HTTPCookie + @inline(__always) + package static func response( + routerStorage: RouterStorage, + + version: HTTPVersion, + status: HTTPResponseStatus.Code, + headers: inout HTTPHeaders, + cookies: [HTTPCookie], + body: inout IntermediateResponseBody?, + contentType: String?, + charset: Charset? + ) -> HTTPResponseMessage { + headers["content-type"] = nil + headers["content-length"] = nil + + #if RouterSettings && Compression + if body != nil, let contentType, routerStorage.settings.compression.isEnabled { + for (algorithm, algorithmSettings) in routerStorage.settings.compression.supportedCompressionAlgorithms { + if let prefixBlacklist = algorithmSettings.contentTypePrefixBlacklist, contentType.hasPrefix(prefixBlacklist) { + continue + } + if algorithmSettings.contentTypeBlacklist.contains(contentType) { + continue + } + if let prefixWhitelist = algorithmSettings.contentTypePrefixWhitelist, !contentType.hasPrefix(prefixWhitelist) { + continue + } + guard algorithmSettings.contentTypeWhitelist.isEmpty || algorithmSettings.contentTypeWhitelist.contains(contentType) else { + continue + } + if let contentLengthThreshold = algorithmSettings.contentLengthThreshold, body!.count < contentLengthThreshold { + continue + } + if let compressed = algorithm.compress(span: body!.value.utf8Span.span) { + if routerStorage.settings.compression.compressOnlyIfResultIsSmaller, compressed.count >= body!.count { + continue + } + headers["content-encoding"] = algorithm.acceptEncodingName + headers["vary"] = "Accept-Encoding" + body!.rawValue = compressed + if case let .string(isNonCopyable, isStatic, withDateHeader, _) = body!.type { + body!.type = .string(isNonCopyable: isNonCopyable, isStatic: isStatic, withDateHeader: withDateHeader, withCompressedBody: true) + } + break + } + } + } + #endif + + return HTTPResponseMessage( + head: .init(headers: headers, cookies: cookies, status: status, version: version), + body: body, + contentType: contentType, + charset: charset + ) + } + #else + @inline(__always) + package static func response( + version: HTTPVersion, + status: HTTPResponseStatus.Code, + headers: inout HTTPHeaders, + body: IntermediateResponseBody?, + contentType: String?, + charset: Charset? + ) -> HTTPResponseMessage { + headers["content-type"] = nil + headers["content-length"] = nil + return HTTPResponseMessage( + head: .init(headers: headers, status: status, version: version), + body: body, + contentType: contentType, + charset: charset + ) + } + #endif +} \ No newline at end of file diff --git a/Sources/DestinyMacros/util/StaticRoute.swift b/Sources/DestinyMacros/util/StaticRoute.swift index e5c2563a..59819bb7 100644 --- a/Sources/DestinyMacros/util/StaticRoute.swift +++ b/Sources/DestinyMacros/util/StaticRoute.swift @@ -9,7 +9,7 @@ import SwiftSyntaxMacros public struct StaticRoute: Sendable { public var path:[String] public let contentType:String? - public let body:IntermediateResponseBody? + public internal(set) var body:IntermediateResponseBody? public var method:HTTPRequestMethod public let status:HTTPResponseStatus.Code @@ -69,189 +69,4 @@ extension StaticRoute { public mutating func insertPath(contentsOf newElements: some Collection, at i: Int) { path.insert(contentsOf: newElements, at: i) } -} - -// MARK: Response -extension StaticRoute { - #if StaticMiddleware - public func response( - middleware: [StaticMiddleware] - ) -> HTTPResponseMessage { - var version = version - let path = path.joined(separator: "/") - var status = status - var contentType = contentType - var headers = HTTPHeaders() - if body?.hasDateHeader ?? false { - headers["date"] = HTTPDateFormat.placeholder - } - - #if HTTPCookie - var cookies = [HTTPCookie]() - #endif - - middleware.forEach { middleware in - if middleware.handles(version: version, path: path, method: method, contentType: contentType, status: status) { - #if HTTPCookie - middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers, cookies: &cookies) - #else - middleware.apply(version: &version, contentType: &contentType, status: &status, headers: &headers) - #endif - } - } - headers["content-type"] = nil - headers["content-length"] = nil - - #if HTTPCookie - return Self.response( - version: version, - status: status, - headers: &headers, - cookies: cookies, - body: body, - contentType: contentType, - charset: charset - ) - #else - return Self.response( - version: version, - status: status, - headers: &headers, - body: body, - contentType: contentType, - charset: charset - ) - #endif - } - #else - public func response() -> HTTPResponseMessage { - var headers = HTTPHeaders() - if body?.hasDateHeader ?? false { - headers["date"] = HTTPDateFormat.placeholder - } - headers["content-type"] = nil - headers["content-length"] = nil - #if HTTPCooke - return Self.response(version: version, status: status, headers: &headers, cookies: [], body: body, contentType: contentType, charset: charset) - #else - return Self.response(version: version, status: status, headers: &headers, body: body, contentType: contentType, charset: charset) - #endif - } - #endif - - #if HTTPCookie - @inline(__always) - package static func response( - version: HTTPVersion, - status: HTTPResponseStatus.Code, - headers: inout HTTPHeaders, - cookies: [HTTPCookie], - body: IntermediateResponseBody?, - contentType: String?, - charset: Charset? - ) -> HTTPResponseMessage { - headers["content-type"] = nil - headers["content-length"] = nil - return HTTPResponseMessage( - head: .init(headers: headers, cookies: cookies, status: status, version: version), - body: body, - contentType: contentType, - charset: charset - ) - } - #else - @inline(__always) - package static func response( - version: HTTPVersion, - status: HTTPResponseStatus.Code, - headers: inout HTTPHeaders, - body: IntermediateResponseBody?, - contentType: String?, - charset: Charset? - ) -> HTTPResponseMessage { - headers["content-type"] = nil - headers["content-length"] = nil - return HTTPResponseMessage( - head: .init(headers: headers, status: status, version: version), - body: body, - contentType: contentType, - charset: charset - ) - } - #endif -} - -// MARK: Responder -extension StaticRoute { - #if StaticMiddleware - public func responder( - middleware: [StaticMiddleware] - ) -> String? { - return response(middleware: middleware).string(escapeLineBreak: true) - } - #else - public func responder() -> String? { - return response().string(escapeLineBreak: true) - } - #endif -} - -// MARK: Response -extension StaticRoute { - /// Builds the HTTP Message for this route. - /// - /// - Parameters: - /// - context: Macro expansion context where it was called. - /// - function: `FunctionCallExprSyntax` that represents this route. - /// - middleware: Static middleware this route will handle. - #if StaticMiddleware - public func response( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax, - middleware: [StaticMiddleware] - ) -> HTTPResponseMessage { - let result = response(middleware: middleware) - if result.statusCode() == 501 { // not implemented - Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) - } - return result - } - #else - public func response( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax - ) -> HTTPResponseMessage { - let result = response() - if result.statusCode() == 501 { // not implemented - Diagnostic.routeResponseStatusNotImplemented(context: context, node: function.calledExpression) - } - return result - } - #endif -} - -// MARK: Responder -extension StaticRoute { - /// The `RouteResponderProtocol` responder for this route. - /// - /// - Parameters: - /// - context: Macro expansion context where it was called. - /// - function: `FunctionCallExprSyntax` that represents this route. - /// - middleware: Static middleware that this route will handle. - #if StaticMiddleware - public func responder( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax, - middleware: [StaticMiddleware] - ) throws(DestinyError) -> String? { - return response(context: context, function: function, middleware: middleware).string(escapeLineBreak: true) - } - #else - public func responder( - context: some MacroExpansionContext, - function: FunctionCallExprSyntax - ) throws(DestinyError) -> String? { - return response(context: context, function: function).string(escapeLineBreak: true) - } - #endif } \ No newline at end of file diff --git a/Sources/TestRouter/TestRouter.swift b/Sources/TestRouter/TestRouter.swift index bbd36069..50efcd14 100644 --- a/Sources/TestRouter/TestRouter.swift +++ b/Sources/TestRouter/TestRouter.swift @@ -35,6 +35,7 @@ package final class TestRouter { routerSettings: .init( //dynamicResponsesAreGeneric: false, //protocolConformances: false, + logging: true, visibility: .package ),