diff --git a/CHANGELOG.md b/CHANGELOG.md index 662f3a3..2c3d31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to NetworkingKit are documented in this file. +## 2.3.5 - 2026-07-19 + +### Changed + +- Make every App-layer request-base example bind its own concrete client directly, rather than exposing a generic `SharedNetworkClient` constraint. + ## 2.3.4 - 2026-07-19 ### Added diff --git a/Examples/NetworkingKitDemo/ContentView.swift b/Examples/NetworkingKitDemo/ContentView.swift index e2860e9..cf6d687 100644 --- a/Examples/NetworkingKitDemo/ContentView.swift +++ b/Examples/NetworkingKitDemo/ContentView.swift @@ -56,7 +56,7 @@ struct ContentView: View { } .multilineTextAlignment(.center) - Text("NetworkingKit on \(platformName)\nAppNetworkClient · AppRequest · REST · GraphQL · Metrics") + Text("NetworkingKit on \(platformName)\nAppNetworkClient · AppNetworkRequest · REST · GraphQL · Metrics") .font(.footnote) .foregroundStyle(.secondary) .multilineTextAlignment(.center) diff --git a/Examples/NetworkingKitDemo/DemoViewModel.swift b/Examples/NetworkingKitDemo/DemoViewModel.swift index add2602..29c3afb 100644 --- a/Examples/NetworkingKitDemo/DemoViewModel.swift +++ b/Examples/NetworkingKitDemo/DemoViewModel.swift @@ -187,14 +187,11 @@ struct AppNetworkErrorLocalizer: NetworkErrorLocalizing { } } -/// An app-specific generic base type that binds a request family to one shared client. -class AppNetworkRequest: @unchecked Sendable { - typealias Client = ClientType - var client: ClientType { .shared } -} - /// The request base for endpoints served by `AppNetworkClient`. -class AppRequest: AppNetworkRequest, @unchecked Sendable {} +class AppNetworkRequest: @unchecked Sendable { + typealias Client = AppNetworkClient + var client: AppNetworkClient { .shared } +} // MARK: - REST @@ -205,7 +202,7 @@ struct RESTCharacter: Codable, Sendable { let status: String } -final class GetCharacterRequest: AppRequest, RestfulRequest, @unchecked Sendable { +final class GetCharacterRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable { typealias Response = RESTCharacter private let id: String @@ -224,7 +221,7 @@ struct GraphQLCharacterPayload: Codable, Sendable { let character: Character? } -final class FetchCharacterProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable { +final class FetchCharacterProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable { typealias Response = GraphQLResponse private let id: String diff --git a/README.md b/README.md index 7f15b83..af08d8b 100644 --- a/README.md +++ b/README.md @@ -115,18 +115,16 @@ final class AppNetworkClient: SharedNetworkClient, @unchecked Sendable { Use a base class to avoid repeating the client in every request. Keep this base class free of `NetworkRequest` conformance so a REST or GraphQL subclass can receive the defaults from its own request protocol. Requests inheriting from a class must also be classes; Swift structures cannot inherit from classes. -`NetworkRequest` binds both a concrete `Client` type and a `Response` type. `AppNetworkRequest` fixes the client family without erasing it to `any NetworkClient`; the concrete REST or GraphQL request declares only its `Response`. This makes it impossible to accidentally use a request from one backend family with another backend's client. The base class should not own common headers, authentication, or logging because those responsibilities apply to every request and belong to `NetworkInterceptor`. +`NetworkRequest` binds both a concrete `Client` type and a `Response` type. `AppNetworkRequest` directly fixes `AppNetworkClient` without erasing it to `any NetworkClient`; the concrete REST or GraphQL request declares only its `Response`. This makes it impossible to accidentally use a request from one backend family with another backend's client. When an app has multiple backend clients, define one equivalent request base class per client. The base class should not own common headers, authentication, or logging because those responsibilities apply to every request and belong to `NetworkInterceptor`. ```swift -class AppNetworkRequest: @unchecked Sendable { - typealias Client = ClientType +class AppNetworkRequest: @unchecked Sendable { + typealias Client = AppNetworkClient - var client: ClientType { + var client: AppNetworkClient { .shared } } - -class AppRequest: AppNetworkRequest, @unchecked Sendable {} ``` ### 3. Define a REST request @@ -137,7 +135,7 @@ struct User: Decodable, Sendable { let name: String } -final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable { +final class GetUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable { typealias Response = User var path: String { "/users/123" } var method: HTTPMethod { .get } @@ -153,7 +151,7 @@ For a JSON request body, return any `Encodable & Sendable` value from `body`. Th For successful endpoints with no response body, such as `204 No Content`, use `EmptyResponse` as the response type. ```swift -final class DeleteUserRequest: AppRequest, RestfulRequest, @unchecked Sendable { +final class DeleteUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable { typealias Response = EmptyResponse var path: String { "/users/123" } var method: HTTPMethod { .delete } @@ -174,7 +172,7 @@ struct UserProfile: Decodable, Sendable { let email: String } -final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable { +final class FetchUserProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable { typealias Response = GraphQLResponse var query: String { """ diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 2686000..4997106 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -110,18 +110,16 @@ final class AppNetworkClient: SharedNetworkClient, @unchecked Sendable { ### 2. 创建 App Request 基类 ```swift -class AppNetworkRequest: @unchecked Sendable { - typealias Client = ClientType +class AppNetworkRequest: @unchecked Sendable { + typealias Client = AppNetworkClient - var client: ClientType { + var client: AppNetworkClient { .shared } } - -class AppRequest: AppNetworkRequest, @unchecked Sendable {} ``` -采用基类时,业务 Request 也必须是 class,因为 Swift 的 `struct` 不能继承 class。基类不应直接遵循 `NetworkRequest`,以便 REST 或 GraphQL 子类获得其各自协议提供的默认值。`NetworkRequest` 同时绑定具体的 `Client` 类型与 `Response` 类型;`AppNetworkRequest` 固定 Client 类型但不使用 `any NetworkClient` 抹除它,具体 REST 或 GraphQL 请求只声明自身的 `Response`。这样能在编译期避免某个后端的 Request 被错误地绑定到另一个后端的 Client。`AppRequest` 不应承载通用 Header、认证或日志;这些跨请求职责属于 `NetworkInterceptor`。 +采用基类时,业务 Request 也必须是 class,因为 Swift 的 `struct` 不能继承 class。基类不应直接遵循 `NetworkRequest`,以便 REST 或 GraphQL 子类获得其各自协议提供的默认值。`NetworkRequest` 同时绑定具体的 `Client` 类型与 `Response` 类型;`AppNetworkRequest` 直接固定为 `AppNetworkClient`,但不使用 `any NetworkClient` 抹除它,具体 REST 或 GraphQL 请求只声明自身的 `Response`。这样能在编译期避免某个后端的 Request 被错误地绑定到另一个后端的 Client。有多个后端 Client 时,为每个 Client 定义一个等价的请求基类。`AppNetworkRequest` 不应承载通用 Header、认证或日志;这些跨请求职责属于 `NetworkInterceptor`。 ### 3. REST 请求 @@ -131,7 +129,7 @@ struct User: Decodable, Sendable { let name: String } -final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable { +final class GetUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable { typealias Response = User var path: String { "/users/123" } var method: HTTPMethod { .get } @@ -146,7 +144,7 @@ final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable { 对于 `204 No Content` 等成功但无 body 的接口,请使用 `EmptyResponse` 作为响应类型: ```swift -final class DeleteUserRequest: AppRequest, RestfulRequest, @unchecked Sendable { +final class DeleteUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable { typealias Response = EmptyResponse var path: String { "/users/123" } var method: HTTPMethod { .delete } @@ -164,7 +162,7 @@ struct UserProfile: Decodable, Sendable { let name: String } -final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable { +final class FetchUserProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable { typealias Response = GraphQLResponse var query: String { "query { user { id name } }" } } diff --git a/Tests/NetworkingKitTests/AppLayerExampleTests.swift b/Tests/NetworkingKitTests/AppLayerExampleTests.swift index 07bdb1b..8db9775 100644 --- a/Tests/NetworkingKitTests/AppLayerExampleTests.swift +++ b/Tests/NetworkingKitTests/AppLayerExampleTests.swift @@ -36,17 +36,15 @@ private final class AppNetworkClient: SharedNetworkClient, @unchecked Sendable { private init() {} } -private class AppNetworkRequest: @unchecked Sendable { - typealias Client = ClientType - var client: ClientType { .shared } +private class AppNetworkRequest: @unchecked Sendable { + typealias Client = AppNetworkClient + var client: AppNetworkClient { .shared } } -private class AppRequest: AppNetworkRequest, @unchecked Sendable {} - private struct User: Codable, Sendable { let id: String } private struct UserProfile: Codable, Sendable { let id: String } -private final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable { +private final class GetUserRequest: AppNetworkRequest, RestfulRequest, @unchecked Sendable { typealias Response = User var path: String { "/users/123" } var method: HTTPMethod { .get } @@ -55,7 +53,7 @@ private final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Senda var contentType: String? { nil } } -private final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable { +private final class FetchUserProfileRequest: AppNetworkRequest, GraphQLRequest, @unchecked Sendable { typealias Response = GraphQLResponse var query: String { "query { user { id } }" } }