From ed54cfebcd0accac856bbbb92bf33f70464c2908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Pi=C3=B1era=20Buend=C3=ADa?= Date: Tue, 8 Oct 2019 15:14:31 +0200 Subject: [PATCH] Add HTTPClient utility (#508) * Remove old Ruby installation scripts and update the installation command to point to the install.tuist.io domain * Create a version file in the latest directory * Add HTTPClient with a read method to download Data from a URL * Some style fixes * Address Marcin's comments on the PR * Update CHANGELOG * Style fix * Address Kas's comments * Fix tests --- CHANGELOG.md | 4 + README.md | 1 - Rakefile | 6 + Sources/TuistCore/Constants.swift | 4 + .../Extensions/URL+TestData.swift | 7 + Sources/TuistEnvKit/HTTP/HTTPClient.swift | 121 ++++++++++++++++++ .../HTTP/HTTPClientTests.swift | 28 ++++ .../HTTP/Mocks/MockHTTPClient.swift | 47 +++++++ script/install | 14 +- script/uninstall | 5 +- 10 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 Sources/TuistCoreTesting/Extensions/URL+TestData.swift create mode 100644 Sources/TuistEnvKit/HTTP/HTTPClient.swift create mode 100644 Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift create mode 100644 Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f6f83ea..81184100f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/ ## Next +### Added + +- `HTTPClient` utility class to `TuistEnvKit` https://github.com/tuist/tuist/pull/508 by @pepibumur. + ## 0.18.1 ### Removed diff --git a/README.md b/README.md index 192e8757b..e85b6ca0c 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ The list of actions will likely grow as we get feedback from you. ```bash bash <(curl -Ls https://tuist.io/install) - ``` ## Bootstrap your first project 🌀 diff --git a/Rakefile b/Rakefile index 5bff414b7..1d2a200bc 100644 --- a/Rakefile +++ b/Rakefile @@ -9,6 +9,7 @@ require "google/cloud/storage" require "encrypted/environment" require 'colorize' require 'highline' +require 'tmpdir' Cucumber::Rake::Task.new(:features) do |t| t.cucumber_opts = "--format pretty" @@ -110,6 +111,11 @@ def release bucket.create_file("build/tuist.zip", "latest/tuist.zip").acl.public! bucket.create_file("build/tuistenv.zip", "latest/tuistenv.zip").acl.public! + Dir.mktmpdir do |tmp_dir| + version_path = File.join(tmp_dir, "version") + File.write(version_path, version) + bucket.create_file(version_path, "latest/version").acl.public! + end end def system(*args) diff --git a/Sources/TuistCore/Constants.swift b/Sources/TuistCore/Constants.swift index aa6aeeee2..e9c8a6e70 100644 --- a/Sources/TuistCore/Constants.swift +++ b/Sources/TuistCore/Constants.swift @@ -13,4 +13,8 @@ public class Constants { public class EnvironmentVariables { public static let colouredOutput = "TUIST_COLOURED_OUTPUT" } + + public class GoogleCloud { + public static let relasesBucketURL = "https://storage.googleapis.com/tuist-releases/" + } } diff --git a/Sources/TuistCoreTesting/Extensions/URL+TestData.swift b/Sources/TuistCoreTesting/Extensions/URL+TestData.swift new file mode 100644 index 000000000..9a8079791 --- /dev/null +++ b/Sources/TuistCoreTesting/Extensions/URL+TestData.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension URL { + static func test() -> URL { + return URL(string: "https://test.tuist.io")! + } +} diff --git a/Sources/TuistEnvKit/HTTP/HTTPClient.swift b/Sources/TuistEnvKit/HTTP/HTTPClient.swift new file mode 100644 index 000000000..c2b56277b --- /dev/null +++ b/Sources/TuistEnvKit/HTTP/HTTPClient.swift @@ -0,0 +1,121 @@ +import Basic +import Foundation +import TuistCore + +enum HTTPClientError: FatalError { + case clientError(URL, Error) + case noData(URL) + case copyFileError(AbsolutePath, Error) + case missingResource(URL) + + /// Error type + var type: ErrorType { + switch self { + case .clientError: + return .abort + case .noData: + return .abort + case .copyFileError: + return .abort + case .missingResource: + return .abort + } + } + + /// Error description. + var description: String { + switch self { + case let .clientError(url, error): + return "The request to \(url.absoluteString) errored with: \(error.localizedDescription)" + case let .noData(url): + return "The request to \(url.absoluteString) returned no data" + case let .copyFileError(path, error): + return "The file could not be copied into \(path.pathString): \(error.localizedDescription)" + case let .missingResource(url): + return "Couldn't locate resource downloaded from \(url.absoluteString)" + } + } +} + +protocol HTTPClienting { + /// Fetches the content from the given URL and returns it as a data. + /// + /// - Parameter url: URL to download the resource from. + /// - Returns: Response body as a data. + /// - Throws: An error if the request fails. + func read(url: URL) throws -> Data + + /// Downloads the resource from the given URL into the file at the given path. + /// + /// - Parameters: + /// - url: URL to download the resource from. + /// - to: Path where the file should be downloaded. + /// - Throws: An error if the dowload fails. + func download(url: URL, to: AbsolutePath) throws +} + +final class HTTPClient: HTTPClienting { + // MARK: - Attributes + + /// URL session. + fileprivate let session: URLSession = .shared + + // MARK: - HTTPClienting + + /// Fetches the content from the given URL and returns it as a data. + /// + /// - Parameter url: URL to download the resource from. + /// - Returns: Response body as a data. + /// - Throws: An error if the request fails. + func read(url: URL) throws -> Data { + var data: Data? + var error: Error? + + let semaphore = DispatchSemaphore(value: 0) + session.dataTask(with: url) { responseData, _, responseError in + data = responseData + error = responseError + semaphore.signal() + }.resume() + semaphore.wait() + + if let error = error { + throw HTTPClientError.clientError(url, error) + } + guard let resultData = data else { + throw HTTPClientError.noData(url) + } + return resultData + } + + /// Downloads the resource from the given URL into the file at the given path. + /// + /// - Parameters: + /// - url: URL to download the resource from. + /// - to: Path where the file should be downloaded. + /// - Throws: An error if the dowload fails. + func download(url: URL, to: AbsolutePath) throws { + let semaphore = DispatchSemaphore(value: 0) + var clientError: HTTPClientError? + + session.downloadTask(with: url) { downloadURL, _, error in + defer { semaphore.signal() } + if let error = error { + clientError = HTTPClientError.clientError(url, error) + } else if let downloadURL = downloadURL { + let from = AbsolutePath(downloadURL.path) + do { + try FileHandler.shared.copy(from: from, to: to) + } catch { + clientError = HTTPClientError.copyFileError(to, error) + } + } else { + clientError = .missingResource(url) + } + }.resume() + semaphore.wait() + if let clientError = clientError { + throw clientError + } + } +} diff --git a/Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift b/Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift new file mode 100644 index 000000000..41a39352f --- /dev/null +++ b/Tests/TuistEnvKitTests/HTTP/HTTPClientTests.swift @@ -0,0 +1,28 @@ +import Foundation +import TuistCore +import XCTest + +@testable import TuistCoreTesting +@testable import TuistEnvKit + +final class HTTPClientErrorTests: XCTestCase { + func test_type() { + // Given + let error = NSError.test() + let url = URL.test() + + // Then + XCTAssertEqual(HTTPClientError.clientError(url, error).type, .abort) + XCTAssertEqual(HTTPClientError.noData(url).type, .abort) + } + + func test_description() { + // Given + let error = NSError.test() + let url = URL.test() + + // Then + XCTAssertEqual(HTTPClientError.clientError(url, error).description, "The request to \(url.absoluteString) errored with: \(error.localizedDescription)") + XCTAssertEqual(HTTPClientError.noData(url).description, "The request to \(url.absoluteString) returned no data") + } +} diff --git a/Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift b/Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift new file mode 100644 index 000000000..38b7c2605 --- /dev/null +++ b/Tests/TuistEnvKitTests/HTTP/Mocks/MockHTTPClient.swift @@ -0,0 +1,47 @@ +import Basic +import Foundation +import TuistCore +import XCTest + +@testable import TuistEnvKit + +final class MockHTTPClient: HTTPClienting { + fileprivate var readStubs: [URL: Result] = [:] + fileprivate var downloadStubs: [URL: Result] = [:] + + func succeedRead(url: URL, response: Data) { + readStubs[url] = .success(response) + } + + func failRead(url: URL, error: Error) { + readStubs[url] = .failure(error) + } + + func read(url: URL) throws -> Data { + if let result = readStubs[url] { + switch result { + case let .failure(error): throw error + case let .success(data): return data + } + } else { + XCTFail("Read request to non-stubbed URL \(url)") + return Data() + } + } + + func download(url: URL, to: AbsolutePath) throws { + if let result = downloadStubs[url] { + switch result { + case let .failure(error): throw error + case let .success(from): + do { + try FileHandler.shared.copy(from: from, to: to) + } catch { + XCTFail("Error copying stubbed download to \(to.pathString)") + } + } + } else { + XCTFail("Download request to non-stubbed URL \(url)") + } + } +} diff --git a/script/install b/script/install index 93a44b494..0e4ccd79f 100755 --- a/script/install +++ b/script/install @@ -1,15 +1,6 @@ #!/bin/bash -URL=$( curl -s "https://api.github.com/repos/tuist/tuist/releases/latest" \ - | jq -r '.assets[] | select(.name=="tuistenv.zip") | .browser_download_url' ) -if [ "$URL" != "" ]; then - echo "Downloading Tuist" -else - echo "Couldn't find tuistenv on the latest release" - exit 1 -fi - -curl -LSs --output /tmp/tuistenv.zip "$URL" +curl -LSs --output /tmp/tuistenv.zip https://storage.googleapis.com/tuist-releases/latest/tuistenv.zip echo "Installing Tuist" unzip -o /tmp/tuistenv.zip -d /tmp/tuistenv > /dev/null @@ -20,4 +11,5 @@ chmod +x /usr/local/bin/tuist rm -rf /tmp/tuistenv rm /tmp/tuistenv.zip -echo "Tuist installed. Try running 'tuist'" \ No newline at end of file +echo "Tuist installed. Try running 'tuist'" +echo "Check out the documentation at https://docs.tuist.io" \ No newline at end of file diff --git a/script/uninstall b/script/uninstall index 54f7684df..b9b48e45a 100755 --- a/script/uninstall +++ b/script/uninstall @@ -1,2 +1,5 @@ #!/bin/bash -rm -rf /usr/local/bin/tuist \ No newline at end of file +rm -rf /usr/local/bin/tuist +rm -rf ~/.tuist + +echo "Tuist uninstalled" \ No newline at end of file