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:
parent
8cb597196c
commit
ed54cfebcd
|
@ -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
|
||||
|
|
|
@ -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 🌀
|
||||
|
|
6
Rakefile
6
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)
|
||||
|
|
|
@ -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/"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
public extension URL {
|
||||
static func test() -> URL {
|
||||
return URL(string: "https://test.tuist.io")!
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -1,2 +1,5 @@
|
|||
#!/bin/bash
|
||||
rm -rf /usr/local/bin/tuist
|
||||
rm -rf /usr/local/bin/tuist
|
||||
rm -rf ~/.tuist
|
||||
|
||||
echo "Tuist uninstalled"
|
Loading…
Reference in New Issue