Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Networking

Functional Networking

This talk presents a set of refactorings that will help make your Swift networking code more functional. It also features a tool, Bow OpenAPI, that generates your network layer from a Swagger/OpenAPI specification file into a Swift Package.

90a2784ff299fdc5b0a46893680e4a44?s=128

Tomás Ruiz-López

February 03, 2020
Tweet

More Decks by Tomás Ruiz-López

Other Decks in Programming

Transcript

  1. Functional Networking Tomás Ruiz López Technical Lead at 47 Degrees

    @tomasruizlopez
  2. openapi: "3.0.0" info: title: Blogging platform version: "1.0.0" paths: /articles:

    get: tags: - Article operationId: getArticlesWithTag parameters: - name: tag in: query required: true schema: type: string responses: '200': description: The list of articles for the specified tag content: application/json: schema: $ref: '#/components/schemas/Articles'
  3. openapi: "3.0.0" info: title: Blogging platform version: "1.0.0" paths: /articles:

    get: tags: - Article operationId: getArticlesWithTag parameters: - name: tag in: query required: true schema: type: string responses: '200': description: The list of articles for the specified tag content: application/json: schema: $ref: '#/components/schemas/Articles'
  4. openapi: "3.0.0" info: title: Blogging platform version: "1.0.0" paths: /articles:

    get: tags: - Article operationId: getArticlesWithTag parameters: - name: tag in: query required: true schema: type: string responses: '200': description: The list of articles for the specified tag content: application/json: schema: $ref: '#/components/schemas/Articles'
  5. openapi: "3.0.0" info: title: Blogging platform version: "1.0.0" paths: /articles:

    get: tags: - Article operationId: getArticlesWithTag parameters: - name: tag in: query required: true schema: type: string responses: '200': description: The list of articles for the specified tag content: application/json: schema: $ref: '#/components/schemas/Articles'
  6. components: schemas: Articles: type: array items: $ref: '#/components/schemas/Article' Article: type:

    object required: - id - title properties: id: type: integer name: type: string
  7. components: schemas: Articles: type: array items: $ref: '#/components/schemas/Article' Article: type:

    object required: - id - title properties: id: type: integer name: type: string
  8. components: schemas: Articles: type: array items: $ref: '#/components/schemas/Article' Article: type:

    object required: - id - title properties: id: type: integer name: type: string
  9. func getArticles(withTag tag: String, callback: @escaping (Result<[Article], Error>) !-> Void)

    { let path = basePath + "/articles" var components = URLComponents(string: path) components!?.queryItems = [URLQueryItem(name: "tag", value: tag)] guard let url = components!?.url else { callback(.failure(NetworkError.malformedURL)) return } var request = URLRequest(url: url) request.httpMethod = "GET" headers.forEach { header in request.setValue(header.value, forHTTPHeaderField: header.key) } send(request: request, callback: callback) }
  10. func getArticles(withTag tag: String, callback: @escaping (Result<[Article], Error>) !-> Void)

    { let path = basePath + "/articles" var components = URLComponents(string: path) components!?.queryItems = [URLQueryItem(name: "tag", value: tag)] guard let url = components!?.url else { callback(.failure(NetworkError.malformedURL)) return } var request = URLRequest(url: url) request.httpMethod = "GET" headers.forEach { header in request.setValue(header.value, forHTTPHeaderField: header.key) } send(request: request, callback: callback) }
  11. func getArticles(withTag tag: String, callback: @escaping (Result<[Article], Error>) !-> Void)

    { let path = basePath + "/articles" var components = URLComponents(string: path) components!?.queryItems = [URLQueryItem(name: "tag", value: tag)] guard let url = components!?.url else { callback(.failure(NetworkError.malformedURL)) return } var request = URLRequest(url: url) request.httpMethod = "GET" headers.forEach { header in request.setValue(header.value, forHTTPHeaderField: header.key) } send(request: request, callback: callback) }
  12. func getArticles(withTag tag: String, callback: @escaping (Result<[Article], Error>) !-> Void)

    { let path = basePath + "/articles" var components = URLComponents(string: path) components!?.queryItems = [URLQueryItem(name: "tag", value: tag)] guard let url = components!?.url else { callback(.failure(NetworkError.malformedURL)) return } var request = URLRequest(url: url) request.httpMethod = "GET" headers.forEach { header in request.setValue(header.value, forHTTPHeaderField: header.key) } send(request: request, callback: callback) }
  13. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() }
  14. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() }
  15. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() }
  16. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() }
  17. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() }
  18. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() }
  19. Bow bow-swift.io

  20. func send(request: URLRequest, callback: @escaping (Result<[Article], Error>) !-> Void) {

    URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode([Article].self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() } Insufficient abstraction
  21. func send<T: Decodable>(request: URLRequest, callback: @escaping (Result<T, Error>) !-> Void)

    { URLSession.shared.dataTask(with: request) { data, response, error in if let data = data, let response = response as? HTTPURLResponse { switch response.statusCode { case 200 !!..< 300: if let decoded = try? JSONDecoder().decode(T.self, from: data) { callback(.success(decoded)) } else { callback(.failure(NetworkError.notDecodable)) } default: callback(.failure(NetworkError.malformedURL)) } } else if let error = error { callback(.failure(error)) } }.resume() } Insufficient abstraction Parametric polymorphism
  22. func dataTask(with request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) !->

    Void) Illegal states
  23. func dataTaskADT(with request: URLRequest, completion: @escaping (Either<Error, (Data, URLResponse)>) !->

    Void) Illegal states Algebraic Data Types
  24. func dataTaskADT(with request: URLRequest, completion: @escaping (Either<Error, (Data, URLResponse)>) !->

    Void) Callback-based communication & Side-effects
  25. func dataTaskIO(with request: URLRequest) !-> IO<Error, (Data, URLResponse)> Callbacks &

    Side-Effects Suspension
  26. extension URLSession { func dataTaskIO(with request: URLRequest) !-> IO<Error, (Data,

    URLResponse)> { IO.async { callback in self.dataTask(with: request) { data, response, error in if let data = data, let response = response { callback(.right((data, response))) } else if let error = error { callback(.left(error)) } }.resume() }^ } }
  27. func send<T: Decodable>(request: URLRequest) !-> IO<Error, T> { URLSession.shared.dataTaskIO(with: request)

    .flatMap { response, data in guard let response = response as? HTTPURLResponse else { return IO.raiseError(NetworkError.malformedURL) } switch response.statusCode { case 200 !!..< 300: return JSONDecoder().safeDecode(T.self, from: data) default: return IO.raiseError(NetworkError.malformedURL) } }^ } Global dependencies
  28. func send<T: Decodable>(request: URLRequest, session: URLSession, decoder: SafeDecoder) !-> IO<Error,

    T> Global dependencies Explicit dependencies
  29. func send<T: Decodable>(request: URLRequest, config: APIConfig) !-> IO<Error, T> Global

    dependencies Explicit dependencies
  30. func send<T: Decodable>(request: URLRequest) !-> (APIConfig) !-> IO<Error, T> Global

    dependencies Explicit dependencies
  31. func send<T: Decodable>(request: URLRequest) !-> EnvIO<APIConfig, Error, T> Global dependencies

    Environmental Effects
  32. func send<T: Decodable>(request: URLRequest) !-> EnvIO<APIConfig, Error, T> { EnvIO

    { config in config.session.dataTaskIO(with: request) .flatMap { response, data in guard let response = response as? HTTPURLResponse else { return IO.raiseError(NetworkError.malformedURL) } switch response.statusCode { case 200 !!..< 300: return config.decoder.safeDecode(T.self, from: data) default: return IO.raiseError(NetworkError.malformedURL) } }^ } } Global dependencies Environmental Effects
  33. None
  34. Bow Open Api openapi.bow-swift.io

  35. OpenAPI.yaml bow-openapi Swift Package

  36. brew tap bow-swift/bow brew install bow-openapi bow-openapi "#name MyAPI "#schema

    Swagger.json "#output ./MyAPI Install Run
  37. import MyAPI let articlesRequest = API.articles.getArticles() Using generated code

  38. let config = API.Config(basePath: “https:!//url-to-my-server.com") let articles = try? articlesRequest.provide(config).unsafeRunSync()

    articlesRequest.provide(config).unsafeRunAsync { either in either.fold({ httpError in !/* !!... !*/ }, { articles in !/* !!... !*/ }) } Running a request
  39. import MyAPITest func testEmptyResponse() { let successConfig = API.Config(basePath: "https:!//url-to-my-server.com")

    .stub(json: "[]", code: 200) let expected = [] assert(API.article.getArticles(), withConfig: successConfig, succeeds: expected, "An empty array was expected") } func testContentResponse() { let successConfig = API.Config(basePath: "https:!//url-to-my-server.com") .stub(contentsOfFile: fileURL, code: 200) let expected = [ Article(identifier: 1234, title: "Intro to Bow OpenAPI") ] assert(API.article.getArticles(), withConfig: successConfig, succeeds: expected, "An array with a single element was expected") } Testing a request
  40. import MyAPITest func testEmptyResponse() { let successConfig = API.Config(basePath: "https:!//url-to-my-server.com")

    .stub(json: "[]", code: 200) let expected = [] assert(API.article.getArticles(), withConfig: successConfig, succeeds: expected, "An empty array was expected") } func testContentResponse() { let successConfig = API.Config(basePath: "https:!//url-to-my-server.com") .stub(contentsOfFile: fileURL, code: 200) let expected = [ Article(identifier: 1234, title: "Intro to Bow OpenAPI") ] assert(API.article.getArticles(), withConfig: successConfig, succeeds: expected, "An array with a single element was expected") } Testing a request
  41. import MyAPITest func testEmptyResponse() { let successConfig = API.Config(basePath: "https:!//url-to-my-server.com")

    .stub(json: "[]", code: 200) let expected = [] assert(API.article.getArticles(), withConfig: successConfig, succeeds: expected, "An empty array was expected") } func testContentResponse() { let successConfig = API.Config(basePath: "https:!//url-to-my-server.com") .stub(contentsOfFile: fileURL, code: 200) let expected = [ Article(identifier: 1234, title: "Intro to Bow OpenAPI") ] assert(API.article.getArticles(), withConfig: successConfig, succeeds: expected, "An array with a single element was expected") } Testing a request
  42. Bow vs. Combine

  43. Bow vs. Combine Works in earlier versions of iOS and

    macOS
  44. Bow vs. Combine Works in earlier versions of iOS and

    macOS Lazyness guarantees referential transparency
  45. Bow vs. Combine io.retry(Schedule.exponential(.milliseconds(250)) .and(Schedule.recurs(10))) publisher.retry(10) Composable and powerful retrial

    policies
  46. binding( user !<- API.user.getMyProfile(), articles !<- API.article.getArticles(withTag: "featured"), home !<-

    makeHome(for: user.get, with: articles.get), yield: home.get) Bow vs. Combine Convenient imperative-style syntax
  47. Conclusion

  48. Conclusion Bow bow-swift.io

  49. Conclusion Bow bow-swift.io Bow Open Api openapi.bow-swift.io

  50. Conclusion FP Bow bow-swift.io Bow Open Api openapi.bow-swift.io

  51. Functional Networking Tomás Ruiz López Technical Lead at 47 Degrees

    @tomasruizlopez