Basic project creation (#3)
This PR contains the work done to create a new *Hummingbird* project with very basic configuration from the _colibri_ executable, just like the project you could create with the [Hummingbird template](https://github.com/hummingbird-project/template) project in Github. Reviewed-on: #3 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
enum File: String {
|
||||
case app = "App"
|
||||
case appArguments = "AppArguments"
|
||||
case appBuilder = "AppBuilder"
|
||||
case appOptions = "AppOptions"
|
||||
case appTests = "AppTests"
|
||||
case dockerFile = "DockerFile"
|
||||
case dockerIgnore = "DockerIgnore"
|
||||
case environment = "Environment"
|
||||
case gitIgnore = "GitIgnore"
|
||||
case license = "License"
|
||||
case loggerLevel = "LoggerLevel"
|
||||
case package = "Package"
|
||||
case readme = "Readme"
|
||||
case testArguments = "TestArguments"
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
extension File {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
var fileName: String {
|
||||
switch self {
|
||||
case .app: "App.swift"
|
||||
case .appArguments: "AppArguments.swift"
|
||||
case .appBuilder: "AppBuilder.swift"
|
||||
case .appOptions: "AppOptions.swift"
|
||||
case .appTests: "AppTests.swift"
|
||||
case .dockerFile: "Dockerfile"
|
||||
case .dockerIgnore: ".dockerignore"
|
||||
case .environment: "Environment+Properties.swift"
|
||||
case .gitIgnore: ".gitignore"
|
||||
case .license: "LICENSE"
|
||||
case .loggerLevel: "LoggerLevel+Conformances.swift"
|
||||
case .readme: "README.md"
|
||||
case .package: "Package.swift"
|
||||
case .testArguments: "TestArguments.swift"
|
||||
}
|
||||
}
|
||||
|
||||
var filePath: String {
|
||||
folder.path + fileName
|
||||
}
|
||||
|
||||
var folder: Folder {
|
||||
switch self {
|
||||
case .app, .appOptions: .app
|
||||
case .appArguments, .appBuilder: .libraryPublic
|
||||
case .appTests: .testCasesPublic
|
||||
case .environment, .loggerLevel: .libraryInternal
|
||||
case .testArguments: .testHelpers
|
||||
default: .root
|
||||
}
|
||||
}
|
||||
|
||||
var resourcePath: String {
|
||||
let basePath = "Resources/Files/Sources"
|
||||
|
||||
return switch self {
|
||||
case .app, .appOptions: "\(basePath)/App"
|
||||
case .appArguments, .appBuilder, .environment, .loggerLevel: "\(basePath)/Library"
|
||||
case .appTests, .testArguments: "\(basePath)/Test"
|
||||
default: basePath
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - CaseIterable
|
||||
|
||||
extension File: CaseIterable {}
|
||||
@@ -0,0 +1,48 @@
|
||||
enum Folder {
|
||||
case app
|
||||
case libraryPublic
|
||||
case libraryInternal
|
||||
case root
|
||||
case testCasesPublic
|
||||
case testCasesInternal
|
||||
case testHelpers
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
extension Folder {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .app: "App/Sources/"
|
||||
case .libraryPublic: "Library/Sources/Public/"
|
||||
case .libraryInternal: "Library/Sources/Internal/"
|
||||
case .root: ""
|
||||
case .testCasesPublic: "Test/Sources/Cases/Public/"
|
||||
case .testCasesInternal: "Test/Sources/Cases/Internal/"
|
||||
case .testHelpers: "Test/Sources/Helpers/"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - CaseIterable
|
||||
|
||||
extension Folder: CaseIterable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
static var allCases: [Folder] {[
|
||||
.app,
|
||||
.libraryPublic,
|
||||
.libraryInternal,
|
||||
.testCasesPublic,
|
||||
.testCasesInternal,
|
||||
.testHelpers
|
||||
]}
|
||||
|
||||
static var allCasesWithRoot: [Folder] { [.root] + Folder.allCases }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Bundleable
|
||||
|
||||
extension Bundle: Bundleable {}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
|
||||
extension Pipe {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
var availableData: AsyncAvailableData { .init(self) }
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AsyncAvailableData
|
||||
|
||||
extension Pipe {
|
||||
struct AsyncAvailableData {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let pipe: Pipe
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(_ pipe: Pipe) {
|
||||
self.pipe = pipe
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func append() async -> Data {
|
||||
var data = Data()
|
||||
|
||||
for await availableData in self {
|
||||
data.append(availableData)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AsyncSequence
|
||||
|
||||
extension Pipe.AsyncAvailableData: AsyncSequence {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias AsyncIterator = AsyncStream<Data>.Iterator
|
||||
typealias Element = Data
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func makeAsyncIterator() -> AsyncIterator {
|
||||
AsyncStream { continuation in
|
||||
pipe.fileHandleForReading.readabilityHandler = { @Sendable handler in
|
||||
let data = handler.availableData
|
||||
|
||||
guard !data.isEmpty else {
|
||||
continuation.finish()
|
||||
return
|
||||
}
|
||||
|
||||
continuation.yield(data)
|
||||
}
|
||||
|
||||
continuation.onTermination = { _ in
|
||||
pipe.fileHandleForReading.readabilityHandler = nil
|
||||
}
|
||||
}
|
||||
.makeAsyncIterator()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Processable
|
||||
|
||||
extension Process: Processable {}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(at filePath: String) {
|
||||
if #available(macOS 13.0, *) {
|
||||
self = URL(filePath: filePath)
|
||||
} else {
|
||||
self = URL(fileURLWithPath: filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
var pathString: String {
|
||||
if #available(macOS 13.0, *) {
|
||||
path(percentEncoded: true)
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func appendingPath(_ path: String) -> URL {
|
||||
if #available(macOS 13.0, *) {
|
||||
appending(path: path)
|
||||
} else {
|
||||
appendingPathComponent(path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
protocol Processable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var arguments: [String]? { get set }
|
||||
var executableURL: URL? { get set }
|
||||
var standardError: Any? { get set }
|
||||
var standardOutput: Any? { get set }
|
||||
var terminationHandler: (@Sendable (Process) -> Void)? { get set }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func run() throws
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
|
||||
struct RunProcessTask {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias Output = String
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private var process: Processable
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(process: Processable) {
|
||||
self.process = process
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
@discardableResult
|
||||
mutating func callAsFunction(
|
||||
path: String, arguments: [String] = []
|
||||
) async throws (RunProcessError) -> Output {
|
||||
process.executableURL = URL(at: path)
|
||||
process.arguments = arguments
|
||||
|
||||
let pipeError = Pipe()
|
||||
let pipeOutput = Pipe()
|
||||
|
||||
process.standardError = pipeError
|
||||
process.standardOutput = pipeOutput
|
||||
|
||||
async let streamOutput = pipeOutput.availableData.append()
|
||||
async let streamError = pipeError.availableData.append()
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
|
||||
let dataOutput = await streamOutput
|
||||
let dataError = await streamError
|
||||
|
||||
guard dataError.isEmpty else {
|
||||
guard let errorOutput = String(data: dataError, encoding: .utf8) else {
|
||||
throw RunProcessError.unexpected
|
||||
}
|
||||
|
||||
throw RunProcessError.output(errorOutput)
|
||||
}
|
||||
|
||||
guard let output = String(data: dataOutput, encoding: .utf8) else {
|
||||
throw RunProcessError.unexpected
|
||||
}
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
process.terminationHandler = { _ in
|
||||
continuation.resume(returning: output)
|
||||
}
|
||||
}
|
||||
} catch let error as RunProcessError {
|
||||
throw error
|
||||
} catch {
|
||||
throw RunProcessError.captured(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum RunProcessError: Error, Equatable {
|
||||
case captured(_ output: String)
|
||||
case output(_ output: String)
|
||||
case unexpected
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
public protocol Bundleable {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func url(forResource name: String?, withExtension ext: String?, subdirectory subpath: String?) -> URL?
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
public protocol FileServicing {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
var currentFolder: URL { get async }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func copyFile(from source: URL, to destination: URL) async throws (FileServiceError)
|
||||
func createFolder(at location: URL) async throws (FileServiceError)
|
||||
func deleteItem(at location: URL) async throws (FileServiceError)
|
||||
func isItemExists(at location: URL) async throws (FileServiceError) -> Bool
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum FileServiceError: Error, Equatable {
|
||||
case folderNotCreated
|
||||
case itemAlreadyExists
|
||||
case itemEmptyData
|
||||
case itemNotCopied
|
||||
case itemNotDeleted
|
||||
case itemNotExists
|
||||
case itemNotFileURL
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import Foundation
|
||||
|
||||
public struct FileService: FileServicing {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let fileManager: FileManager
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
public init(fileManager: FileManager = .default) {
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
public var currentFolder: URL {
|
||||
get async {
|
||||
.init(at: fileManager.currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) {
|
||||
guard try await !isItemExists(at: destination) else {
|
||||
throw FileServiceError.itemAlreadyExists
|
||||
}
|
||||
|
||||
var itemData: Data?
|
||||
|
||||
do {
|
||||
itemData = try Data(contentsOf: source)
|
||||
} catch {
|
||||
throw FileServiceError.itemEmptyData
|
||||
}
|
||||
|
||||
do {
|
||||
try itemData?.write(to: destination, options: .atomic)
|
||||
} catch {
|
||||
throw FileServiceError.itemNotCopied
|
||||
}
|
||||
}
|
||||
|
||||
public func createFolder(at location: URL) async throws (FileServiceError) {
|
||||
guard try await !isItemExists(at: location) else {
|
||||
throw FileServiceError.itemAlreadyExists
|
||||
}
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(at: location, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
throw FileServiceError.folderNotCreated
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteItem(at location: URL) async throws (FileServiceError) {
|
||||
guard try await isItemExists(at: location) else {
|
||||
throw FileServiceError.itemNotExists
|
||||
}
|
||||
|
||||
do {
|
||||
try fileManager.removeItem(at: location)
|
||||
} catch {
|
||||
throw FileServiceError.itemNotDeleted
|
||||
}
|
||||
}
|
||||
|
||||
public func isItemExists(at location: URL) async throws (FileServiceError) -> Bool {
|
||||
guard location.isFileURL else {
|
||||
throw FileServiceError.itemNotFileURL
|
||||
}
|
||||
|
||||
let filePath = location.pathString
|
||||
|
||||
return fileManager.fileExists(atPath: filePath)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
|
||||
public struct CopyFilesTask {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let bundle: Bundleable
|
||||
private let fileService: FileServicing
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
public init(
|
||||
bundle: Bundleable? = nil,
|
||||
fileService: FileServicing
|
||||
) {
|
||||
self.bundle = bundle ?? Bundle.module
|
||||
self.fileService = fileService
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func callAsFunction(to rootFolder: URL) async throws (FileServiceError) {
|
||||
for file in File.allCases {
|
||||
guard let source = bundle.url(
|
||||
forResource: file.rawValue,
|
||||
withExtension: nil,
|
||||
subdirectory: file.resourcePath
|
||||
) else {
|
||||
assertionFailure("URL should have been initialized.")
|
||||
return
|
||||
}
|
||||
|
||||
let destination = rootFolder.appendingPath(file.filePath)
|
||||
|
||||
try await fileService.copyFile(from: source, to: destination)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
public struct CreateFoldersTask {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let fileService: FileServicing
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
public init(fileService: FileServicing) {
|
||||
self.fileService = fileService
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func callAsFunction(at rootFolder: URL) async throws {
|
||||
let folders = Folder.allCases.map { rootFolder.appendingPath($0.path) }
|
||||
|
||||
for folder in folders {
|
||||
try await fileService.createFolder(at: folder)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
public struct CreateRootFolderTask {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let fileService: FileServicing
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
public init(fileService: FileServicing) {
|
||||
self.fileService = fileService
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func callAsFunction(name: String, at location: URL? = nil) async throws -> URL {
|
||||
guard !name.isEmpty else {
|
||||
throw CreateRootFolderError.nameIsEmpty
|
||||
}
|
||||
|
||||
let rootFolder = if let location {
|
||||
location
|
||||
} else {
|
||||
await fileService.currentFolder
|
||||
}
|
||||
|
||||
let newFolder = rootFolder.appendingPath(name)
|
||||
|
||||
try await fileService.createFolder(at: newFolder)
|
||||
|
||||
return newFolder
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
public enum CreateRootFolderError: Error {
|
||||
case nameIsEmpty
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
|
||||
public struct InitGitInFolderTask {
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
public init() {}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func callAsFunction(at rootFolder: URL) async throws (RunProcessError) {
|
||||
let pathCommand = "/usr/bin/git"
|
||||
let pathFolder = rootFolder.pathString
|
||||
|
||||
var gitInit = RunProcessTask(process: Process())
|
||||
var gitAdd = RunProcessTask(process: Process())
|
||||
var gitCommit = RunProcessTask(process: Process())
|
||||
|
||||
try await gitInit(path: pathCommand, arguments: ["init", pathFolder])
|
||||
try await gitAdd(path: pathCommand, arguments: ["-C", pathFolder, "add", "."])
|
||||
try await gitCommit(path: pathCommand, arguments: ["-C", pathFolder, "commit", "-m", "Initial commit"])
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user