Code Monkey home page Code Monkey logo

tiny-networking's Introduction

TinyNetworking

This package contains a tiny networking library. It provides a struct Endpoint, which combines a URL request and a way to parse responses for that request. Because Endpoint is generic over the parse result, it provides a type-safe way to use HTTP endpoints.

Here are some examples:

A Simple Endpoint

This is an endpoint that represents a user's data (note that there are more fields in the JSON, left out for brevity):

struct User: Codable {
    var name: String
    var location: String?
}

func userInfo(login: String) -> Endpoint<User> {
    return Endpoint(json: .get, url: URL(string: "https://api.github.com/users/\(login)")!)
}

let sample = userInfo(login: "objcio")

The code above is just a description of an endpoint, it does not load anything. sample is a simple struct, which you can inspect (for example, in a unit test).

Here's how you can load an endpoint. The result is of type Result<User, Error>.

URLSession.shared.load(endpoint) { result in
   print(result)
}

Alternatively, you can use the async/await option.

let result = try await URLSession.shared.load(endpoint)

Authenticated Endpoints

Here's an example of how you can have authenticated endpoints. You initialize the Mailchimp struct with an API key, and use that to compute an authHeader. You can then use the authHeader when you create endpoints.

struct Mailchimp {
    let base = URL(string: "https://us7.api.mailchimp.com/3.0/")!
    var apiKey = env.mailchimpApiKey
    var authHeader: [String: String] { 
        ["Authorization": "Basic " + "anystring:\(apiKey)".base64Encoded] 
    }

    func addContent(for episode: Episode, toCampaign campaignId: String) -> Endpoint<()> {
        struct Edit: Codable {
            var plain_text: String
            var html: String
        }
        let body = Edit(plain_text: plainText(episode), html: html(episode))
        let url = base.appendingPathComponent("campaigns/\(campaignId)/content")
        return Endpoint<()>(json: .put, url: url, body: body, headers: authHeader)
    }
}

Custom Parsing

The JSON encoding and decoding are added as conditional extensions on top of the Codable infrastructure. However, Endpoint itself is not at all tied to that. Here's the type of the parsing function:

var parse: (Data?, URLResponse?) -> Result<A, Error>

Having Data as the input means that you can write our own functionality on top. For example, here's a resource that parses images:

struct ImageError: Error {}

extension Endpoint where A == UIImage {
    init(imageURL: URL) {
        self = Endpoint(.get, url: imageURL) { data in
            Result {
                guard let d = data, let i = UIImage(data: d) else { throw ImageError() }
                return i
            }
        }
    }
}

You can also write extensions that do custom JSON serialization, or parse XML, or another format.

Testing Endpoints

Because an Endpoint is a plain struct, it's easy to test synchronously without a network connection. For example, you can test the image endpoint like this:

XCTAssertThrows(try Endpoint(imageURL: someURL).parse(nil, nil).get())
XCTAssertThrows(try Endpoint(imageURL: someURL).parse(invalidData, nil).get())
XCTAssertNoThrow(try Endpoint(imageURL: someURL).parse(validData, nil).get())

Combine

Hsieh Min Che created a library that adds Combine endpoints to this library: https://github.com/Hsieh-1989/CombinedEndpoint

More Examples

More Documentation

The design and implementation of this library is covered extensively on Swift Talk. There's a collection with all the relevant episodes:

Networking

tiny-networking's People

Contributors

bennettsmith avatar chriseidhof avatar corysullivan avatar jagreenwood avatar ls-bertrand-landry-hetu avatar mackoj avatar mgacy avatar ralfebert avatar stgarrity avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tiny-networking's Issues

Get response headers

Hi I was wondering how I can get the response headers from a request, so that I can pass a value from the repsonse header to a diffrent request.

License?

Is this under any particular license if we wanted to play around with this?

A server with the specified hostname could not be found

I'm following the SwiftUI Collection videos, but I can't get this networking module to work.
I'm defining the Endpoint like the video:

var latest = Endpoint<FixerData>(
    json: .get,
    url: URL(string: "http://data.fixer.io/api/latest?access_key=3fcce37c14f9d10234cec75d1e624536&format=1")!
)

But I got the following error:

