Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

All notable changes to NetworkingKit are documented in this file.

## 2.3.2 - Unreleased
## 2.3.3 - 2026-07-19

### Changed

- Keep app-level request base classes model-agnostic: concrete REST and GraphQL requests now declare their own `Response` type in the README, Demo, and app-layer example tests.

## 2.3.2 - 2026-07-19

### Added

Expand Down
11 changes: 6 additions & 5 deletions Examples/NetworkingKitDemo/DemoViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,8 @@ struct AppNetworkErrorLocalizer: NetworkErrorLocalizing {
}
}

/// An app-specific base type that injects `AppNetworkClient` without selecting a request protocol.
class AppRequest<T: Decodable & Sendable>: @unchecked Sendable {
typealias Response = T
/// An app-specific base type that injects `AppNetworkClient` without selecting a response model or request protocol.
class AppRequest: @unchecked Sendable {
let client: any NetworkClient = AppNetworkClient.shared
}

Expand All @@ -202,7 +201,8 @@ struct RESTCharacter: Codable, Sendable {
let status: String
}

final class GetCharacterRequest: AppRequest<RESTCharacter>, RestfulRequest, @unchecked Sendable {
final class GetCharacterRequest: AppRequest, RestfulRequest, @unchecked Sendable {
typealias Response = RESTCharacter
private let id: String

init(id: String) { self.id = id }
Expand All @@ -220,7 +220,8 @@ struct GraphQLCharacterPayload: Codable, Sendable {
let character: Character?
}

final class FetchCharacterProfileRequest: AppRequest<GraphQLResponse<GraphQLCharacterPayload>>, GraphQLRequest, @unchecked Sendable {
final class FetchCharacterProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<GraphQLCharacterPayload>
private let id: String

init(id: String) { self.id = id }
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Add the package in Xcode through **File > Add Package Dependencies**, or declare

```swift
dependencies: [
.package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.0.0")
.package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.3.3")
]
```

Expand Down Expand Up @@ -115,11 +115,10 @@ final class AppNetworkClient: NetworkClient, @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.

`AppRequest` is intentionally limited to client and response-type injection. It should not own common headers, authentication, or logging because those responsibilities apply to every request and belong to `NetworkInterceptor`.
`AppRequest` is intentionally limited to client injection. The concrete REST or GraphQL request declares its own `Response` type, because the response model belongs to that business endpoint. The base class should not own common headers, authentication, or logging because those responsibilities apply to every request and belong to `NetworkInterceptor`.

```swift
class AppRequest<T: Decodable & Sendable>: @unchecked Sendable {
typealias Response = T
class AppRequest: @unchecked Sendable {
let client: any NetworkClient = AppNetworkClient.shared
}
```
Expand All @@ -132,7 +131,8 @@ struct User: Decodable, Sendable {
let name: String
}

final class GetUserRequest: AppRequest<User>, RestfulRequest, @unchecked Sendable {
final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
typealias Response = User
var path: String { "/users/123" }
var method: HTTPMethod { .get }

Expand All @@ -147,7 +147,8 @@ 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<EmptyResponse>, RestfulRequest, @unchecked Sendable {
final class DeleteUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
typealias Response = EmptyResponse
var path: String { "/users/123" }
var method: HTTPMethod { .delete }
var queryItems: [URLQueryItem]? { nil }
Expand All @@ -167,7 +168,8 @@ struct UserProfile: Decodable, Sendable {
let email: String
}

final class FetchUserProfileRequest: AppRequest<GraphQLResponse<UserProfile>>, GraphQLRequest, @unchecked Sendable {
final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<UserProfile>
var query: String {
"""
query {
Expand Down
16 changes: 9 additions & 7 deletions README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ NetworkingKit 是一个面向 iOS 与 macOS App 的轻量级原生 Swift 网络

```swift
dependencies: [
.package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.0.0")
.package(url: "https://github.com/relaxfinger/NetworkingKit.git", from: "2.3.3")
]
```

Expand Down Expand Up @@ -110,13 +110,12 @@ final class AppNetworkClient: NetworkClient, @unchecked Sendable {
### 2. 创建 App Request 基类

```swift
class AppRequest<T: Decodable & Sendable>: @unchecked Sendable {
typealias Response = T
class AppRequest: @unchecked Sendable {
let client: any NetworkClient = AppNetworkClient.shared
}
```

采用基类时,业务 Request 也必须是 class,因为 Swift 的 `struct` 不能继承 class。基类不应直接遵循 `NetworkRequest`,以便 REST 或 GraphQL 子类获得其各自协议提供的默认值。`AppRequest` 只负责注入 Client 和 Response 类型,不应承载通用 Header、认证或日志;这些跨请求职责属于 `NetworkInterceptor`。
采用基类时,业务 Request 也必须是 class,因为 Swift 的 `struct` 不能继承 class。基类不应直接遵循 `NetworkRequest`,以便 REST 或 GraphQL 子类获得其各自协议提供的默认值。`AppRequest` 只负责注入 Client;具体 REST 或 GraphQL 请求自行声明 `Response`,因为响应模型属于业务接口本身。`AppRequest` 不应承载通用 Header、认证或日志;这些跨请求职责属于 `NetworkInterceptor`。

### 3. REST 请求

Expand All @@ -126,7 +125,8 @@ struct User: Decodable, Sendable {
let name: String
}

final class GetUserRequest: AppRequest<User>, RestfulRequest, @unchecked Sendable {
final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
typealias Response = User
var path: String { "/users/123" }
var method: HTTPMethod { .get }
var queryItems: [URLQueryItem]? { nil }
Expand All @@ -140,7 +140,8 @@ final class GetUserRequest: AppRequest<User>, RestfulRequest, @unchecked Sendabl
对于 `204 No Content` 等成功但无 body 的接口,请使用 `EmptyResponse` 作为响应类型:

```swift
final class DeleteUserRequest: AppRequest<EmptyResponse>, RestfulRequest, @unchecked Sendable {
final class DeleteUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
typealias Response = EmptyResponse
var path: String { "/users/123" }
var method: HTTPMethod { .delete }
var queryItems: [URLQueryItem]? { nil }
Expand All @@ -157,7 +158,8 @@ struct UserProfile: Decodable, Sendable {
let name: String
}

final class FetchUserProfileRequest: AppRequest<GraphQLResponse<UserProfile>>, GraphQLRequest, @unchecked Sendable {
final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<UserProfile>
var query: String { "query { user { id name } }" }
}
```
Expand Down
9 changes: 5 additions & 4 deletions Tests/NetworkingKitTests/AppLayerExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,23 @@ private final class AppNetworkClient: NetworkClient, @unchecked Sendable {
private init() {}
}

private class AppRequest<T: Decodable & Sendable>: @unchecked Sendable {
typealias Response = T
private class AppRequest: @unchecked Sendable {
let client: any NetworkClient = AppNetworkClient.shared
}

private struct User: Codable, Sendable { let id: String }
private struct UserProfile: Codable, Sendable { let id: String }

private final class GetUserRequest: AppRequest<User>, RestfulRequest, @unchecked Sendable {
private final class GetUserRequest: AppRequest, RestfulRequest, @unchecked Sendable {
typealias Response = User
var path: String { "/users/123" }
var method: HTTPMethod { .get }
var queryItems: [URLQueryItem]? { nil }
var body: (any Encodable & Sendable)? { nil }
var contentType: String? { nil }
}

private final class FetchUserProfileRequest: AppRequest<GraphQLResponse<UserProfile>>, GraphQLRequest, @unchecked Sendable {
private final class FetchUserProfileRequest: AppRequest, GraphQLRequest, @unchecked Sendable {
typealias Response = GraphQLResponse<UserProfile>
var query: String { "query { user { id } }" }
}
Loading