Slide 1

Slide 1 text

Functional Networking Tomás Ruiz López Technical Lead at 47 Degrees @tomasruizlopez

Slide 2

Slide 2 text

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'

Slide 3

Slide 3 text

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'

Slide 4

Slide 4 text

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'

Slide 5

Slide 5 text

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'

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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) }

Slide 10

Slide 10 text

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) }

Slide 11

Slide 11 text

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) }

Slide 12

Slide 12 text

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) }

Slide 13

Slide 13 text

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() }

Slide 14

Slide 14 text

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() }

Slide 15

Slide 15 text

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() }

Slide 16

Slide 16 text

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() }

Slide 17

Slide 17 text

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() }

Slide 18

Slide 18 text

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() }

Slide 19

Slide 19 text

Bow bow-swift.io

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

func send(request: URLRequest, callback: @escaping (Result) !-> 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

Slide 22

Slide 22 text

func dataTask(with request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) !-> Void) Illegal states

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

func dataTaskADT(with request: URLRequest, completion: @escaping (Either) !-> Void) Callback-based communication & Side-effects

Slide 25

Slide 25 text

func dataTaskIO(with request: URLRequest) !-> IO Callbacks & Side-Effects Suspension

Slide 26

Slide 26 text

extension URLSession { func dataTaskIO(with request: URLRequest) !-> IO { 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() }^ } }

Slide 27

Slide 27 text

func send(request: URLRequest) !-> IO { 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

Slide 28

Slide 28 text

func send(request: URLRequest, session: URLSession, decoder: SafeDecoder) !-> IO Global dependencies Explicit dependencies

Slide 29

Slide 29 text

func send(request: URLRequest, config: APIConfig) !-> IO Global dependencies Explicit dependencies

Slide 30

Slide 30 text

func send(request: URLRequest) !-> (APIConfig) !-> IO Global dependencies Explicit dependencies

Slide 31

Slide 31 text

func send(request: URLRequest) !-> EnvIO Global dependencies Environmental Effects

Slide 32

Slide 32 text

func send(request: URLRequest) !-> EnvIO { 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

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Bow Open Api openapi.bow-swift.io

Slide 35

Slide 35 text

OpenAPI.yaml bow-openapi Swift Package

Slide 36

Slide 36 text

brew tap bow-swift/bow brew install bow-openapi bow-openapi "#name MyAPI "#schema Swagger.json "#output ./MyAPI Install Run

Slide 37

Slide 37 text

import MyAPI let articlesRequest = API.articles.getArticles() Using generated code

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Bow vs. Combine

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Bow vs. Combine Works in earlier versions of iOS and macOS Lazyness guarantees referential transparency

Slide 45

Slide 45 text

Bow vs. Combine io.retry(Schedule.exponential(.milliseconds(250)) .and(Schedule.recurs(10))) publisher.retry(10) Composable and powerful retrial policies

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Conclusion

Slide 48

Slide 48 text

Conclusion Bow bow-swift.io

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Functional Networking Tomás Ruiz López Technical Lead at 47 Degrees @tomasruizlopez