failure(Error Domain=NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found." UserInfo={NSUnderlyingError=0x600000cb6a30 {Error Domain=kCFErrorDomainCFNetwork Code=-1003 "(null)" UserInfo={_kCFStreamErrorCodeKey=-72000, _kCFStreamErrorDomainKey=10}}, NSErrorFailingURLStringKey=http://data.fixer.io/api/latest?access_key=3fcce37c14f9d10234cec75d1e624536&format=1, NSErrorFailingURLKey=http://data.fixer.io/api/latest?access_key=3fcce37c14f9d10234cec75d1e624536&format=1, _kCFStreamErrorDomainKey=10, _kCFStreamErrorCodeKey=-72000, NSLocalizedDescription=A server with the specified hostname could not be found.})

When I use the same URL above in a browser, it's working properly.

Cannot build with Xcode 12.5.1 or earlier

This code does not build with Xcode 12.5.1. Only works with not yet publicly released versions of iOS and dev tools.

The changes from #25 require beta iOS 15 developer tools and may not be backward compatible. Of course this hinges on what Apple releases next week but I feel that merging changes like this that are currently in beta are premature.

Building on Big Sur with Xcode 12.5.1 results in an error 'async' modifier is only valid when experimental concurrency is enabled on line 289 of Endpoint.swift.

[meta] generate API documentation

Not sure yet what's the most robust way to do this. Definitely run jazzy, maybe in a pre-commit hook or something? That way we can just generate it in the docs folder (together with some high-level documentation maybe) and use github pages. Any ideas?

Allow other http loading infrastructure

Since expectedStatusCode and parse are both internal it's not possible to reimplement the load method using a different url request loading infrastructure, e.g. using Alamofire or some other http client. Is there a specific reason they're not public?

[proposal] Extract setting up an URLRequest into a separate library

I'd like to propose a structural refactoring of TinyNetworking.

It's a bit daring but I am using it already in one of my projects and I thought I'd write it up and share it with you. Maybe its helpful to you or not; in any case I'd be grateful for feedback about the idea.

My starting point was that while I'm excited about the Endpoint abstraction that TinyNetworking provides, I am not a fan of these very-many-parameters initializers like:

public init(_ method: Method, url: URL, accept: ContentType? = nil, contentType: ContentType? = nil, body: Data? = nil, headers: [String:String] = [:], expectedStatusCode: @escaping (Int) -> Bool = expected200to300, timeOutInterval: TimeInterval = 10, query: [String:String] = [:], parse: @escaping (Data?, URLResponse?) -> Result<A, Error>) {
   ...
}

I think TinyNetworking does two things at once here: a) providing the endpoint abstraction (URLRequest+how to validate/parse the response) and b) provide convenience to setup the URLRequest.

I suggest to separate out the code to setup a URLRequest conveniently into a separate tiny library which TinyNetworking could depend on/re-export (or maybe not).

This simplifies the Endpoint abstraction a lot. It's a tiny bit more to write because you always need to create an URLRequest but overall for me this feels a lot more clean and tiny.

I did a fork with a prototype implementation here:
https://github.com/ralfebert/tiny-networking/commits/urlrequest
https://github.com/ralfebert/sweeturlrequest

This could be backward-compatible, but I wanted to do a rough sketch at first. Here is an example what the usage of the URLRequest-only-based API could look like compared to what it looks like now.

I also extended Endpoint a bit to model the status code validation more generically as 'validate' function.

What do you think about such a separation?

Please tag latest release

I've got two packages which are dependent on this package and SwiftPM is unable to reconcile.

I believe tagging the latest commit on master will resolve my issue.

Please Tag Latest Commit

The most recent tag, 0.4.1, does not include the latest commits adding support for loading an endpoint with Swift Concurrency. As a result, the async version of URLSession.load(_:) is only available if packages specify a dependency on TinyNetworking with a branch or revision dependency requirement. However, SPM regards these as unstable and will not allow specifying a dependency on a stable version of a package with an unstable dependency requirement.

If you specify an unstable dependency requirement in one library to have access to async await:

let package = Package(
    name: "swift-foo",
    products: [...],
    dependencies: [
        .package(url: "https://github.com/objcio/tiny-networking.git", revision: "ee700f5")
    ],
    targets: [...]
)

and try to specify a stable dependency requirement in another library that depends on it:

let package = Package(
    name: "swift-bar",
    products: [...],
    dependencies: [
        .package(url: "https://github.com/me/swift-foo.git", from: "1.0.6")
    ],
    targets: [...]
)

Dependency resolution fails:

Failed to resolve dependencies Dependencies could not be resolved because ‘swift-bar’ depends on ‘swift-foo’ 1.0.6..<2.0.0.
‘swift-foo’ >= 1.0.6 cannot be used because package ‘swift-foo’ is required using a stable-version but ‘swift-foo’ depends on an unstable-version package ‘tiny-networking’

Adopting async/await

What are your thoughts on adding an async/await option? Would you consider a PR with sonething like the following?

@available(iOS 15, macOS 12.0, watchOS 8, tvOS 15, *)
public extension URLSession {

    func load<A>(_ e: Endpoint<A>) async throws -> A {
        let request = e.request
        let (data, resp) = try await data(for: request)
        guard let h = resp as? HTTPURLResponse else {
            throw UnknownError()
        }
        guard e.expectedStatusCode(h.statusCode) else {
            throw WrongStatusCodeError(statusCode: h.statusCode, response: h, responseBody: data)
        }
        return try e.parse(data, resp).get()
    }
}

Cannot Archive with TinyNetworking as Swift Package in Xcode 13 RC

In my main project we have been waiting for Xcode 13 RC to drop to hopefully fix the errors when trying to archive a project that has TinyNetworking. It has always compiled, built and archived with Xcode 12.5.x and we have been using this package for our new features.

Xcode 13 RC was released a few hours ago.

However, we are still getting errors when archiving (building and running from Xcode 13 RC work fine still).

To replicate, I created a brand new Xcode project in Xcode 13RC. It was a SwiftUI app. I did no coding and immediately archived. It was successfully archived. Then I added TinyNetworking through the SPM Xcode interface. I could build and run and saw "Hello World" on my physical device. Lastly, I tried to archive the project and there was the error again.

"Cannot find 'dataTaskPublisher' in scope"

Screen Shot 2021-09-14 at 6 28 04 PM

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.