{"id":22913994,"url":"https://github.com/bpisano/network-kit","last_synced_at":"2025-05-12T13:40:33.094Z","repository":{"id":189807780,"uuid":"678042105","full_name":"bpisano/network-kit","owner":"bpisano","description":"Easy and structured HTTP networking in Swift.","archived":false,"fork":false,"pushed_at":"2024-12-05T18:14:56.000Z","size":392,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-31T22:34:56.596Z","etag":null,"topics":["access-token","http","networking","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bpisano.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-13T13:50:32.000Z","updated_at":"2024-12-09T17:13:14.000Z","dependencies_parsed_at":"2024-12-05T18:39:55.740Z","dependency_job_id":null,"html_url":"https://github.com/bpisano/network-kit","commit_stats":null,"previous_names":["bpisano/network-kit"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpisano%2Fnetwork-kit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpisano%2Fnetwork-kit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpisano%2Fnetwork-kit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bpisano%2Fnetwork-kit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bpisano","download_url":"https://codeload.github.com/bpisano/network-kit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253749368,"owners_count":21958110,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-token","http","networking","swift"],"created_at":"2024-12-14T05:12:47.862Z","updated_at":"2025-05-12T13:40:33.034Z","avatar_url":"https://github.com/bpisano.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NetworkKit\n\nA versatile Swift package that simplifies HTTP requests, enabling efficient communication with APIs and servers in your apps.\n\n### Key Features\n\n1. **Separation of Client and Request**: NetworkKit distinguishes between client configuration and request creation allowing each request to reside in its own file. This modularity is beneficial for managing various client environments, including development, preproduction, and production.\n2. **Modern Request Body Construction**: The `HttpRequest` protocol simplifies the process of defining HTTP methods, headers, query parameters, and body content.\n3. **Automated Refresh Token Management**: NetworkKit simplifies access token handling through the `AccessTokenProvider` protocol. Efficiently manage token refreshes, ensuring consistent and secure communication with APIs.\n4. **Per-Request Error Handling**: Define custom error behaviors and contextual descriptions for specific status codes.\n\n# Installation\n\nAdd the following dependency to your `Package.swift` file:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/bpisano/network-kit\", .upToNextMajor(from: \"0.1.0\"))\n]\n```\n\n# Quick start\n\n1. **Create a Client Configuration**: Define a client configuration using a `struct` that conforms to the `Client` protocol. You can create your own client structure based on your client's URL and configuration.\n\n```swift\nstruct MyClient: Client {\n    let host: String = \"api.example.com\"\n}\n```\n\n2. **Define a Request**: Create a request structure that conforms to the `HttpRequest` protocol. For this example, let's assume you have a request to retrieve a list of articles:\n\n```swift\nstruct GetArticlesRequest: HttpRequest {\n    let path: String = \"/articles\"\n    let method: HttpMethod = .get // optional, defaults to .get\n}\n```\n\n3. **Perform the Request**: Use the client configuration to perform the request.\n\n```swift\nlet client = MyClient()\nlet articles: [Article] = try await client.perform(GetArticlesRequest())\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nGET https://api.example.com/articles\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch1\u003eHttpRequest\u003c/h1\u003e\u003c/summary\u003e\n\nTo define a custom HTTP request, you need to create a structure that conforms to the `HttpRequest` protocol. This protocol defines the properties and methods required to construct a complete HTTP request. Here's a breakdown of the key components you can customize:\n\n| Parameter         | Description                                                                       |\n|-------------------|-----------------------------------------------------------------------------------|\n| `path`            | URL path of the request (excluding base client URL)                              |\n| `method`          | HTTP method to be used for the request (e.g., GET, POST, PUT, DELETE)            |\n| `headers`         | Additional headers required for the request                                      |\n| `queryParameters` | Query parameters to include in the URL                                           |\n| `body`            | Body of the request (can be customized based on data format)                     |\n| `jsonEncoder`     | JSON encoder to use for encoding the request's body data                         |\n| `successStatusCodes` | Array of status codes interpreted as successful responses                      |\n| `timeout`         | Maximum time interval for waiting for a response                                 |\n| `cachePolicy`     | Caching behavior for the request                                                |\n\n## Headers\n\nTo include custom headers in your request, use the `headers` property within a structure that conforms to the `HttpRequest` protocol. This property enables you to specify one or more headers, enriching the context and behavior of your request.\n\nConsider the following example of a request to retrieve user data while including custom headers:\n\n```swift\nstruct GetUserRequest: HttpRequest {\n    let path: String = \"/user\"\n\n    var headers: HttpHeaders? {\n        HttpHeader(\"Language\", value: \"fr-FR\")\n        HttpHeader(\"Client-Version\", value: \"2.0\")\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nGET https://api.example.com/user\nHeaders:\n    Language: fr-FR\n    Client-Version: 2.0\n```\n\n\u003c/details\u003e\n\nThe `@HttpHeadersBuilder` result builder streamlines the process of combining multiple headers within the headers property.\n\n## Query parameters\n\nTo include query parameters in your request, use the `queryParameters` property within a structure that conforms to the `HttpRequest` protocol. This property allows you to specify one or more query parameters, enhancing the specificity and context of your request.\n\nFor example, consider the following request to retrieve user data by providing an `id` parameter:\n\n```swift\nstruct GetUserRequest: HttpRequest {\n    let path: String = \"/user\"\n\n    private let id: String\n\n    init(id: String) {\n        self.id = id\n    }\n\n    var queryParameters: HttpQueryParameters? {\n        HttpQueryParameter(\"id\", value: id)\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nGET https://api.example.com/user?id=YOUR_ID\n```\n\n\u003c/details\u003e\n\nYou can also combine multiple query parameters by taking advantage of the `@HttpQueryParametersBuilder`.\n\n```swift\nstruct GetPostsRequest: HttpRequest {\n    let path: String = \"/posts\"\n\n    var queryParameters: HttpQueryParameters? {\n        HttpQueryParameter(\"category\", value: \"technology\")\n        HttpQueryParameter(\"author\", value: \"john_doe\")\n        HttpQueryParameter(\"limit\", value: \"10\")\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nGET https://api.example.com/posts?category=technology\u0026author=john_doe\u0026limit=10\n```\n\n\u003c/details\u003e\n\n## Body\n\n### Sending Data in the Request Body\n\n#### Dictionary\n\nYou can use a dictionary to represent the request body as its conforms to the `HttpBody` protocol.\n\n```swift\nstruct LoginRequest: HttpRequest {\n    let path: String = \"/login\"\n    let method: HttpMethod = .post\n\n    private let login: String\n    private let password: String\n\n    init(\n        login: String,\n        password: String\n    ) {\n        self.login = login\n        self.password = password\n    }\n\n    var body: some HttpBody {\n        [\n            \"login\": login,\n            \"password\": password\n        ]\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nPOST https://api.example.com/login\nHeaders:\n    Content-Type: application/json\n\nBody:\n{\n    \"login\": \"YOUR_LOGIN\",\n    \"password\": \"YOUR_PASSWORD\"\n}\n```\n\n\u003c/details\u003e\n\n#### Using the Encode Struct\n\nFor more complex data structures, you can use the `Encode` struct to encode objects conforming to the `Encodable` protocol into the request body.\n\n```swift\nstruct CreateUserRequest: HttpRequest {\n    let path: String = \"/user\"\n    let method: HttpMethod = .post\n\n    private let user: User\n\n    init(user: User) {\n        self.user = user\n    }\n\n    var body: some HttpBody {\n        Encode(user)\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nPOST https://api.example.com/user\nHeaders:\n    Content-Type: application/json\n\nBody:\n{\n    \"id\": \"YOUR_ID\",\n    \"username\": \"YOUR_USERNAME\"\n}\n```\n\n\u003c/details\u003e\n\n#### Using the Raw Struct for Raw Data\n\nTo send raw data, such as binary or custom formats, you can use the `Raw` struct. This allows you to pass raw data directly as the request body.\n\n```swift\nstruct UploadDataRequest: HttpRequest {\n    let path: String = \"/data\"\n    let method: HttpMethod = .post\n\n    private let data: Data\n\n    init(data: Data) {\n        self.data = data\n    }\n\n    var body: some HttpBody {\n        Raw(data)\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nPOST https://api.example.com/data\nHeaders:\n    Content-Type: application/octet-stream\n\nBody:\n[Binary Data]\n```\n\n\u003c/details\u003e\n\n### Uploading Files with Multipart Form\n\nFor uploading files and text data, NetworkKit provides the `MultipartForm` structure, which handles creating the correct headers and formatting the data for multipart form requests. You can conveniently combine multiple fields within the `MultipartForm` since it uses the `@resultBuilder` Swift property.\n\n#### Uploading Data Field\n\nFor sending binary data, you can use the `DataField` structure. This allows you to include raw data in the request body.\n\n```swift\nstruct PostImageRequest: HttpRequest {\n    let path: String = \"/image\"\n    let method: HttpMethod = .post\n\n    private let imageData: Data\n\n    init(imageData: Data) {\n        self.imageData = imageData\n    }\n\n    var body: some HttpBody {\n        MultipartForm {\n            DataField(\n                named: \"image\",\n                data: imageData,\n                mimeType: .jpegImage,\n                fileName: \"image\"\n            )\n        }\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nPOST https://api.example.com/image\nContent-Type: multipart/form-data; boundary=BOUNDARY_STRING\n\n--BOUNDARY_STRING\nContent-Disposition: form-data; name=\"image\"; filename=\"image\"\nContent-Type: image/jpeg\n\n[Image Data]\n\n--BOUNDARY_STRING--\n```\n\n\u003c/details\u003e\n\n#### Uploading Text Field\n\nFor sending plain text data, you can use the `TextField` structure. This allows you to include text data in the request body.\n\n```swift\nstruct UpdateProfileRequest: HttpRequest {\n    let path: String = \"/profile\"\n    let method: HttpMethod = .post\n\n    private let bio: String\n\n    init(bio: String) {\n        self.bio = bio\n    }\n\n    var body: some HttpBody {\n        MultipartForm {\n            TextField(named: \"bio\", value: bio)\n        }\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nPOST https://api.example.com/profile\nContent-Type: multipart/form-data; boundary=BOUNDARY_STRING\n\n--BOUNDARY_STRING\nContent-Disposition: form-data; name=\"bio\"; filename=\"bio\"\nContent-Type: text/plain; charset=ISO-8859-1\n\n[Your Bio Content]\n\n--BOUNDARY_STRING--\n```\n\n\u003c/details\u003e\n\n## Error Handling\n\nWhen a request encounters an HTTP response with a non-successful status code, NetworkKit provides the flexibility to define how the package should handle the error. To customize this behavior, override the `failureBehavior(for:)` method in your request structure that conforms to the `HttpRequest` protocol. This method takes the status code as a parameter and returns an instance of `RequestFailureBehavior` that indicates how the error should be handled.\n\nFor instance, consider the following example where you want to provide a custom error type with a detailed description for a specific status code:\n\n```swift\nstruct GetBookRequest: HttpRequest {\n    let path: String = \"/books\"\n\n    private let bookID: String\n\n    init(bookID: String) {\n        self.bookID = bookID\n    }\n\n    var queryParameters: HttpQueryParameters? {\n        HttpQueryParameter(\"id\", value: bookID)\n    }\n\n    func failureBehavior(for statusCode: Int) -\u003e RequestFailureBehavior {\n        switch statusCode {\n        case 404:\n            return .throwError(RequestError.bookNotFound(bookID: bookID))\n        default:\n            return .default\n        }\n    }\n}\n\nextension GetBookRequest {\n    enum RequestError: Error, LocalizedError {\n        case bookNotFound(bookID: String)\n\n        var errorDescription: String? {\n            switch self {\n            case .bookNotFound(let bookID):\n                return \"Book with ID \\(bookID) not found.\"\n            }\n        }\n    }\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nGET https://api.example.com/books?id=YOUR_BOOK_ID\n```\n\n\u003c/details\u003e\n\nIn this example, the `GetBookRequest` structure defines a custom error enum `RequestError` for the 404 status code. The `failureBehavior(for:)` method returns `.throwError(RequestError.bookNotFound(bookID: bookID))` for the specified status code, causing the package to throw the custom error enum with its detailed description, including the book ID.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch1\u003eClient\u003c/h1\u003e\u003c/summary\u003e\n\nNetworkKit allows you to configure client settings separately from request creation, promoting scalability and ease of maintenance. This separation enables you to create multiple client configurations, each handling specific requests or targeting different client environments, such as development, preproduction, and production.\n\n## Defining a Client\n\nTo configure a client, create a structure that conforms to the `Client` protocol. This structure defines properties such as the client's scheme, host, port, and an optional `AccessTokenProvider` for managing access tokens and their automatic refreshing.\n\nHere's an example of defining a client configuration:\n\n```swift\nstruct MyClient: Client {\n    let scheme: String = \"https\" // optional. Defaults to \"https\".\n    let host: String = \"api.myserver.com\"\n    let port: Int? = nil // optional. Defaults to nil.\n    let accessTokenProvider: AccessTokenProvider? // optional. Defaults to nil.\n\n    init(accessTokenProvider: AccessTokenProvider? = nil) {\n        self.accessTokenProvider = accessTokenProvider\n    }\n}\n```\n\nIn this example, the `MyClient` structure specifies the client's scheme, host, and an optional access token provider for managing access tokens.\n\n## Client Configuration Properties\n\nWhen configuring a client using NetworkKit, you have the following properties that can be customized:\n\n| Property                 | Description                                                                     |\n|--------------------------|---------------------------------------------------------------------------------|\n| `scheme`                 | The scheme of the client (e.g., \"http\" or \"https\")                              |\n| `host`                   | The base URL of the client (e.g., \"api.example.com\")                           |\n| `port`                   | The port number for the client (optional)                                      |\n| `accessTokenProvider`    | An object responsible for managing access tokens and their automatic refreshing |\n| `jsonDecoder`                | The decoder used for parsing data responses                                     |\n\n## Performing Requests\n\nNetworkKit provides several methods to perform HTTP requests using the configured client. Each method caters to different scenarios, such as retrieving decoded data, fetching raw data, or simply executing a request.\n\n### Perform and Decode\n\nThe `perform` method is used when you want to retrieve and decode data from the client's response. This method takes an `HttpRequest` instance as its parameter and returns a decoded object of the specified type.\n\n```swift\nlet client = MyClient()\nlet getUserRequest = GetUserRequest(id: \"123\")\nlet user: User = try await client.perform(getUserRequest) // User should conforms to Decodable\n```\n\n### Perform Raw\n\nThe `performRaw` method is suitable when you want to fetch the raw data of the response without decoding it. This can be useful when you need to access the raw data for purposes such as file downloads.\n\n```swift\nlet client = MyClient()\nlet getImageRequest = GetImageRequest(imageID: \"456\")\nlet imageData: Data = try await client.performRaw(getImageRequest) // Returns the raw data of the response\n```\n\n### Perform Request\n\nIf you only want to execute a request without requiring any response data or raw data retrieval, you can use the `perform` method without specifying a return type.\n\n```swift\nlet client = MyClient()\nlet deletePostRequest = DeletePostRequest(postID: \"789\")\ntry await client.perform(deletePostRequest)\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch1\u003eAccessTokenProvider\u003c/h1\u003e\u003c/summary\u003e\n\nThe NetworkKit package simplifies access token management through the `AccessTokenProvider` protocol and the `AccessTokenType` enum.\n\n### Creating an AccessTokenProvider\n\nTo create an `AccessTokenProvider`, implement a class or struct conforming to the protocol. Here's an example:\n\n```swift\nfinal class KeychainAccessTokenProvider: AccessTokenProvider {\n    var accessToken: String? {\n        // Return the access token stored in the keychain here\n    } \n\n    func refreshAccessToken() async throws {\n        accessToken = // Implement token refreshing logic\n    }\n}\n```\n\n### Configuring an AccessTokenProvider in a Client\n\nInject your custom `AccessTokenProvider` into a client to enable access token management:\n\n```swift\nstruct MyClient: Client {\n    let host: String = \"api.example.com\"\n    let accessTokenProvider: AccessTokenProvider? // add the property of the Client protocol\n\n    // inject the access token provider in the initializer\n    init(accessTokenProvider: AccessTokenProvider? = nil) {\n        self.accessTokenProvider = accessTokenProvider\n    }\n}\n```\n\n### Setting the AccessTokenType in a Request\n\nSpecify how the access token should be added to the request header using the `accessTokenType` property inside a request:\n\n```swift\nstruct GetUserProfileRequest: HttpRequest {\n    let path: String = \"/user/profile\"\n    let method: HttpMethod = .get\n    let accessTokenType: AccessTokenType = .bearer\n}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to see the generated request\u003c/summary\u003e\n\n```http\nGET /user/profile\nHeaders:\n    Authorization: Bearer [Access Token]\n```\n\n\u003c/details\u003e\n\n### Performing a Request with an Access Token\n\n```swift\nlet accessTokenProvider = KeychainAccessTokenProvider()\nlet client = MyClient(accessTokenProvider: accessTokenProvider)\nlet userProfile: UserProfile = try await client.perform(GetUserProfileRequest())\n```\n\nWhen performing a request with an access token, the client will automatically add the token to the request header. If the provided access token is invalid, the client will attempt to refresh it using the `refreshAccessToken` method of the `AccessTokenProvider`. If the refreshed access token is still invalid, an error will be thrown, indicating the failure to authenticate the request.\n\n\u003c/details\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbpisano%2Fnetwork-kit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbpisano%2Fnetwork-kit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbpisano%2Fnetwork-kit/lists"}