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
This commit is contained in:
Pedro Piñera Buendía 2019-10-08 15:14:31 +02:00 committed by GitHub
parent 8cb597196c
commit ed54cfebcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 224 additions and 13 deletions

View File

@ -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

View File

@ -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 🌀

View File

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

View File

@ -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/"
}
}

View File

@ -0,0 +1,7 @@
import Foundation
public extension URL {
static func test() -> URL {
return URL(string: "https://test.tuist.io")!
}
}

View File

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

View File

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

View File

@ -0,0 +1,47 @@
import Basic
import Foundation
import TuistCore
import XCTest
@testable import TuistEnvKit
final class MockHTTPClient: HTTPClienting {
fileprivate var readStubs: [URL: Result<Data, Error>] = [:]
fileprivate var downloadStubs: [URL: Result<AbsolutePath, Error>] = [:]
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)")
}
}
}

View File

@ -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'"
echo "Tuist installed. Try running 'tuist'"
echo "Check out the documentation at https://docs.tuist.io"

View File

@ -1,2 +1,5 @@
#!/bin/bash
rm -rf /usr/local/bin/tuist
rm -rf /usr/local/bin/tuist
rm -rf ~/.tuist
echo "Tuist uninstalled"