diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..a985967 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,25 @@ +name: Release Please + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..2537c1f --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.4.0" +} diff --git a/Sources/HTTPClient/Interface/Client.swift b/Sources/HTTPClient/Interface/Client.swift index 841d3c7..f022656 100644 --- a/Sources/HTTPClient/Interface/Client.swift +++ b/Sources/HTTPClient/Interface/Client.swift @@ -33,14 +33,18 @@ public struct Client: Sendable { public var middlewares: [any ClientMiddleware] /// Creates a new client. + // / - Parameters: + /// - serverURL: The URL of the server, used as the base URL for requests made by the client. + /// - transport: A type capable of sending HTTP requests and receiving HTTP responses. + /// - middlewares: The middlewares to be invoked before the transport. If not provided, a default middleware that adds default headers to requests will be used. public init( serverURL: URL, transport: any ClientTransport, - middlewares: [any ClientMiddleware] = [] + middlewares: [any ClientMiddleware]? = nil ) { self.serverURL = serverURL self.transport = transport - self.middlewares = middlewares + self.middlewares = middlewares ?? [DefaultHeadersMiddleware()] } /// Sends the HTTP request and returns the HTTP response. diff --git a/Sources/HTTPClient/Middlewares/DefaultHeadersMiddleware.swift b/Sources/HTTPClient/Middlewares/DefaultHeadersMiddleware.swift new file mode 100644 index 0000000..b60ee39 --- /dev/null +++ b/Sources/HTTPClient/Middlewares/DefaultHeadersMiddleware.swift @@ -0,0 +1,110 @@ +import Foundation +import HTTPTypes + +public struct DefaultHeadersMiddleware: ClientMiddleware { + + let headers = HTTPFields([ + HTTPField.defaultUserAgent, + HTTPField.defaultAcceptEncoding, + HTTPField.defaultAcceptLanguage, + ]) + + public func intercept( + _ request: HTTPRequest, + body: sending HTTPBody?, + baseURL: URL, + next: (HTTPRequest, sending HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) { + var request = request + for field in headers { + if !request.headerFields.contains(field.name) { + request.headerFields[field.name] = field.value + } + } + return try await next(request, body, baseURL) + } +} + +extension HTTPField { + /// Returns default `Accept-Encoding` header, appropriate for the encodings supported by particular OS versions. + /// + /// See the [Accept-Encoding HTTP header documentation](https://tools.ietf.org/html/rfc7230#section-4.2.3) . + static let defaultAcceptEncoding: HTTPField = { + let encodings = ["gzip", "deflate"] + return HTTPField(name: .acceptEncoding, value: encodings.qualityEncoded()) + }() + + /// Returns default `Accept-Language` header, generated by querying `Locale` for the user's `preferredLanguages`. + /// + /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). + static let defaultAcceptLanguage: HTTPField = HTTPField( + name: .acceptLanguage, value: Locale.preferredLanguages.prefix(6).qualityEncoded()) + + /// Returns default `User-Agent` header. + /// + /// See the [User-Agent header documentation](https://tools.ietf.org/html/rfc7231#section-5.5.3). + /// + /// Example: `iOS Example/1.0 (dev.grds.swift-http-client.iOS-Example; build:1; iOS 16.0.0) swift-http-client/1.0.0` + static let defaultUserAgent: HTTPField = { + let info = Bundle.main.infoDictionary + let executable = + (info?["CFBundleExecutable"] as? String) + ?? (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) + ?? "Unknown" + let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown" + let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown" + let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown" + + let osNameVersion: String = { + let version = ProcessInfo.processInfo.operatingSystemVersion + let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)" + let osName: String = { + #if os(iOS) + #if targetEnvironment(macCatalyst) + return "macOS(Catalyst)" + #else + return "iOS" + #endif + #elseif os(watchOS) + return "watchOS" + #elseif os(tvOS) + return "tvOS" + #elseif os(macOS) + #if targetEnvironment(macCatalyst) + return "macOS(Catalyst)" + #else + return "macOS" + #endif + #elseif os(visionOS) + return "visionOS" + #elseif os(Linux) + return "Linux" + #elseif os(Windows) + return "Windows" + #elseif os(Android) + return "Android" + #elseif os(WASI) + return "WASI" + #else + return "Unknown" + #endif + }() + + return "\(osName) \(versionString)" + }() + + let userAgent = + "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) swift-http-client/\(version)" + + return HTTPField(name: .userAgent, value: userAgent) + }() +} + +extension Collection { + func qualityEncoded() -> String { + enumerated().map { index, encoding in + let quality = 1.0 - (Double(index) * 0.1) + return "\(encoding);q=\(quality)" + }.joined(separator: ", ") + } +} diff --git a/Sources/HTTPClient/Version.swift b/Sources/HTTPClient/Version.swift new file mode 100644 index 0000000..d76fc65 --- /dev/null +++ b/Sources/HTTPClient/Version.swift @@ -0,0 +1,3 @@ +// x-release-please-start-version +let version = "0.4.0" +// x-release-please-end diff --git a/Tests/HTTPClientTests/ClientTests.swift b/Tests/HTTPClientTests/ClientTests.swift index d9a6167..746b3df 100644 --- a/Tests/HTTPClientTests/ClientTests.swift +++ b/Tests/HTTPClientTests/ClientTests.swift @@ -89,7 +89,9 @@ struct ClientTests { let client = Client(serverURL: serverURL, transport: transport) #expect(client.serverURL == serverURL) - #expect(client.middlewares.isEmpty) + #expect( + client.middlewares.isEmpty == false, + "Default middleware should be added when no middlewares are provided") } @Test func clientInitializationWithMiddlewares() { diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..b20289d --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "simple", + "packages": { + ".": { + "release-type": "simple", + "package-name": "swift-http-client", + "changelog-path": "CHANGELOG.md", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "extra-files": [ + { + "type": "generic", + "path": "Sources/HTTPClient/Version.swift" + } + ] + } + } +}