{"id":21684027,"url":"https://github.com/tordwessman/vsnl","last_synced_at":"2026-05-20T03:08:49.953Z","repository":{"id":230083268,"uuid":"778346098","full_name":"TordWessman/VSNL","owner":"TordWessman","description":"A downscaled network layer in Swift for iOS","archived":false,"fork":false,"pushed_at":"2024-10-22T21:45:24.000Z","size":50,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-20T11:29:24.582Z","etag":null,"topics":["async","ios","json","network","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/TordWessman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2024-03-27T14:51:35.000Z","updated_at":"2024-10-22T21:45:29.000Z","dependencies_parsed_at":"2024-05-28T10:55:06.019Z","dependency_job_id":"0ea0e29b-cc9e-4ee6-be01-69f8ef7de98b","html_url":"https://github.com/TordWessman/VSNL","commit_stats":null,"previous_names":["tordwessman/vsnl"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/TordWessman/VSNL","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TordWessman%2FVSNL","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TordWessman%2FVSNL/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TordWessman%2FVSNL/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TordWessman%2FVSNL/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TordWessman","download_url":"https://codeload.github.com/TordWessman/VSNL/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TordWessman%2FVSNL/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33244136,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-19T15:49:41.270Z","status":"online","status_checked_at":"2026-05-20T02:00:07.149Z","response_time":356,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["async","ios","json","network","swift"],"created_at":"2024-11-25T16:14:21.510Z","updated_at":"2026-05-20T03:08:49.938Z","avatar_url":"https://github.com/TordWessman.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# VSNL\nSwift Network Layer\n\nVSNL, or \"Vintage Scaffolding Network Layer,\" is a simple, yet modular and thread-safe HTTP/JSON network layer written in Swift, wrapping `URLSession`. It employs the `async/await` and `throws`, reducing complexity and increasing visibility. \n\nThe goal is to provide a network interface that is simple and easy to read. Instead of configuring the request _calls_, the request _models_ contains the request configuration.\n\n## 1. Basic Examples\n\nThe `VSNL.SimpleClient` provides basic interface configuration options suitable for most applications.\n\n### 1.1 Basic Usage\n\nWill make a `GET https://example.com/some/path?requestValue=42` request.\n```swift\nimport VSNL\n\n// The `host` parameters provide the \"root URL\" for every request. It can optionally define a URL Scheme (\"http://example.com\") and/or a base path (\"example.com/base/path\").\nlet client = VSNL.SimpleClient(host: \"example.com\")\ndo {\n    if let response = try await client.send(MyGetRequest(requestValue: 42)) {\n        print(response)\n    } else {\n        print(\"Request was canceled\")\n    }\n} catch {\n    print(\"Request failed: \\(error)\")\n}\n\n// ...\n\n// `MyGetRequest` defines the HTTP Method (defaults to GET) and the HTTP path.\nstruct MyGetRequest: VSNL.Request {\n\n    // This request expects a response object of type `MyRequest.MyResponse`.\n    typealias ResponseType = MyResponse\n\n    // A request parameter.\n    let requestValue: Int\n\n    // The (relative) path to the resource.\n    func path() -\u003e String { \"/some/path\" }\n\n    // Definition of the response object. In this case, { \"someReturnValue\": Int }\n    struct MyResponse: Decodable { let someReturnValue: Int }\n}\n```\n\n### 1.2 Basic Usage, including Session Configuration\n\nIn most cases, requests need to be decorated with extra information. This could be an authentication token of some sort or an API key.\n\nThe following example will perform a \"sign-in\" (`POST https://example.com/signin?apiKey=1234` with the body `{\"user\": user, \"password\": password}`.)  It will set the \"authentication token\" for every consecutive request if the sign-in is successful.\n\nThe example below displays the usage of the \"global\" configuration in the `VSNL.Session` bound to `client`. \n\n* `session.setQueryStringParameter(key:value:)` Will set a query string parameter to every request.\n* `session.setHeader(key:value:)` Will set an HTTP header value which will be included in every request.\n\nIn the example below, if the sign-in request was successful, the response model, `SignInRequest.Response`, provides a `jwt` token used for consecutive requests.\n\nThis configuration is also applicable for `VSNL.Client` and `VSNL.TypedClient`.\n```swift\nlet session = VSNL.Session(host: \"example.com\")\n\n// Every request (regardless of HTTP method will include the query string parameter \"apiKey=1234\").\nawait session.setQueryStringParameter(key: \"apiKey\", value: \"1234\")\nlet client = VSNL.SimpleClient(session: session)\n\ndo {\n    if let response = try await client.send(SignInRequest(user: \"foo\", password: \"bar\")) {\n        if response.authenticated, let jwt = response.jwt {\n\n            // Every consecutive request contains the header \"Authentication: Bearer \u003cjwt\u003e\".\n            await client.session.setHeader(key: \"Authentication\", value: \"Bearer \\(jwt)\")\n        } else {\n            print(\"User not authenticated\")\n        }\n    } else {\n        print(\"Sign in task was canceled.\")\n    }\n} catch {\n    print(\"Sign in failed with error: \\(error).\")\n}\n\n// ... \n\nstruct SignInRequest: VSNL.Request {\n    typealias ResponseType = Response\n\n    let user: String\n    let password: String\n    func path() -\u003e String { \"/signin\" }\n    func method() -\u003e VSNL.HttpMethod { .post }\n    struct Response: Decodable { let authenticated: Bool, jwt: String? }\n}\n```\n## 2. Architecture\n\nThe core implementation of the client is the [VSNLDefaultClient\u003cT: Decodable\u003e](Sources/VSNL/VSNLTypedClient.swift). VSNL does, however, provide a set of simplified interfaces.\n\n* `VSNL.Client` Is the primary network interface, allowing more accurate investigation of responses. \n* `VSNL.SimpleClient` Is a more straight-forward interface with reduced configuration options.\n* `VSNL.TypedClient` Is similar to `VSNL.Client`, but it provides logic for \"expected errors\" (see [Typed Usage](#31-expected-errors) )\n\n### 2.1 The Request Model\n\nThe [VSNL.Request](Sources/VSNL/VSNLRequest.swift) protocol defines the input parameters for a network request as well as the output of the request. Every call is declared by specifying an implementation of `VSNL.Request`. The implementation will be `Encodable` and contain a set of parameters (encoded into a request) and must implement one or more request-specific methods (e.g. `path()`, `method()`). \n\nThe following code will generate a `POST example.com/relative/path` with the body `{\"foo\": 42, \"bar\": \"argh\" }` and expects a response of `{\"baz\": Int, \"boo\": Int? }`.\n\n```swift\nstruct MyRequestExample: VSNL.Request {\n\n    typealias ResponseType = MyRequestExample.MyResponseExample\n    let foo: Int\n    let bar: String\n\n    func path() -\u003e String { \"relative/path\" }\n    func method() -\u003e VSNL.HttpMethod { .post }\n    func headers() -\u003e [String : String]? { nil }\n\n    // Definition of response object ({\"baz\": Int, \"boo\": Int? })\n    struct MyResponseExample: Decodable {\n        let baz: Int\n        let boo: Int?\n    }\n}\n\n// ...\n\nlet client = VSNL.SimpleClient(host: \"example.com\")\nlet response = try? await client.send(MyRequestExample(foo: 42, bar: \"argh\"))\n```\n\n### 2.2 Other Classes\n* `VSNL.Session` Represent shared client configuration.\n* `URLSession` Is the default data transport layer.\n* `VSNLRequestFactory` Is responsible of creating an `URLRequest`.\n\n## 3. Advanced Examples\n\nThere are many situations where the configurational capabilities of the `VSNL.SimpleClient` is insufficient. `VSNL.SimpleClient` wraps a `VSNL.TypedClient`, which provides more configuration options. The `VSNL.Client` is a middle ground that supports everything `VSNL.TypedClient` does but without the \"expected error\" logic.\n\n### 3.1 Expected Errors\n\nOften, backends have an \"expected error,\" which employs a well-defined format. In contrast to `VSNL.SimpleClient` (which always will throw an error if a result is unsuccessful), A `VSNL.TypedClient` provides a mechanism for defining \"recoverable errors\" by specifying the error model during instantiation  (`VSNL.TypedClient\u003cErrorModelType\u003e`).\n\nHere's an example of an implementation:\n```swift\nimport VSNL\n\n// This is a common error the backend uses to communicate recoverable information.\nstruct ExpectedErrorModel: Decodable {\n    let message: String\n    let code: Int\n}\n\nstruct Request: VSNL.Request {\n    typealias ResponseType = Response\n    /* ... request properties ... */\n    func path() -\u003e String { \"/path\" }\n    struct Response: Decodable { /* ... response properties ... */ }\n}\n\nlet session = VSNL.Session(host: \"example.com\")\n\n// The client is typed to `ExpectedErrorModel`. If the HTTP status code != 200, the client will try to parse the response into an `ExpectedErrorModel`.\nlet client: VSNL.TypedClient\u003cExpectedErrorModel\u003e = VSNL.TypedClient(session: session)\n\ndo {\n    // Make a request. If `response?.result` is not `nil`, the request was successful _or_ a \"recoverable error\" occurred.\n    if let response = try await client.send(Request()),\n        let result = response.result {\n        switch(result) {\n        case .success(let responseModel):\n            print(\"Got response: \\(responseModel)\")\n        case .failure(let expectedErrorModel):\n            print(\"This can happen sometimes: \\(expectedErrorModel)\")\n        }\n    } else { print(\"Request task was canceled\") }\n} catch {\n    print(\"Request failed critically!\")\n}\n```\n\n### 3.2 Other Configuration Options\nHere, we'll employ a `VSNL.Client`  to demonstrate some of the other available configuration options. \n\n* Inject a custom `URLSession` (with a separate cache).\n* Set the request cache policy by injecting a custom `VSNLDefaultRequestFactory`.\n* Check the HTTP response headers.\n* Use `CodingKeys` to mask out one of the properties in a `VSNL.Request` in order for it to be used when composing the `path()`.\n* Use custom headers for a `VSNL.Request`.\n* See what happens if we get an HTTP status code 204 from the backend.\n\n```swift\n // Example of a request with `CodingKeys` to mask out some properties.\nstruct Request: VSNL.Request {\n    typealias ResponseType = Response\n\n    // Encoded property\n    let aProperty: Int\n    let id: Int\n\n    // Make sure `id` is not encoded, since it's used to derive the `path()`.\n    enum CodingKeys: String, CodingKey {\n        case aProperty = \"a_property\"\n    }\n\n    func path() -\u003e String { \"/path/\\(id)\" }\n    func method() -\u003e VSNL.HttpMethod { .put }\n\n    // Enables custom headers to be set for this request type only.\n    func headers() -\u003e [String : String]? { [\"Special-Header\": \"A-very-very-very-very-very-secret-message\"] }\n\n    struct Response: Decodable { /* ... response properties ... */ }\n}\n\n// Set up a custom `URLSession` with a custom cache configuration.\nlet urlSessionConfiguration = URLSessionConfiguration.default\nurlSessionConfiguration.urlCache = URLCache(memoryCapacity: 2 * 1024 * 1024,\n                                            diskCapacity: 8 * 1024 * 1024)\nlet myURLSession = URLSession(configuration: urlSessionConfiguration)\n\n// Set up cache-load policy and timeout interval.\nlet requestFactory = VSNLDefaultRequestFactory(cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5.0)\n\nlet client = VSNL.Client(\n    session: VSNL.Session(host: \"example.com\"),\n    network: myURLSession,\n    requestFactory: requestFactory)\n\ndo {\n    // Send a `PUT https://example.com/path/44`.\n    if let response = try await client.send(Request(aProperty: 42, id: 44)) {\n\n        // Fetch the HTTP headers from the response.\n        if let responseHeaders = response.headers {\n            print(\"Got response headers: \\(responseHeaders)\")\n        }\n\n        // Fetch the model from the response.\n        guard let responseModel = response.model else {\n\n            // If the responseModel was nil, it should be a HTTP 204 (no content) reply.\n            return print(\"Got no response model. Was it a HTTP 204? (code: \\(response.code)\")\n        }\n        print(\"Got response: \\(responseModel)\")\n    }\n} catch {\n    print(\"Argh! \\(error)\")\n}\n```\n## 4. Unit Tests\nAn application needs to be able to inject a network layer to promote testability.\n\nEach of the VSNL-client implementations has their corresponding `protocol`s.\n* `VSNL.SimpleClient` implements `VSNLSimpleClient`\n* `VSNL.Client` implements `VSNLClient`\n* `VSNL.TypedClient` implements `VSNLTypedClient`\n\n### 4.1 VSNLSimpleClient and VSNLClient\nNeither `VSNLSimpleClient` or `VSNLClient` have any `associatedType`, so injection or mocking is relatively straight-forward.\n```swift\nimport VSNL\n\n// View model bound to a `VSNLSimpleClient`.\nclass ViewModel_SimpleClient {\n\n    let client: any VSNLSimpleClient\n    init(client: any VSNLSimpleClient) {\n        self.client = client\n    }\n}\n\n// View model bound to a `VSNLClient`.\nclass ViewModel_Client {\n\n    let client: any VSNLClient\n    init(client: any VSNLClient) {\n        self.client = client\n    }\n}\n\n/** Example of simple mock-class.\n    Implements both `VSNLClient` and `VSNLSimpleClient` because I'm lazy. */\nactor MyMockClient: VSNLClient, VSNLSimpleClient {\n\n    var responseModel: Decodable?\n    var error: Error?\n\n    func send\u003cRequestType: VSNLRequest\u003e(_ request: RequestType) async throws -\u003e VSNLResponse\u003cRequestType, VSNLNoErrorModelDefined\u003e? {\n\n        if let error { throw error }\n        if let responseModel { return VSNLResponse(code: 200, model: responseModel as? RequestType.ResponseType) }\n        return nil\n    }\n\n    @discardableResult\n    func send\u003cRequestType: VSNL.Request\u003e(_ request: RequestType) async throws -\u003e\n    RequestType.ResponseType? {\n        if let error { throw error }\n        return responseModel as? RequestType.ResponseType\n    }\n}\n\nfunc production() {\n\n    // VSNL.SimpleClient usage.\n    let clientSimple = VSNL.SimpleClient(session: VSNL.Session(host: \"www.com\"))\n    let vmSimple = ViewModel_SimpleClient(client: clientSimple)\n\n    // VSNL.Client usage.\n    let client = VSNL.Client(session: VSNL.Session(host: \"www.com\"))\n    let vm = ViewModel_Client(client: client)\n}\n\nfunc testViewModels() {\n\n    let client = MyMockClient()\n    // Yay! I'm to lazy to write two mock classes for VSNLClient and VSNLSimpleClient\n    let vm_simple = ViewModel_SimpleClient(client: client)\n    let vm = ViewModel_Client(client: client)\n}\n```\n\n### 4.2 VSNLTypedClient\nTo inject and mock a `VSNLTypedClient`, some idiosyncrasy is required. In short, create a protocol corresponding to your `VSNLTypedClient` client's implementation and use this for conformance and reference. Here's an example of code that do just that.\n```swift\n// Define an \"expected error\" type used by your network client.\nstruct MyErrorType: Decodable {}\n\n// Declare a protocol for your specific `VSNL.TypedClient` implementation.\nprotocol MyTypedClientProtocol: VSNLTypedClient where ErrorType == MyErrorType { }\n\n// Make sure your network client implementation conforms to your protocol.\nextension VSNL.TypedClient\u003cMyErrorType\u003e : MyTypedClientProtocol { }\n\n// Create a simple mock-nework.\nactor MyTypedMockClient: MyTypedClientProtocol {\n\n    var responseModel: Decodable?\n    var errorModel: Decodable?\n    var error: Error?\n\n    func send\u003cRequestType: VSNLRequest\u003e(_ request: RequestType) async throws -\u003e VSNLResponse\u003cRequestType, MyErrorType\u003e? {\n\n        if let error { throw error }\n        if let responseModel { return VSNLResponse(code: 200, model: responseModel as? RequestType.ResponseType) }\n        if let errorModel { return VSNLResponse(code: 444, model: nil, error: errorModel as? MyErrorType) }\n        return nil\n    }\n}\n\n// View model where the client is injected.\nclass ViewModel_Typed {\n\n    // The client adhers to the associated\n    let client: any MyTypedClientProtocol\n\n    // Allow injection of any client class that implements `MyNetworkClientProtocol`\n    init(client: any MyTypedClientProtocol) {\n        self.client = client\n    }\n}\n\nfunc productionTyped() async {\n\n    // Use the production client\n    let client = VSNL.TypedClient\u003cMyErrorType\u003e(session: VSNL.Session(host: \"example.com\"))\n    let vm = ViewModel_Typed(client: client)\n}\n\nfunc testTyped() {\n    // Use the mock client for unit tests\n    let client_mock = MyTypedMockClient()\n    let vm_for_unit_tests = ViewModel_Typed(client: client_mock)\n}\n```\n\n## 5. Other Examples\n* [Simple Weather app](Example/VSNLExample)\n* [User handling](Example/VSNLExample/UsersExample.swift)\n* [Basic Requests](Example/VSNLExample/SimpleExamples.swift)\n* [Typed Requests](Example/VSNLExample/AdvancedExamples.swift)\n* [ViewModel Injection](Example/VSNLExample/ViewModelExample.swift)\n\n## 6. About\n_VSNL_ was an acronym for \"Very Simple Network Layer.\" Still, once I wrote it, I realized it wasn't very simple anymore, so I believe it's a more suitable abbreviation for \"Vintage Scaffolding Network Layer\" or \"Vampires Spreading Neurotic Love.\"\n\n## 7. License\nVSNL is released under the MIT license. See [LICENCE](LICENCE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftordwessman%2Fvsnl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftordwessman%2Fvsnl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftordwessman%2Fvsnl/lists"}