From a3420423d66243e3e09e1158d8ca152c46c5737c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 7 Jan 2025 01:41:10 +0100 Subject: [PATCH 01/80] Removed unnecessary comments from the Package file. --- Package.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 80334c3..7c0e04b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,14 +1,10 @@ // swift-tools-version: 6.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Colibri", targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .executableTarget( - name: "Colibri"), + .executableTarget(name: "Colibri"), ] ) -- 2.47.1 From 98dca62dcad48231bf6f5747d2c0811855863479 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 9 Jan 2025 09:24:55 +0100 Subject: [PATCH 02/80] Added the ArgumentParser package dependency to the package, and plugged it in into the executable target. --- Package.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7c0e04b..d632c08 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,18 @@ import PackageDescription let package = Package( name: "Colibri", + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", + from: "1.0.0") + ], targets: [ - .executableTarget(name: "Colibri"), + .executableTarget( + name: "Colibri", + dependencies: [ + .product(name: "ArgumentParser", + package: "swift-argument-parser") + ], + path: "Sources" + ), ] ) -- 2.47.1 From 3e8e321c73978865939b72805fef1aebfc8edc47 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 9 Jan 2025 09:56:47 +0100 Subject: [PATCH 03/80] Created the Colibri command. --- Sources/Colibri.swift | 12 ++++++++++++ Sources/main.swift | 4 ---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 Sources/Colibri.swift delete mode 100644 Sources/main.swift diff --git a/Sources/Colibri.swift b/Sources/Colibri.swift new file mode 100644 index 0000000..a9a5b9f --- /dev/null +++ b/Sources/Colibri.swift @@ -0,0 +1,12 @@ +import ArgumentParser + +@main +struct Colibri: AsyncParsableCommand { + + // MARK: Functions + + func run() async throws { + // ... + } + +} diff --git a/Sources/main.swift b/Sources/main.swift deleted file mode 100644 index 44e20d5..0000000 --- a/Sources/main.swift +++ /dev/null @@ -1,4 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - -print("Hello, world!") -- 2.47.1 From ecbec1f4c8cf41b92bf207cd4a6ed54cd54b4854 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 00:06:58 +0100 Subject: [PATCH 04/80] Created the library and test targets as well as the executable and library products for the Package file. --- Package.swift | 35 +++++++++++++++++++++---- Sources/{ => Executable}/Colibri.swift | 1 + Sources/Library/ColibriLibrary.swift | 1 + Tests/Library/ColibriLibraryTests.swift | 3 +++ 4 files changed, 35 insertions(+), 5 deletions(-) rename Sources/{ => Executable}/Colibri.swift (87%) create mode 100644 Sources/Library/ColibriLibrary.swift create mode 100644 Tests/Library/ColibriLibraryTests.swift diff --git a/Package.swift b/Package.swift index d632c08..5026c61 100644 --- a/Package.swift +++ b/Package.swift @@ -4,18 +4,43 @@ import PackageDescription let package = Package( name: "Colibri", + products: [ + .executable( + name: "colibri", + targets: ["Colibri"] + ), + .library( + name: "ColibriLibrary", + targets: ["ColibriLibrary"] + ) + ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", - from: "1.0.0") + .package( + url: "https://github.com/apple/swift-argument-parser", + from: "1.0.0" + ) ], targets: [ .executableTarget( name: "Colibri", dependencies: [ - .product(name: "ArgumentParser", - package: "swift-argument-parser") + .product( + name: "ArgumentParser", + package: "swift-argument-parser" + ), + .target(name: "ColibriLibrary") ], - path: "Sources" + path: "Sources/Executable" ), + .target( + name: "ColibriLibrary", + dependencies: [], + path: "Sources/Library" + ), + .testTarget( + name: "ColibriTests", + dependencies: ["ColibriLibrary"], + path: "Tests/Library" + ) ] ) diff --git a/Sources/Colibri.swift b/Sources/Executable/Colibri.swift similarity index 87% rename from Sources/Colibri.swift rename to Sources/Executable/Colibri.swift index a9a5b9f..02690d1 100644 --- a/Sources/Colibri.swift +++ b/Sources/Executable/Colibri.swift @@ -1,4 +1,5 @@ import ArgumentParser +import ColibriLibrary @main struct Colibri: AsyncParsableCommand { diff --git a/Sources/Library/ColibriLibrary.swift b/Sources/Library/ColibriLibrary.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Sources/Library/ColibriLibrary.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/Library/ColibriLibraryTests.swift b/Tests/Library/ColibriLibraryTests.swift new file mode 100644 index 0000000..5cfd2da --- /dev/null +++ b/Tests/Library/ColibriLibraryTests.swift @@ -0,0 +1,3 @@ +import Testing + +struct ColibriLibraryTests {} -- 2.47.1 From 6938b358e100d7cd9da162827aa5105d686b8998 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 01:26:26 +0100 Subject: [PATCH 05/80] Implemented the "currentFolder" property for the FileService service in the Library target. --- Sources/Library/Protocols/FileServicing.swift | 9 +++++++ Sources/Library/Services/FileService.swift | 27 +++++++++++++++++++ Tests/Library/Services/FileServiceTests.swift | 22 +++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 Sources/Library/Protocols/FileServicing.swift create mode 100644 Sources/Library/Services/FileService.swift create mode 100644 Tests/Library/Services/FileServiceTests.swift diff --git a/Sources/Library/Protocols/FileServicing.swift b/Sources/Library/Protocols/FileServicing.swift new file mode 100644 index 0000000..55dc203 --- /dev/null +++ b/Sources/Library/Protocols/FileServicing.swift @@ -0,0 +1,9 @@ +import Foundation + +protocol FileServicing { + + // MARK: Properties + + var currentFolder: URL { get async } + +} diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift new file mode 100644 index 0000000..a81b2ce --- /dev/null +++ b/Sources/Library/Services/FileService.swift @@ -0,0 +1,27 @@ +import Foundation + +struct FileService: FileServicing { + + // MARK: Properties + + private let fileManager: FileManager + + // MARK: Initialisers + + init(fileManager: FileManager = .default) { + self.fileManager = fileManager + } + + // MARK: Computed + + var currentFolder: URL { + get async { + if #available(macOS 13.0, *) { + .init(filePath: fileManager.currentDirectoryPath) + } else { + .init(fileURLWithPath: fileManager.currentDirectoryPath) + } + } + } + +} diff --git a/Tests/Library/Services/FileServiceTests.swift b/Tests/Library/Services/FileServiceTests.swift new file mode 100644 index 0000000..01878a2 --- /dev/null +++ b/Tests/Library/Services/FileServiceTests.swift @@ -0,0 +1,22 @@ +import Testing + +@testable import ColibriLibrary + +struct FileServiceTests { + + // MARK: Properties tests + + @Test("Test the file service provides a current folder URL") + func currentFolder() async { + // GIVEN + let service = FileService() + + // WHEN + let url = await service.currentFolder + + // THEN + #expect(url.path() == "/private/tmp") + #expect(url.isFileURL == true) + } + +} -- 2.47.1 From 2128c0cde29d10b1cab33ff4c6a1df10c8435976 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 01:35:51 +0100 Subject: [PATCH 06/80] Removed boilerplate files from the Library and Tests targets. --- Sources/Library/ColibriLibrary.swift | 1 - Tests/Library/ColibriLibraryTests.swift | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 Sources/Library/ColibriLibrary.swift delete mode 100644 Tests/Library/ColibriLibraryTests.swift diff --git a/Sources/Library/ColibriLibrary.swift b/Sources/Library/ColibriLibrary.swift deleted file mode 100644 index fecc4ab..0000000 --- a/Sources/Library/ColibriLibrary.swift +++ /dev/null @@ -1 +0,0 @@ -import Foundation diff --git a/Tests/Library/ColibriLibraryTests.swift b/Tests/Library/ColibriLibraryTests.swift deleted file mode 100644 index 5cfd2da..0000000 --- a/Tests/Library/ColibriLibraryTests.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Testing - -struct ColibriLibraryTests {} -- 2.47.1 From 739fe0c8def2fdb09a6e17d9098ace40f2349668 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 03:19:59 +0100 Subject: [PATCH 07/80] Implemented the "init(at: )" initialiser function for the URL+Inits extension in the Library target. --- Sources/Library/Extensions/URL+Inits.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Sources/Library/Extensions/URL+Inits.swift diff --git a/Sources/Library/Extensions/URL+Inits.swift b/Sources/Library/Extensions/URL+Inits.swift new file mode 100644 index 0000000..7c31114 --- /dev/null +++ b/Sources/Library/Extensions/URL+Inits.swift @@ -0,0 +1,15 @@ +import Foundation + +extension URL { + + // MARK: Initialisers + + init(at filePath: String) { + if #available(macOS 13.0, *) { + self = URL(filePath: filePath) + } else { + self = URL(fileURLWithPath: filePath) + } + } + +} -- 2.47.1 From 7d0ad3461ad117f92c260c50fc96c56e6ad90576 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 03:24:22 +0100 Subject: [PATCH 08/80] Implemented the "exists(at: )" function for the FileService service in the module target. --- Sources/Library/Protocols/FileServicing.swift | 18 +++++- Sources/Library/Services/FileService.swift | 38 ++++++++++--- Tests/Library/Services/FileServiceTests.swift | 57 +++++++++++++++++-- 3 files changed, 98 insertions(+), 15 deletions(-) diff --git a/Sources/Library/Protocols/FileServicing.swift b/Sources/Library/Protocols/FileServicing.swift index 55dc203..cff10db 100644 --- a/Sources/Library/Protocols/FileServicing.swift +++ b/Sources/Library/Protocols/FileServicing.swift @@ -1,9 +1,23 @@ import Foundation -protocol FileServicing { +public protocol FileServicing { - // MARK: Properties + // MARK: Computed var currentFolder: URL { get async } + // MARK: Functions + + func exists(at url: URL) async throws (FileServiceError) -> Bool + +} + +// MARK: - Errors + +public enum FileServiceError: Error, Equatable { + case folderNotCreated + case folderNotDeleted + case urlAlreadyExists + case urlNotExists + case urlNotFileURL } diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift index a81b2ce..4c164e7 100644 --- a/Sources/Library/Services/FileService.swift +++ b/Sources/Library/Services/FileService.swift @@ -1,6 +1,6 @@ import Foundation -struct FileService: FileServicing { +public struct FileService: FileServicing { // MARK: Properties @@ -8,19 +8,41 @@ struct FileService: FileServicing { // MARK: Initialisers - init(fileManager: FileManager = .default) { + public init(fileManager: FileManager = .default) { self.fileManager = fileManager } // MARK: Computed - var currentFolder: URL { + public var currentFolder: URL { get async { - if #available(macOS 13.0, *) { - .init(filePath: fileManager.currentDirectoryPath) - } else { - .init(fileURLWithPath: fileManager.currentDirectoryPath) - } + .init(at: fileManager.currentDirectoryPath) + } + } + + public func exists(at url: URL) async throws (FileServiceError) -> Bool { + guard url.isFileURL else { + throw FileServiceError.urlNotFileURL + } + + let filePath = getPath(for: url) + + return fileManager.fileExists(atPath: filePath) + } + +} + +// MARK: - Helpers + +private extension FileService { + + // MARK: Functions + + func getPath(for url: URL) -> String { + if #available(macOS 13.0, *) { + return url.path() + } else { + return url.path } } diff --git a/Tests/Library/Services/FileServiceTests.swift b/Tests/Library/Services/FileServiceTests.swift index 01878a2..7dde431 100644 --- a/Tests/Library/Services/FileServiceTests.swift +++ b/Tests/Library/Services/FileServiceTests.swift @@ -1,22 +1,69 @@ +import Foundation import Testing @testable import ColibriLibrary struct FileServiceTests { + + // MARK: Properties + + private let service: FileServicing + + // MARK: Initialisers + + init() { + self.service = FileService() + } // MARK: Properties tests - @Test("Test the file service provides a current folder URL") - func currentFolder() async { + @Test func currentFolder() async { // GIVEN - let service = FileService() - // WHEN let url = await service.currentFolder // THEN - #expect(url.path() == "/private/tmp") + #expect(url == .someExistingFolder) #expect(url.isFileURL == true) } + // GIVEN + // WHEN + // THEN + } + + @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNonExistingFolder, .someNonExistingFile], + [true, true, false, false])) + func exists( + with url: URL, + expects outcome: Bool + ) async throws { + // GIVEN + // WHEN + let result = try await service.exists(at: url) + + // THEN + #expect(result == outcome) + } + + @Test func existsThrows() async throws { + // GIVEN + let url = URL.someRandomURL + + // WHEN + // THEN + await #expect(throws: FileServiceError.urlNotFileURL) { + try await service.exists(at: url) + } + } } + +// MARK: - URL+Constants + +private extension URL { + static let someExistingFolder = URL(at: "/private/tmp") + static let someExistingFile = URL(at: "/etc/null") + static let someNonExistingFolder = URL(at: "/some/random/folder") + static let someNonExistingFile = URL(at: "/some/random/file.ext") + static let someRandomURL = URL(string: "https://some.random.url")! +} -- 2.47.1 From 58151a4e5a800da069165f2bc32c053f1100a0d5 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 04:44:02 +0100 Subject: [PATCH 09/80] Implemented the "delete(at: )" function for the FileService service in the module target. --- Sources/Library/Protocols/FileServicing.swift | 3 +- Sources/Library/Services/FileService.swift | 14 +++++++++ Tests/Library/Services/FileServiceTests.swift | 29 ++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Sources/Library/Protocols/FileServicing.swift b/Sources/Library/Protocols/FileServicing.swift index cff10db..9f8d20a 100644 --- a/Sources/Library/Protocols/FileServicing.swift +++ b/Sources/Library/Protocols/FileServicing.swift @@ -8,6 +8,7 @@ public protocol FileServicing { // MARK: Functions + func delete(at url: URL) async throws (FileServiceError) func exists(at url: URL) async throws (FileServiceError) -> Bool } @@ -16,8 +17,8 @@ public protocol FileServicing { public enum FileServiceError: Error, Equatable { case folderNotCreated - case folderNotDeleted case urlAlreadyExists + case urlNotDeleted case urlNotExists case urlNotFileURL } diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift index 4c164e7..d1d6397 100644 --- a/Sources/Library/Services/FileService.swift +++ b/Sources/Library/Services/FileService.swift @@ -20,6 +20,20 @@ public struct FileService: FileServicing { } } + // MARK: Functions + + public func delete(at url: URL) async throws (FileServiceError) { + guard try await exists(at: url) else { + throw FileServiceError.urlNotExists + } + + do { + try fileManager.removeItem(at: url) + } catch { + throw FileServiceError.urlNotDeleted + } + } + public func exists(at url: URL) async throws (FileServiceError) -> Bool { guard url.isFileURL else { throw FileServiceError.urlNotFileURL diff --git a/Tests/Library/Services/FileServiceTests.swift b/Tests/Library/Services/FileServiceTests.swift index 7dde431..9193264 100644 --- a/Tests/Library/Services/FileServiceTests.swift +++ b/Tests/Library/Services/FileServiceTests.swift @@ -26,9 +26,34 @@ struct FileServiceTests { #expect(url == .someExistingFolder) #expect(url.isFileURL == true) } + @Test(arguments: [URL.someNewFolder, .someNewFile]) + func delete(with url: URL) async throws { + // GIVEN + if try await !service.exists(at: url) { + try await service.createFolder(at: url) + } + + // WHEN + try await service.delete(at: url) + + // THEN + let result = try await service.exists(at: url) + + #expect(result == false) + } + + @Test(arguments: zip([URL.someNonExistingFolder, .someNonExistingFile, .someRandomURL], + [FileServiceError.urlNotExists, .urlNotExists, .urlNotFileURL])) + func deleteThrows( + with url: URL, + expects error: FileServiceError + ) async throws { // GIVEN // WHEN // THEN + await #expect(throws: error) { + try await service.delete(at: url) + } } @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNonExistingFolder, .someNonExistingFile], @@ -62,7 +87,9 @@ struct FileServiceTests { private extension URL { static let someExistingFolder = URL(at: "/private/tmp") - static let someExistingFile = URL(at: "/etc/null") + static let someExistingFile = URL(at: "/etc/ssh/ssh_config") + static let someNewFolder = URL(at: "/private/tmp/folder") + static let someNewFile = URL(at: "/private/tmp/file.ext") static let someNonExistingFolder = URL(at: "/some/random/folder") static let someNonExistingFile = URL(at: "/some/random/file.ext") static let someRandomURL = URL(string: "https://some.random.url")! -- 2.47.1 From 1d080cce45210644ef0e906cbe5fd8e225b2704a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 05:08:06 +0100 Subject: [PATCH 10/80] Implemented the "createFolder(at: )" function for the FileService service in the module target. --- Sources/Library/Protocols/FileServicing.swift | 1 + Sources/Library/Services/FileService.swift | 15 +++++++ Tests/Library/Services/FileServiceTests.swift | 41 +++++++++++++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Sources/Library/Protocols/FileServicing.swift b/Sources/Library/Protocols/FileServicing.swift index 9f8d20a..5f85840 100644 --- a/Sources/Library/Protocols/FileServicing.swift +++ b/Sources/Library/Protocols/FileServicing.swift @@ -8,6 +8,7 @@ public protocol FileServicing { // MARK: Functions + func createFolder(at url: URL) async throws (FileServiceError) func delete(at url: URL) async throws (FileServiceError) func exists(at url: URL) async throws (FileServiceError) -> Bool diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift index d1d6397..3e91ad2 100644 --- a/Sources/Library/Services/FileService.swift +++ b/Sources/Library/Services/FileService.swift @@ -22,6 +22,21 @@ public struct FileService: FileServicing { // MARK: Functions + public func createFolder(at url: URL) async throws (FileServiceError) { + guard try await !exists(at: url) else { + throw FileServiceError.urlAlreadyExists + } + + do { + try fileManager.createDirectory( + at: url, + withIntermediateDirectories: true + ) + } catch { + throw FileServiceError.folderNotCreated + } + } + public func delete(at url: URL) async throws (FileServiceError) { guard try await exists(at: url) else { throw FileServiceError.urlNotExists diff --git a/Tests/Library/Services/FileServiceTests.swift b/Tests/Library/Services/FileServiceTests.swift index 9193264..8a53c0e 100644 --- a/Tests/Library/Services/FileServiceTests.swift +++ b/Tests/Library/Services/FileServiceTests.swift @@ -26,6 +26,41 @@ struct FileServiceTests { #expect(url == .someExistingFolder) #expect(url.isFileURL == true) } + + // MARK: Functions + + @Test(arguments: [URL.someNewFolder, .someNewFile]) + func createFolder(with url: URL) async throws { + // GIVEN + if try await service.exists(at: url) { + try await service.delete(at: url) + } + + // WHEN + try await service.createFolder(at: url) + + // THEN + let result = try await service.exists(at: url) + + #expect(result == true) + + try await service.delete(at: url) + } + + @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someRandomURL], + [FileServiceError.urlAlreadyExists, .urlAlreadyExists, .urlNotFileURL])) + func createFolderThrows( + with url: URL, + expects error: FileServiceError + ) async throws { + // GIVEN + // WHEN + // THEN + await #expect(throws: error) { + try await service.createFolder(at: url) + } + } + @Test(arguments: [URL.someNewFolder, .someNewFile]) func delete(with url: URL) async throws { // GIVEN @@ -42,7 +77,7 @@ struct FileServiceTests { #expect(result == false) } - @Test(arguments: zip([URL.someNonExistingFolder, .someNonExistingFile, .someRandomURL], + @Test(arguments: zip([URL.someNewFolder, .someNewFile, .someRandomURL], [FileServiceError.urlNotExists, .urlNotExists, .urlNotFileURL])) func deleteThrows( with url: URL, @@ -56,7 +91,7 @@ struct FileServiceTests { } } - @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNonExistingFolder, .someNonExistingFile], + @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNewFolder, .someNewFile], [true, true, false, false])) func exists( with url: URL, @@ -90,7 +125,5 @@ private extension URL { static let someExistingFile = URL(at: "/etc/ssh/ssh_config") static let someNewFolder = URL(at: "/private/tmp/folder") static let someNewFile = URL(at: "/private/tmp/file.ext") - static let someNonExistingFolder = URL(at: "/some/random/folder") - static let someNonExistingFile = URL(at: "/some/random/file.ext") static let someRandomURL = URL(string: "https://some.random.url")! } -- 2.47.1 From 4b900818ec384030d88f4d98f58ac1a234a27bbb Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 11:27:39 +0100 Subject: [PATCH 11/80] Implemented the FileServiceSpy spy in the Tests target. --- .../Helpers/Spies/FileServiceSpy.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Tests/Library/Helpers/Spies/FileServiceSpy.swift diff --git a/Tests/Library/Helpers/Spies/FileServiceSpy.swift b/Tests/Library/Helpers/Spies/FileServiceSpy.swift new file mode 100644 index 0000000..c149c14 --- /dev/null +++ b/Tests/Library/Helpers/Spies/FileServiceSpy.swift @@ -0,0 +1,47 @@ +import Foundation + +@testable import ColibriLibrary + +final class FileServiceSpy { + + // MARK: Properties + + private(set) var isCreateFolderCalled: Bool = false + private(set) var isDeleteCalled: Bool = false + private(set) var isExistsAtCalled: Bool = false + private(set) var urlCalled: URL? + +} + +// MARK: - FileServicing + +extension FileServiceSpy: FileServicing { + var currentFolder: URL { + get async { .someCurrentFolder } + } + + func createFolder(at url: URL) async throws(FileServiceError) { + isCreateFolderCalled = true + urlCalled = url + } + + func delete(at url: URL) async throws(FileServiceError) { + isDeleteCalled = true + urlCalled = url + } + + @discardableResult + func exists(at url: URL) async throws(FileServiceError) -> Bool { + isExistsAtCalled = true + urlCalled = url + + return .random() + } + +} + +// MARK: - URL+Constants + +private extension URL { + static let someCurrentFolder = URL(at: "some/current/folder") +} -- 2.47.1 From a7be1ec0b094ae3073115280f0764f9d857a37f9 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 11:28:27 +0100 Subject: [PATCH 12/80] Implemented the FileServiceMock mock in the Tests target. --- .../Helpers/Mocks/FileServiceMock.swift | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 Tests/Library/Helpers/Mocks/FileServiceMock.swift diff --git a/Tests/Library/Helpers/Mocks/FileServiceMock.swift b/Tests/Library/Helpers/Mocks/FileServiceMock.swift new file mode 100644 index 0000000..fa7b6d7 --- /dev/null +++ b/Tests/Library/Helpers/Mocks/FileServiceMock.swift @@ -0,0 +1,84 @@ +import ColibriLibrary +import Foundation + +struct FileServiceMock { + + // MARK: Properties + + private let action: Action? + private let folder: URL + + private weak var spy: FileServiceSpy? + + // MARK: Initialisers + + init( + currentFolder: URL, + action: Action? = nil, + spy: FileServiceSpy? = nil + ) { + self.action = action + self.folder = currentFolder + self.spy = spy + } + +} + +// MARK: - FileServicing + +extension FileServiceMock: FileServicing { + + // MARK: Computed + + var currentFolder: URL { + get async { folder } + } + + // MARK: Functions + + func createFolder(at url: URL) async throws(FileServiceError) { + switch action { + case .error(let error): + throw error + case let .createFolder(url): + try await spy?.createFolder(at: url) + default: + break + } + } + + func delete(at url: URL) async throws(FileServiceError) { + switch action { + case .error(let error): + throw error + case let .delete(url): + try await spy?.delete(at: url) + default: + break + } + } + + func exists(at url: URL) async throws(FileServiceError) -> Bool { + switch action { + case .error(let error): + throw error + case let .exists(url, exists): + try await spy?.exists(at: url) + return exists + default: + return false + } + } + +} + +// MARK: - Enumerations + +extension FileServiceMock { + enum Action { + case createFolder(URL) + case delete(URL) + case error(FileServiceError) + case exists(URL, Bool) + } +} -- 2.47.1 From 3f98d08a00b3a1afa95faa3724a82c517548f6c6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 11:33:19 +0100 Subject: [PATCH 13/80] Written test cases for the FileServicing conformance using relevant mock and spy in the Tests target. --- .../Cases/Services/FileServiceTests.swift | 166 ++++++++++++++++++ Tests/Library/Services/FileServiceTests.swift | 129 -------------- 2 files changed, 166 insertions(+), 129 deletions(-) create mode 100644 Tests/Library/Cases/Services/FileServiceTests.swift delete mode 100644 Tests/Library/Services/FileServiceTests.swift diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Tests/Library/Cases/Services/FileServiceTests.swift new file mode 100644 index 0000000..092ee9c --- /dev/null +++ b/Tests/Library/Cases/Services/FileServiceTests.swift @@ -0,0 +1,166 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct FileServiceTests { + + // MARK: Properties + + private let spy = FileServiceSpy() + + // MARK: Properties tests + + @Test func currentFolder() async { + // GIVEN + let url: URL = .someCurrentFolder + + let service = FileServiceMock(currentFolder: url) + + // WHEN + let folder = await service.currentFolder + + // THEN + #expect(folder == url) + #expect(folder.isFileURL == true) + } + + // MARK: Functions + + @Test(arguments: [URL.someNewFolder, .someNewFile]) + func createFolder(with url: URL) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .createFolder(url), + spy: spy + ) + + // WHEN + try await service.createFolder(at: url) + + // THEN + #expect(spy.isCreateFolderCalled == true) + #expect(spy.urlCalled == url) + } + + @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someRandomURL], + [FileServiceError.urlAlreadyExists, .urlAlreadyExists, .urlNotFileURL])) + func createFolder( + with url: URL, + throws error: FileServiceError + ) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .error(error), + spy: spy + ) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.createFolder(at: url) + } + + #expect(spy.isCreateFolderCalled == false) + #expect(spy.urlCalled == nil) + } + + @Test(arguments: [URL.someNewFolder, .someNewFile]) + func delete(with url: URL) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .delete(url), + spy: spy + ) + + // WHEN + try await service.delete(at: url) + + // THEN + #expect(spy.isDeleteCalled == true) + #expect(spy.urlCalled == url) + } + + @Test(arguments: zip([URL.someNewFolder, .someNewFile, .someRandomURL], + [FileServiceError.urlNotExists, .urlNotExists, .urlNotFileURL])) + func delete( + with url: URL, + throws error: FileServiceError + ) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .error(error), + spy: spy + ) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.delete(at: url) + } + + #expect(spy.isDeleteCalled == false) + #expect(spy.urlCalled == nil) + } + + @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNewFolder, .someNewFile], + [true, true, false, false])) + func exists( + with url: URL, + expects outcome: Bool + ) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .exists(url, outcome), + spy: spy + ) + + // WHEN + let result = try await service.exists(at: url) + + // THEN + #expect(result == outcome) + + #expect(spy.isExistsAtCalled == true) + #expect(spy.urlCalled == url) + } + + @Test(arguments: zip([URL.someRandomURL], [FileServiceError.urlNotFileURL])) + func exists( + with url: URL, + throws error: FileServiceError + ) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .error(error), + spy: spy + ) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.exists(at: url) + } + + #expect(spy.isExistsAtCalled == false) + #expect(spy.urlCalled == nil) + } + +} + +// MARK: - URL+Constants + +private extension URL { + static let someCurrentFolder = URL(at: "/some/current/folder") + static let someExistingFolder = URL(at: "/some/existing/folder") + static let someExistingFile = URL(at: "/some/existing/file") + static let someNewFolder = URL(at: "/some/new/folder") + static let someNewFile = URL(at: "/some/new/file") + static let someRandomURL = URL(string: "some.random.url")! +} diff --git a/Tests/Library/Services/FileServiceTests.swift b/Tests/Library/Services/FileServiceTests.swift deleted file mode 100644 index 8a53c0e..0000000 --- a/Tests/Library/Services/FileServiceTests.swift +++ /dev/null @@ -1,129 +0,0 @@ -import Foundation -import Testing - -@testable import ColibriLibrary - -struct FileServiceTests { - - // MARK: Properties - - private let service: FileServicing - - // MARK: Initialisers - - init() { - self.service = FileService() - } - - // MARK: Properties tests - - @Test func currentFolder() async { - // GIVEN - // WHEN - let url = await service.currentFolder - - // THEN - #expect(url == .someExistingFolder) - #expect(url.isFileURL == true) - } - - // MARK: Functions - - @Test(arguments: [URL.someNewFolder, .someNewFile]) - func createFolder(with url: URL) async throws { - // GIVEN - if try await service.exists(at: url) { - try await service.delete(at: url) - } - - // WHEN - try await service.createFolder(at: url) - - // THEN - let result = try await service.exists(at: url) - - #expect(result == true) - - try await service.delete(at: url) - } - - @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someRandomURL], - [FileServiceError.urlAlreadyExists, .urlAlreadyExists, .urlNotFileURL])) - func createFolderThrows( - with url: URL, - expects error: FileServiceError - ) async throws { - // GIVEN - // WHEN - // THEN - await #expect(throws: error) { - try await service.createFolder(at: url) - } - } - - @Test(arguments: [URL.someNewFolder, .someNewFile]) - func delete(with url: URL) async throws { - // GIVEN - if try await !service.exists(at: url) { - try await service.createFolder(at: url) - } - - // WHEN - try await service.delete(at: url) - - // THEN - let result = try await service.exists(at: url) - - #expect(result == false) - } - - @Test(arguments: zip([URL.someNewFolder, .someNewFile, .someRandomURL], - [FileServiceError.urlNotExists, .urlNotExists, .urlNotFileURL])) - func deleteThrows( - with url: URL, - expects error: FileServiceError - ) async throws { - // GIVEN - // WHEN - // THEN - await #expect(throws: error) { - try await service.delete(at: url) - } - } - - @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNewFolder, .someNewFile], - [true, true, false, false])) - func exists( - with url: URL, - expects outcome: Bool - ) async throws { - // GIVEN - // WHEN - let result = try await service.exists(at: url) - - // THEN - #expect(result == outcome) - } - - @Test func existsThrows() async throws { - // GIVEN - let url = URL.someRandomURL - - // WHEN - // THEN - await #expect(throws: FileServiceError.urlNotFileURL) { - try await service.exists(at: url) - } - } - -} - -// MARK: - URL+Constants - -private extension URL { - static let someExistingFolder = URL(at: "/private/tmp") - static let someExistingFile = URL(at: "/etc/ssh/ssh_config") - static let someNewFolder = URL(at: "/private/tmp/folder") - static let someNewFile = URL(at: "/private/tmp/file.ext") - static let someRandomURL = URL(string: "https://some.random.url")! -} -- 2.47.1 From af1703e6c165a0db9e3c2181e30b2069c18a9bb2 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 11 Jan 2025 18:40:09 +0100 Subject: [PATCH 14/80] Implemented the CreatedRootFolderTask task in the Library target. --- .../Library/Tasks/CreateRootFolderTask.swift | 38 +++++++++++ .../Cases/Services/FileServiceTests.swift | 14 +--- .../Tasks/CreateRootFolderTaskTests.swift | 64 +++++++++++++++++++ .../Helpers/Extensions/URL+Samples.swift | 16 +++++ .../Helpers/Spies/FileServiceSpy.swift | 6 -- 5 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 Sources/Library/Tasks/CreateRootFolderTask.swift create mode 100644 Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift create mode 100644 Tests/Library/Helpers/Extensions/URL+Samples.swift diff --git a/Sources/Library/Tasks/CreateRootFolderTask.swift b/Sources/Library/Tasks/CreateRootFolderTask.swift new file mode 100644 index 0000000..805b7c2 --- /dev/null +++ b/Sources/Library/Tasks/CreateRootFolderTask.swift @@ -0,0 +1,38 @@ +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 { + let rootFolder = if let location { + location + } else { + await fileService.currentFolder + } + + let newFolder = if #available(macOS 13.0, *) { + rootFolder.appending(path: name) + } else { + rootFolder.appendingPathComponent(name) + } + + try await fileService.createFolder(at: newFolder) + + return newFolder + } + +} diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Tests/Library/Cases/Services/FileServiceTests.swift index 092ee9c..f686fe2 100644 --- a/Tests/Library/Cases/Services/FileServiceTests.swift +++ b/Tests/Library/Cases/Services/FileServiceTests.swift @@ -1,8 +1,7 @@ +import ColibriLibrary import Foundation import Testing -@testable import ColibriLibrary - struct FileServiceTests { // MARK: Properties @@ -153,14 +152,3 @@ struct FileServiceTests { } } - -// MARK: - URL+Constants - -private extension URL { - static let someCurrentFolder = URL(at: "/some/current/folder") - static let someExistingFolder = URL(at: "/some/existing/folder") - static let someExistingFile = URL(at: "/some/existing/file") - static let someNewFolder = URL(at: "/some/new/folder") - static let someNewFile = URL(at: "/some/new/file") - static let someRandomURL = URL(string: "some.random.url")! -} diff --git a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift new file mode 100644 index 0000000..72ce72e --- /dev/null +++ b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift @@ -0,0 +1,64 @@ +import ColibriLibrary +import Foundation +import Testing + +struct CreateRootFolderTaskTests { + + // MARK: Functions tests + + @Test(arguments: [String.someProjectName], [URL.someCurrentProjectFolder, .someNewProjectFolder]) + func task( + name: String, + expects folder: URL + ) async throws { + // GIVEN + let fileService = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .createFolder(folder) + ) + + let task = CreateRootFolderTask(fileService: fileService) + + // WHEN + let result = try await task(name: name, + at: folder == .someNewProjectFolder ? .someNewFolder : nil) + + // THEN + #expect(result == folder) + #expect(result.isFileURL == true) + } + + @Test(arguments: [String.someProjectName], [FileServiceError.urlAlreadyExists]) + func task( + name: String, + throws error: FileServiceError + ) async throws { + // GIVEN + let fileService = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .error(error) + ) + + let task = CreateRootFolderTask(fileService: fileService) + + // WHEN + // THEN + await #expect(throws: error) { + try await task(name: name) + } + } + +} + +// MARK: - String+Constants + +private extension String { + static let someProjectName = "SomeProjectName" +} + +// MARK: - URL+Constants + +private extension URL { + static let someCurrentProjectFolder = URL.someCurrentFolder.appending(component: String.someProjectName) + static let someNewProjectFolder = URL.someNewFolder.appending(component: String.someProjectName) +} diff --git a/Tests/Library/Helpers/Extensions/URL+Samples.swift b/Tests/Library/Helpers/Extensions/URL+Samples.swift new file mode 100644 index 0000000..1d0622a --- /dev/null +++ b/Tests/Library/Helpers/Extensions/URL+Samples.swift @@ -0,0 +1,16 @@ +import Foundation + +@testable import ColibriLibrary + +extension URL { + + // MARK: Constants + + static let someCurrentFolder = URL(at: "/some/current/folder") + static let someExistingFolder = URL(at: "/some/existing/folder") + static let someExistingFile = URL(at: "/some/existing/file") + static let someNewFolder = URL(at: "/some/new/folder") + static let someNewFile = URL(at: "/some/new/file") + static let someRandomURL = URL(string: "some.random.url")! + +} diff --git a/Tests/Library/Helpers/Spies/FileServiceSpy.swift b/Tests/Library/Helpers/Spies/FileServiceSpy.swift index c149c14..6422bc9 100644 --- a/Tests/Library/Helpers/Spies/FileServiceSpy.swift +++ b/Tests/Library/Helpers/Spies/FileServiceSpy.swift @@ -39,9 +39,3 @@ extension FileServiceSpy: FileServicing { } } - -// MARK: - URL+Constants - -private extension URL { - static let someCurrentFolder = URL(at: "some/current/folder") -} -- 2.47.1 From 15d1e22f1c33e6574f382870e159a94cfc7fe36c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 01:33:52 +0100 Subject: [PATCH 15/80] Defined the minimum platform requirements for the Package file. --- Package.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Package.swift b/Package.swift index 5026c61..d8870c1 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,9 @@ import PackageDescription let package = Package( name: "Colibri", + platforms: [ + .macOS(.v10_15) + ], products: [ .executable( name: "colibri", -- 2.47.1 From db1df0ec62bf505d0583dee541d747c18cabd1d7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 01:42:43 +0100 Subject: [PATCH 16/80] Created the basis of the Create command in the executable target. --- Sources/Executable/Colibri.swift | 12 +++--- Sources/Executable/Commands/Create.swift | 41 +++++++++++++++++++ .../Helpers/Extensions/URL+Samples.swift | 2 +- 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 Sources/Executable/Commands/Create.swift diff --git a/Sources/Executable/Colibri.swift b/Sources/Executable/Colibri.swift index 02690d1..a27ae37 100644 --- a/Sources/Executable/Colibri.swift +++ b/Sources/Executable/Colibri.swift @@ -1,13 +1,13 @@ import ArgumentParser -import ColibriLibrary @main struct Colibri: AsyncParsableCommand { - // MARK: Functions - - func run() async throws { - // ... - } + // MARK: Properties + + static let configuration = CommandConfiguration( + abstract: "The utility to manage your Hummingbird projects", + subcommands: [Create.self] + ) } diff --git a/Sources/Executable/Commands/Create.swift b/Sources/Executable/Commands/Create.swift new file mode 100644 index 0000000..d65c947 --- /dev/null +++ b/Sources/Executable/Commands/Create.swift @@ -0,0 +1,41 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri { + struct Create: AsyncParsableCommand { + + // MARK: Properties + + static let configuration = CommandConfiguration( + commandName: "create project", + abstract: "Create a new, tailored Colibri project.", + helpNames: .shortAndLong, + aliases: ["create"] + ) + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + + } + + } +} + +// MARK: - Options + +extension Colibri.Create { + struct Options: ParsableArguments { + + // MARK: Properties + + @Option(name: .shortAndLong) + var name: String + + @Option(name: .shortAndLong) + var location: String? + + } +} diff --git a/Tests/Library/Helpers/Extensions/URL+Samples.swift b/Tests/Library/Helpers/Extensions/URL+Samples.swift index 1d0622a..83c9f6a 100644 --- a/Tests/Library/Helpers/Extensions/URL+Samples.swift +++ b/Tests/Library/Helpers/Extensions/URL+Samples.swift @@ -11,6 +11,6 @@ extension URL { static let someExistingFile = URL(at: "/some/existing/file") static let someNewFolder = URL(at: "/some/new/folder") static let someNewFile = URL(at: "/some/new/file") - static let someRandomURL = URL(string: "some.random.url")! + static let someRandomURL = URL(string: "http://some.random.url")! } -- 2.47.1 From 6bf9c30ad17e33d7cec45af8ffc1dd97949feea9 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 02:16:30 +0100 Subject: [PATCH 17/80] Improved the CreatedRootFolderTask task in the library target to throw error in case the function receives an empty name. --- .../Library/Tasks/CreateRootFolderTask.swift | 10 ++++++ .../Tasks/CreateRootFolderTaskTests.swift | 31 +++++++++++++++++-- .../Helpers/Extensions/URL+Samples.swift | 2 ++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Sources/Library/Tasks/CreateRootFolderTask.swift b/Sources/Library/Tasks/CreateRootFolderTask.swift index 805b7c2..fdee7e0 100644 --- a/Sources/Library/Tasks/CreateRootFolderTask.swift +++ b/Sources/Library/Tasks/CreateRootFolderTask.swift @@ -18,6 +18,10 @@ public struct CreateRootFolderTask { name: String, at location: URL? = nil ) async throws -> URL { + guard !name.isEmpty else { + throw CreateRootFolderError.nameIsEmpty + } + let rootFolder = if let location { location } else { @@ -36,3 +40,9 @@ public struct CreateRootFolderTask { } } + +// MARK: - Errors + +public enum CreateRootFolderError: Error { + case nameIsEmpty +} diff --git a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift index 72ce72e..a3f6795 100644 --- a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift +++ b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift @@ -6,12 +6,19 @@ struct CreateRootFolderTaskTests { // MARK: Functions tests - @Test(arguments: [String.someProjectName], [URL.someCurrentProjectFolder, .someNewProjectFolder]) + @Test(arguments: [String.someProjectName], [URL.someCurrentProjectFolder, .someNewProjectFolder, .someDotProjectFolder, .someTildeProjectFolder]) func task( name: String, expects folder: URL ) async throws { // GIVEN + let location: URL? = switch folder { + case .someNewProjectFolder: .someNewFolder + case .someDotProjectFolder: .someDotFolder + case .someTildeProjectFolder: .someTildeFolder + default: nil + } + let fileService = FileServiceMock( currentFolder: .someCurrentFolder, action: .createFolder(folder) @@ -21,7 +28,7 @@ struct CreateRootFolderTaskTests { // WHEN let result = try await task(name: name, - at: folder == .someNewProjectFolder ? .someNewFolder : nil) + at: location) // THEN #expect(result == folder) @@ -47,12 +54,30 @@ struct CreateRootFolderTaskTests { try await task(name: name) } } + + @Test(arguments: [String.someEmptyName], [CreateRootFolderError.nameIsEmpty]) + func task( + name: String, + throws error: CreateRootFolderError + ) async throws { + // GIVEN + let fileService = FileServiceMock(currentFolder: .someCurrentFolder) + + let task = CreateRootFolderTask(fileService: fileService) + + // WHEN + // THEN + await #expect(throws: error) { + try await task(name: name) + } + } } // MARK: - String+Constants private extension String { + static let someEmptyName = "" static let someProjectName = "SomeProjectName" } @@ -60,5 +85,7 @@ private extension String { private extension URL { static let someCurrentProjectFolder = URL.someCurrentFolder.appending(component: String.someProjectName) + static let someDotProjectFolder = URL.someDotFolder.appending(component: String.someProjectName) static let someNewProjectFolder = URL.someNewFolder.appending(component: String.someProjectName) + static let someTildeProjectFolder = URL.someTildeFolder.appending(component: String.someProjectName) } diff --git a/Tests/Library/Helpers/Extensions/URL+Samples.swift b/Tests/Library/Helpers/Extensions/URL+Samples.swift index 83c9f6a..5869bb5 100644 --- a/Tests/Library/Helpers/Extensions/URL+Samples.swift +++ b/Tests/Library/Helpers/Extensions/URL+Samples.swift @@ -7,10 +7,12 @@ extension URL { // MARK: Constants static let someCurrentFolder = URL(at: "/some/current/folder") + static let someDotFolder = URL(at: ".") static let someExistingFolder = URL(at: "/some/existing/folder") static let someExistingFile = URL(at: "/some/existing/file") static let someNewFolder = URL(at: "/some/new/folder") static let someNewFile = URL(at: "/some/new/file") static let someRandomURL = URL(string: "http://some.random.url")! + static let someTildeFolder = URL(at: "~") } -- 2.47.1 From cbd3789ca7e45e13be4faf9107473b5f3b45d50b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 02:41:17 +0100 Subject: [PATCH 18/80] Implemented the creation of a root folder within the Create command in the executable target. --- Sources/Executable/Colibri.swift | 2 +- Sources/Executable/Commands/Create.swift | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Sources/Executable/Colibri.swift b/Sources/Executable/Colibri.swift index a27ae37..194d287 100644 --- a/Sources/Executable/Colibri.swift +++ b/Sources/Executable/Colibri.swift @@ -6,7 +6,7 @@ struct Colibri: AsyncParsableCommand { // MARK: Properties static let configuration = CommandConfiguration( - abstract: "The utility to manage your Hummingbird projects", + abstract: "The utility to manage your Hummingbird apps", subcommands: [Create.self] ) diff --git a/Sources/Executable/Commands/Create.swift b/Sources/Executable/Commands/Create.swift index d65c947..a8d4373 100644 --- a/Sources/Executable/Commands/Create.swift +++ b/Sources/Executable/Commands/Create.swift @@ -1,5 +1,6 @@ import ArgumentParser import ColibriLibrary +import Foundation extension Colibri { struct Create: AsyncParsableCommand { @@ -7,18 +8,26 @@ extension Colibri { // MARK: Properties static let configuration = CommandConfiguration( - commandName: "create project", - abstract: "Create a new, tailored Colibri project.", + commandName: "create-project", + abstract: "Create a new, tailored Hummingbird app", helpNames: .shortAndLong, aliases: ["create"] ) @OptionGroup var options: Options - + // MARK: Functions mutating func run() async throws { - + let fileService = FileService() + let createRootFolder = CreateRootFolderTask(fileService: fileService) + + let rootFolder = try await createRootFolder( + name: options.name, + at: options.locationURL + ) + + print(rootFolder) } } @@ -36,6 +45,12 @@ extension Colibri.Create { @Option(name: .shortAndLong) var location: String? + + // MARK: Computed + + var locationURL: URL? { + location.flatMap { URL(fileURLWithPath: $0) } + } } } -- 2.47.1 From 1f738fe64403a7546658ff638130b5927811f1a2 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 03:11:03 +0100 Subject: [PATCH 19/80] Created the CreateProjectTask task in the library target. --- Sources/Library/Tasks/CreateProjectTask.swift | 3 +++ .../Library/Cases/Tasks/CreateProjectTaskTests.swift | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 Sources/Library/Tasks/CreateProjectTask.swift create mode 100644 Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift diff --git a/Sources/Library/Tasks/CreateProjectTask.swift b/Sources/Library/Tasks/CreateProjectTask.swift new file mode 100644 index 0000000..c1083f5 --- /dev/null +++ b/Sources/Library/Tasks/CreateProjectTask.swift @@ -0,0 +1,3 @@ +public struct CreateProjectTask { + +} diff --git a/Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift b/Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift new file mode 100644 index 0000000..5e885ed --- /dev/null +++ b/Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift @@ -0,0 +1,11 @@ +import ColibriLibrary +import Testing + +struct CreateProjectTaskTests { + + @Test(.disabled()) + func something() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} -- 2.47.1 From afa7686407c9f77e077c75c8ec226cccd3378ed5 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 03:13:42 +0100 Subject: [PATCH 20/80] Moved the Create.Options model to its own file in the executable target. --- Sources/Executable/Commands/Create.swift | 23 ------------------- .../Executable/Options/CreateOptions.swift | 22 ++++++++++++++++++ 2 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 Sources/Executable/Options/CreateOptions.swift diff --git a/Sources/Executable/Commands/Create.swift b/Sources/Executable/Commands/Create.swift index a8d4373..556fcb7 100644 --- a/Sources/Executable/Commands/Create.swift +++ b/Sources/Executable/Commands/Create.swift @@ -1,6 +1,5 @@ import ArgumentParser import ColibriLibrary -import Foundation extension Colibri { struct Create: AsyncParsableCommand { @@ -32,25 +31,3 @@ extension Colibri { } } - -// MARK: - Options - -extension Colibri.Create { - struct Options: ParsableArguments { - - // MARK: Properties - - @Option(name: .shortAndLong) - var name: String - - @Option(name: .shortAndLong) - var location: String? - - // MARK: Computed - - var locationURL: URL? { - location.flatMap { URL(fileURLWithPath: $0) } - } - - } -} diff --git a/Sources/Executable/Options/CreateOptions.swift b/Sources/Executable/Options/CreateOptions.swift new file mode 100644 index 0000000..8050ea8 --- /dev/null +++ b/Sources/Executable/Options/CreateOptions.swift @@ -0,0 +1,22 @@ +import ArgumentParser +import Foundation + +extension Colibri.Create { + struct Options: ParsableArguments { + + // MARK: Properties + + @Option(name: .shortAndLong) + var name: String + + @Option(name: .shortAndLong) + var location: String? + + // MARK: Computed + + var locationURL: URL? { + location.flatMap { URL(fileURLWithPath: $0) } + } + + } +} -- 2.47.1 From 3a447b4f71e4fbcc36b04c366320efb587db8ce0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 03:15:35 +0100 Subject: [PATCH 21/80] Renamed the Create file in the executable target as CreateCommand. --- Sources/Executable/Commands/{Create.swift => CreateCommand.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/Executable/Commands/{Create.swift => CreateCommand.swift} (100%) diff --git a/Sources/Executable/Commands/Create.swift b/Sources/Executable/Commands/CreateCommand.swift similarity index 100% rename from Sources/Executable/Commands/Create.swift rename to Sources/Executable/Commands/CreateCommand.swift -- 2.47.1 From a1ad391baa0a2391e94fc5f890839c27d4af7705 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 21:25:51 +0100 Subject: [PATCH 22/80] Implemented the "init(at: )" initialiser, the "pathString" computed property, and the "appendingPath(_: )" function for the URL+Extensions extension in the library target. --- .../Library/Extensions/URL+Extensions.swift | 35 +++++++++ Sources/Library/Extensions/URL+Inits.swift | 15 ---- Sources/Library/Services/FileService.swift | 18 +---- .../Library/Tasks/CreateRootFolderTask.swift | 8 +- .../Extensions/URL+ExtensionsTests.swift | 76 +++++++++++++++++++ 5 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 Sources/Library/Extensions/URL+Extensions.swift delete mode 100644 Sources/Library/Extensions/URL+Inits.swift create mode 100644 Tests/Library/Cases/Extensions/URL+ExtensionsTests.swift diff --git a/Sources/Library/Extensions/URL+Extensions.swift b/Sources/Library/Extensions/URL+Extensions.swift new file mode 100644 index 0000000..04b5999 --- /dev/null +++ b/Sources/Library/Extensions/URL+Extensions.swift @@ -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() + } else { + path + } + } + + // MARK: Functions + + func appendingPath(_ path: String) -> URL { + if #available(macOS 13.0, *) { + appending(path: path) + } else { + appendingPathComponent(path) + } + } + +} diff --git a/Sources/Library/Extensions/URL+Inits.swift b/Sources/Library/Extensions/URL+Inits.swift deleted file mode 100644 index 7c31114..0000000 --- a/Sources/Library/Extensions/URL+Inits.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -extension URL { - - // MARK: Initialisers - - init(at filePath: String) { - if #available(macOS 13.0, *) { - self = URL(filePath: filePath) - } else { - self = URL(fileURLWithPath: filePath) - } - } - -} diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift index 3e91ad2..1ccc4e6 100644 --- a/Sources/Library/Services/FileService.swift +++ b/Sources/Library/Services/FileService.swift @@ -54,25 +54,9 @@ public struct FileService: FileServicing { throw FileServiceError.urlNotFileURL } - let filePath = getPath(for: url) + let filePath = url.pathString return fileManager.fileExists(atPath: filePath) } } - -// MARK: - Helpers - -private extension FileService { - - // MARK: Functions - - func getPath(for url: URL) -> String { - if #available(macOS 13.0, *) { - return url.path() - } else { - return url.path - } - } - -} diff --git a/Sources/Library/Tasks/CreateRootFolderTask.swift b/Sources/Library/Tasks/CreateRootFolderTask.swift index fdee7e0..6345aee 100644 --- a/Sources/Library/Tasks/CreateRootFolderTask.swift +++ b/Sources/Library/Tasks/CreateRootFolderTask.swift @@ -27,12 +27,8 @@ public struct CreateRootFolderTask { } else { await fileService.currentFolder } - - let newFolder = if #available(macOS 13.0, *) { - rootFolder.appending(path: name) - } else { - rootFolder.appendingPathComponent(name) - } + + let newFolder = rootFolder.appendingPath(name) try await fileService.createFolder(at: newFolder) diff --git a/Tests/Library/Cases/Extensions/URL+ExtensionsTests.swift b/Tests/Library/Cases/Extensions/URL+ExtensionsTests.swift new file mode 100644 index 0000000..b5525ea --- /dev/null +++ b/Tests/Library/Cases/Extensions/URL+ExtensionsTests.swift @@ -0,0 +1,76 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct URL_ExtensionsTests { + + // MARK: Initialisers tests + + @Test(arguments: zip([String.someFilePath, .dotPath, .tildePath], + [URL.someFile, .dotFile, .tildeFile])) + func initAt( + with filePath: String, + expects url: URL + ) async throws { + // GIVEN + // WHEN + let result = URL(at: filePath) + + // THEN + #expect(result == url) + #expect(result.isFileURL == true) + } + + // MARK: Computed tests + + @Test(arguments: zip([URL.someFile, .dotFile, .tildeFile, .someURL], + [String.someFilePath, .dotPath, .tildePath, .empty])) + func pathString( + with url: URL, + expects path: String + ) async throws { + // GIVEN + // WHEN + let result = url.pathString + + // THEN + #expect(result == path) + } + + // MARK: Functions tests + + @Test(arguments: zip([URL.dotFile, .tildeFile, .someFile], + [".\(String.someFilePath)", "~\(String.someFilePath)", "\(String.someFilePath)\(String.someFilePath)"])) + func appendingPath( + with url: URL, + expects path: String + ) async throws { + // GIVEN + // WHEN + let result = url.appendingPath(.someFilePath) + + // THEN + #expect(result.pathString == path) + #expect(result.isFileURL == true) + } + +} + +// MARK: - String+Constants + +private extension String { + static let dotPath = "." + static let empty = "" + static let tildePath = "~" + static let someFilePath = "/some/file/path" +} + +// MARK: - URL+Constants + +private extension URL { + static let dotFile = URL(at: .dotPath) + static let someFile = URL(at: .someFilePath) + static let someURL = URL(string: "https://some.url.path")! + static let tildeFile = URL(at: .tildePath) +} -- 2.47.1 From 12151deea0779047ef872581e013cf74bc418b84 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 12 Jan 2025 23:28:20 +0100 Subject: [PATCH 23/80] Implemented the "actions" property for the FileServiceSpy spy in the tests target to support tracking multiple actions. --- .../Cases/Services/FileServiceTests.swift | 30 +++++++++-------- .../Helpers/Spies/FileServiceSpy.swift | 32 +++++++++++-------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Tests/Library/Cases/Services/FileServiceTests.swift index f686fe2..f67ac0d 100644 --- a/Tests/Library/Cases/Services/FileServiceTests.swift +++ b/Tests/Library/Cases/Services/FileServiceTests.swift @@ -39,8 +39,11 @@ struct FileServiceTests { try await service.createFolder(at: url) // THEN - #expect(spy.isCreateFolderCalled == true) - #expect(spy.urlCalled == url) + #expect(spy.actions.count == 1) + + let action = try #require(spy.actions.last) + + #expect(action == .folderCreated(url)) } @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someRandomURL], @@ -61,9 +64,8 @@ struct FileServiceTests { await #expect(throws: error) { try await service.createFolder(at: url) } - - #expect(spy.isCreateFolderCalled == false) - #expect(spy.urlCalled == nil) + + #expect(spy.actions.isEmpty == true) } @Test(arguments: [URL.someNewFolder, .someNewFile]) @@ -79,8 +81,11 @@ struct FileServiceTests { try await service.delete(at: url) // THEN - #expect(spy.isDeleteCalled == true) - #expect(spy.urlCalled == url) + #expect(spy.actions.count == 1) + + let action = try #require(spy.actions.last) + + #expect(action == .itemDeleted(url)) } @Test(arguments: zip([URL.someNewFolder, .someNewFile, .someRandomURL], @@ -102,8 +107,7 @@ struct FileServiceTests { try await service.delete(at: url) } - #expect(spy.isDeleteCalled == false) - #expect(spy.urlCalled == nil) + #expect(spy.actions.isEmpty == true) } @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNewFolder, .someNewFile], @@ -125,8 +129,9 @@ struct FileServiceTests { // THEN #expect(result == outcome) - #expect(spy.isExistsAtCalled == true) - #expect(spy.urlCalled == url) + let action = try #require(spy.actions.last) + + #expect(action == .itemExists(url)) } @Test(arguments: zip([URL.someRandomURL], [FileServiceError.urlNotFileURL])) @@ -147,8 +152,7 @@ struct FileServiceTests { try await service.exists(at: url) } - #expect(spy.isExistsAtCalled == false) - #expect(spy.urlCalled == nil) + #expect(spy.actions.isEmpty == true) } } diff --git a/Tests/Library/Helpers/Spies/FileServiceSpy.swift b/Tests/Library/Helpers/Spies/FileServiceSpy.swift index 6422bc9..7472218 100644 --- a/Tests/Library/Helpers/Spies/FileServiceSpy.swift +++ b/Tests/Library/Helpers/Spies/FileServiceSpy.swift @@ -6,10 +6,7 @@ final class FileServiceSpy { // MARK: Properties - private(set) var isCreateFolderCalled: Bool = false - private(set) var isDeleteCalled: Bool = false - private(set) var isExistsAtCalled: Bool = false - private(set) var urlCalled: URL? + private(set) var actions: [Action] = [] } @@ -20,22 +17,29 @@ extension FileServiceSpy: FileServicing { get async { .someCurrentFolder } } - func createFolder(at url: URL) async throws(FileServiceError) { - isCreateFolderCalled = true - urlCalled = url + func createFolder(at url: URL) async throws (FileServiceError) { + actions.append(.folderCreated(url)) } - func delete(at url: URL) async throws(FileServiceError) { - isDeleteCalled = true - urlCalled = url + func delete(at url: URL) async throws (FileServiceError) { + actions.append(.itemDeleted(url)) } @discardableResult - func exists(at url: URL) async throws(FileServiceError) -> Bool { - isExistsAtCalled = true - urlCalled = url - + func exists(at url: URL) async throws (FileServiceError) -> Bool { + actions.append(.itemExists(url)) + return .random() } } + +// MARK: - Action + +extension FileServiceSpy { + enum Action: Equatable { + case folderCreated(_ url: URL) + case itemDeleted(_ url: URL) + case itemExists(_ url: URL) + } +} -- 2.47.1 From 2852f4b1bf548ec52e823107cf34b6306a6fbf3d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 13 Jan 2025 00:32:16 +0100 Subject: [PATCH 24/80] Improved the FileServiceMock mock in the tests target to support multiple actions. --- .../Cases/Services/FileServiceTests.swift | 2 +- .../Helpers/Mocks/FileServiceMock.swift | 51 ++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Tests/Library/Cases/Services/FileServiceTests.swift index f67ac0d..9dd2ed2 100644 --- a/Tests/Library/Cases/Services/FileServiceTests.swift +++ b/Tests/Library/Cases/Services/FileServiceTests.swift @@ -24,7 +24,7 @@ struct FileServiceTests { #expect(folder.isFileURL == true) } - // MARK: Functions + // MARK: Functions tests @Test(arguments: [URL.someNewFolder, .someNewFile]) func createFolder(with url: URL) async throws { diff --git a/Tests/Library/Helpers/Mocks/FileServiceMock.swift b/Tests/Library/Helpers/Mocks/FileServiceMock.swift index fa7b6d7..5ccf7d7 100644 --- a/Tests/Library/Helpers/Mocks/FileServiceMock.swift +++ b/Tests/Library/Helpers/Mocks/FileServiceMock.swift @@ -1,13 +1,14 @@ import ColibriLibrary import Foundation -struct FileServiceMock { +final class FileServiceMock { // MARK: Properties - private let action: Action? private let folder: URL + private var actions: [Action] = [] + private weak var spy: FileServiceSpy? // MARK: Initialisers @@ -17,7 +18,21 @@ struct FileServiceMock { action: Action? = nil, spy: FileServiceSpy? = nil ) { - self.action = action + self.actions = if let action { + [action] + } else { + [] + } + self.folder = currentFolder + self.spy = spy + } + + init( + currentFolder: URL, + actions: [Action], + spy: FileServiceSpy? = nil + ) { + self.actions = actions self.folder = currentFolder self.spy = spy } @@ -37,7 +52,9 @@ extension FileServiceMock: FileServicing { // MARK: Functions func createFolder(at url: URL) async throws(FileServiceError) { - switch action { + guard let nextAction else { return } + + switch nextAction { case .error(let error): throw error case let .createFolder(url): @@ -48,7 +65,9 @@ extension FileServiceMock: FileServicing { } func delete(at url: URL) async throws(FileServiceError) { - switch action { + guard let nextAction else { return } + + switch nextAction { case .error(let error): throw error case let .delete(url): @@ -59,7 +78,9 @@ extension FileServiceMock: FileServicing { } func exists(at url: URL) async throws(FileServiceError) -> Bool { - switch action { + guard let nextAction else { return false } + + switch nextAction { case .error(let error): throw error case let .exists(url, exists): @@ -72,7 +93,23 @@ extension FileServiceMock: FileServicing { } -// MARK: - Enumerations +// MARK: - Helpers + +private extension FileServiceMock { + + // MARK: Computed + + var nextAction: Action? { + guard !actions.isEmpty else { + return nil + } + + return actions.removeFirst() + } + +} + +// MARK: - Actions extension FileServiceMock { enum Action { -- 2.47.1 From d065425c69ea79d7e8009f03b6dbc87c7da4c666 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 13 Jan 2025 00:33:13 +0100 Subject: [PATCH 25/80] Implemented the CreateFoldersTask task in the library target. --- Sources/Library/Tasks/CreateFoldersTask.swift | 38 ++++++++++++++++ Sources/Library/Tasks/CreateProjectTask.swift | 3 -- .../Cases/Tasks/CreateFoldersTaskTests.swift | 45 +++++++++++++++++++ .../Cases/Tasks/CreateProjectTaskTests.swift | 11 ----- .../Tasks/CreateRootFolderTaskTests.swift | 11 ++--- 5 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 Sources/Library/Tasks/CreateFoldersTask.swift delete mode 100644 Sources/Library/Tasks/CreateProjectTask.swift create mode 100644 Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift delete mode 100644 Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift diff --git a/Sources/Library/Tasks/CreateFoldersTask.swift b/Sources/Library/Tasks/CreateFoldersTask.swift new file mode 100644 index 0000000..4a512f2 --- /dev/null +++ b/Sources/Library/Tasks/CreateFoldersTask.swift @@ -0,0 +1,38 @@ +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 folderApp = rootFolder.appendingPath(.folderApp) + let folderAppInfrastructure = rootFolder.appendingPath(.folderAppInfrastructure) + let folderAppTestCases = rootFolder.appendingPath(.folderAppTestCases) + let folderAppTestSources = rootFolder.appendingPath(.folderAppTestSources) + + try await fileService.createFolder(at: folderApp) + try await fileService.createFolder(at: folderAppInfrastructure) + try await fileService.createFolder(at: folderAppTestCases) + try await fileService.createFolder(at: folderAppTestSources) + } + +} + +// MARK: - String+Constants + +private extension String { + static let folderApp = "Sources/App" + static let folderAppInfrastructure = "Sources/AppInfrastructure" + static let folderAppTestCases = "Tests/App/Cases" + static let folderAppTestSources = "Tests/App/Sources" +} diff --git a/Sources/Library/Tasks/CreateProjectTask.swift b/Sources/Library/Tasks/CreateProjectTask.swift deleted file mode 100644 index c1083f5..0000000 --- a/Sources/Library/Tasks/CreateProjectTask.swift +++ /dev/null @@ -1,3 +0,0 @@ -public struct CreateProjectTask { - -} diff --git a/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift b/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift new file mode 100644 index 0000000..137f6c1 --- /dev/null +++ b/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct CreateFoldersTaskTests { + + // MARK: Properties + + private let spy = FileServiceSpy() + + // MARK: Functions tests + + @Test(arguments: [URL.someCurrentFolder, .someDotFolder, .someTildeFolder]) + func createFolders(with rootFolder: URL) async throws { + // GIVEN + let folderApp = rootFolder.appendingPath("Sources/App") + let folderAppInfrastructure = rootFolder.appendingPath("Sources/AppInfrastructure") + let folderTestSources = rootFolder.appendingPath("Test/App/Sources") + let folderTestCases = rootFolder.appendingPath("Test/App/Cases") + + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + actions: [ + .createFolder(folderApp), + .createFolder(folderAppInfrastructure), + .createFolder(folderTestSources), + .createFolder(folderTestCases), + ], + spy: spy + ) + + let createFolders = CreateFoldersTask(fileService: service) + + // WHEN + try await createFolders(at: rootFolder) + + // THEN + #expect(spy.actions[0] == .folderCreated(folderApp)) + #expect(spy.actions[1] == .folderCreated(folderAppInfrastructure)) + #expect(spy.actions[2] == .folderCreated(folderTestSources)) + #expect(spy.actions[3] == .folderCreated(folderTestCases)) + } + +} diff --git a/Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift b/Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift deleted file mode 100644 index 5e885ed..0000000 --- a/Tests/Library/Cases/Tasks/CreateProjectTaskTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import ColibriLibrary -import Testing - -struct CreateProjectTaskTests { - - @Test(.disabled()) - func something() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - } - -} diff --git a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift index a3f6795..1fffb44 100644 --- a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift +++ b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift @@ -1,7 +1,8 @@ -import ColibriLibrary import Foundation import Testing +@testable import ColibriLibrary + struct CreateRootFolderTaskTests { // MARK: Functions tests @@ -84,8 +85,8 @@ private extension String { // MARK: - URL+Constants private extension URL { - static let someCurrentProjectFolder = URL.someCurrentFolder.appending(component: String.someProjectName) - static let someDotProjectFolder = URL.someDotFolder.appending(component: String.someProjectName) - static let someNewProjectFolder = URL.someNewFolder.appending(component: String.someProjectName) - static let someTildeProjectFolder = URL.someTildeFolder.appending(component: String.someProjectName) + static let someCurrentProjectFolder = URL.someCurrentFolder.appendingPath(.someProjectName) + static let someDotProjectFolder = URL.someDotFolder.appendingPath(.someProjectName) + static let someNewProjectFolder = URL.someNewFolder.appendingPath(.someProjectName) + static let someTildeProjectFolder = URL.someTildeFolder.appendingPath(.someProjectName) } -- 2.47.1 From df556f83abfa7a42f807d572eec3030bec2af357 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 13 Jan 2025 00:36:56 +0100 Subject: [PATCH 26/80] Added the CreateFoldersTasks task into the "run()" function for the CreateCommand command in the app target. --- Sources/Executable/Commands/CreateCommand.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/Executable/Commands/CreateCommand.swift b/Sources/Executable/Commands/CreateCommand.swift index 556fcb7..12d62a4 100644 --- a/Sources/Executable/Commands/CreateCommand.swift +++ b/Sources/Executable/Commands/CreateCommand.swift @@ -12,7 +12,7 @@ extension Colibri { helpNames: .shortAndLong, aliases: ["create"] ) - + @OptionGroup var options: Options // MARK: Functions @@ -20,13 +20,14 @@ extension Colibri { mutating func run() async throws { let fileService = FileService() let createRootFolder = CreateRootFolderTask(fileService: fileService) + let createFolders = CreateFoldersTask(fileService: fileService) let rootFolder = try await createRootFolder( name: options.name, at: options.locationURL ) - - print(rootFolder) + + try await createFolders(at: rootFolder) } } -- 2.47.1 From 26fde119ef4d8696c8ed8a03c6da5ed8b26161b1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 13 Jan 2025 23:27:50 +0100 Subject: [PATCH 27/80] Improved the naming of the functions for the FileServicing protocol in the library target. --- Sources/Library/Protocols/FileServicing.swift | 14 +++--- Sources/Library/Services/FileService.swift | 29 +++++------ .../Cases/Services/FileServiceTests.swift | 48 +++++++++---------- .../Tasks/CreateRootFolderTaskTests.swift | 2 +- .../Helpers/Mocks/FileServiceMock.swift | 24 +++++----- .../Helpers/Spies/FileServiceSpy.swift | 21 ++++---- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/Sources/Library/Protocols/FileServicing.swift b/Sources/Library/Protocols/FileServicing.swift index 5f85840..011c47a 100644 --- a/Sources/Library/Protocols/FileServicing.swift +++ b/Sources/Library/Protocols/FileServicing.swift @@ -8,9 +8,9 @@ public protocol FileServicing { // MARK: Functions - func createFolder(at url: URL) async throws (FileServiceError) - func delete(at url: URL) async throws (FileServiceError) - func exists(at url: URL) async throws (FileServiceError) -> Bool + 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 } @@ -18,8 +18,8 @@ public protocol FileServicing { public enum FileServiceError: Error, Equatable { case folderNotCreated - case urlAlreadyExists - case urlNotDeleted - case urlNotExists - case urlNotFileURL + case itemAlreadyExists + case itemNotDeleted + case itemNotExists + case itemNotFileURL } diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift index 1ccc4e6..acc4e7c 100644 --- a/Sources/Library/Services/FileService.swift +++ b/Sources/Library/Services/FileService.swift @@ -22,39 +22,36 @@ public struct FileService: FileServicing { // MARK: Functions - public func createFolder(at url: URL) async throws (FileServiceError) { - guard try await !exists(at: url) else { - throw FileServiceError.urlAlreadyExists + public func createFolder(at location: URL) async throws (FileServiceError) { + guard try await !isItemExists(at: location) else { + throw FileServiceError.itemAlreadyExists } do { - try fileManager.createDirectory( - at: url, - withIntermediateDirectories: true - ) + try fileManager.createDirectory(at: location, withIntermediateDirectories: true) } catch { throw FileServiceError.folderNotCreated } } - public func delete(at url: URL) async throws (FileServiceError) { - guard try await exists(at: url) else { - throw FileServiceError.urlNotExists + public func deleteItem(at location: URL) async throws (FileServiceError) { + guard try await isItemExists(at: location) else { + throw FileServiceError.itemNotExists } do { - try fileManager.removeItem(at: url) + try fileManager.removeItem(at: location) } catch { - throw FileServiceError.urlNotDeleted + throw FileServiceError.itemNotDeleted } } - public func exists(at url: URL) async throws (FileServiceError) -> Bool { - guard url.isFileURL else { - throw FileServiceError.urlNotFileURL + public func isItemExists(at location: URL) async throws (FileServiceError) -> Bool { + guard location.isFileURL else { + throw FileServiceError.itemNotFileURL } - let filePath = url.pathString + let filePath = location.pathString return fileManager.fileExists(atPath: filePath) } diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Tests/Library/Cases/Services/FileServiceTests.swift index 9dd2ed2..5df3b97 100644 --- a/Tests/Library/Cases/Services/FileServiceTests.swift +++ b/Tests/Library/Cases/Services/FileServiceTests.swift @@ -27,29 +27,29 @@ struct FileServiceTests { // MARK: Functions tests @Test(arguments: [URL.someNewFolder, .someNewFile]) - func createFolder(with url: URL) async throws { + func createFolder(with location: URL) async throws { // GIVEN let service = FileServiceMock( currentFolder: .someCurrentFolder, - action: .createFolder(url), + action: .createFolder(location), spy: spy ) // WHEN - try await service.createFolder(at: url) + try await service.createFolder(at: location) // THEN #expect(spy.actions.count == 1) let action = try #require(spy.actions.last) - #expect(action == .folderCreated(url)) + #expect(action == .folderCreated(location)) } @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someRandomURL], - [FileServiceError.urlAlreadyExists, .urlAlreadyExists, .urlNotFileURL])) + [FileServiceError.itemAlreadyExists, .itemAlreadyExists, .itemNotFileURL])) func createFolder( - with url: URL, + with location: URL, throws error: FileServiceError ) async throws { // GIVEN @@ -62,36 +62,36 @@ struct FileServiceTests { // WHEN // THEN await #expect(throws: error) { - try await service.createFolder(at: url) + try await service.createFolder(at: location) } #expect(spy.actions.isEmpty == true) } @Test(arguments: [URL.someNewFolder, .someNewFile]) - func delete(with url: URL) async throws { + func deleteItem(with location: URL) async throws { // GIVEN let service = FileServiceMock( currentFolder: .someCurrentFolder, - action: .delete(url), + action: .deleteItem(location), spy: spy ) // WHEN - try await service.delete(at: url) + try await service.deleteItem(at: location) // THEN #expect(spy.actions.count == 1) let action = try #require(spy.actions.last) - #expect(action == .itemDeleted(url)) + #expect(action == .itemDeleted(location)) } @Test(arguments: zip([URL.someNewFolder, .someNewFile, .someRandomURL], - [FileServiceError.urlNotExists, .urlNotExists, .urlNotFileURL])) - func delete( - with url: URL, + [FileServiceError.itemNotExists, .itemNotExists, .itemNotFileURL])) + func deleteItem( + with location: URL, throws error: FileServiceError ) async throws { // GIVEN @@ -104,7 +104,7 @@ struct FileServiceTests { // WHEN // THEN await #expect(throws: error) { - try await service.delete(at: url) + try await service.deleteItem(at: location) } #expect(spy.actions.isEmpty == true) @@ -112,31 +112,31 @@ struct FileServiceTests { @Test(arguments: zip([URL.someExistingFolder, .someExistingFile, .someNewFolder, .someNewFile], [true, true, false, false])) - func exists( - with url: URL, + func isItemExists( + with location: URL, expects outcome: Bool ) async throws { // GIVEN let service = FileServiceMock( currentFolder: .someCurrentFolder, - action: .exists(url, outcome), + action: .isItemExists(location, outcome), spy: spy ) // WHEN - let result = try await service.exists(at: url) + let result = try await service.isItemExists(at: location) // THEN #expect(result == outcome) let action = try #require(spy.actions.last) - #expect(action == .itemExists(url)) + #expect(action == .itemExists(location)) } - @Test(arguments: zip([URL.someRandomURL], [FileServiceError.urlNotFileURL])) - func exists( - with url: URL, + @Test(arguments: zip([URL.someRandomURL], [FileServiceError.itemNotFileURL])) + func isItemExists( + with location: URL, throws error: FileServiceError ) async throws { // GIVEN @@ -149,7 +149,7 @@ struct FileServiceTests { // WHEN // THEN await #expect(throws: error) { - try await service.exists(at: url) + try await service.isItemExists(at: location) } #expect(spy.actions.isEmpty == true) diff --git a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift index 1fffb44..1b9b5f1 100644 --- a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift +++ b/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift @@ -36,7 +36,7 @@ struct CreateRootFolderTaskTests { #expect(result.isFileURL == true) } - @Test(arguments: [String.someProjectName], [FileServiceError.urlAlreadyExists]) + @Test(arguments: [String.someProjectName], [FileServiceError.itemAlreadyExists]) func task( name: String, throws error: FileServiceError diff --git a/Tests/Library/Helpers/Mocks/FileServiceMock.swift b/Tests/Library/Helpers/Mocks/FileServiceMock.swift index 5ccf7d7..dbc7368 100644 --- a/Tests/Library/Helpers/Mocks/FileServiceMock.swift +++ b/Tests/Library/Helpers/Mocks/FileServiceMock.swift @@ -42,7 +42,7 @@ final class FileServiceMock { // MARK: - FileServicing extension FileServiceMock: FileServicing { - + // MARK: Computed var currentFolder: URL { @@ -51,40 +51,40 @@ extension FileServiceMock: FileServicing { // MARK: Functions - func createFolder(at url: URL) async throws(FileServiceError) { + func createFolder(at location: URL) async throws (FileServiceError) { guard let nextAction else { return } switch nextAction { case .error(let error): throw error - case let .createFolder(url): - try await spy?.createFolder(at: url) + case let .createFolder(location): + try await spy?.createFolder(at: location) default: break } } - func delete(at url: URL) async throws(FileServiceError) { + func deleteItem(at location: URL) async throws (FileServiceError) { guard let nextAction else { return } switch nextAction { case .error(let error): throw error - case let .delete(url): - try await spy?.delete(at: url) + case let .deleteItem(location): + try await spy?.deleteItem(at: location) default: break } } - func exists(at url: URL) async throws(FileServiceError) -> Bool { + func isItemExists(at location: URL) async throws (FileServiceError) -> Bool { guard let nextAction else { return false } switch nextAction { case .error(let error): throw error - case let .exists(url, exists): - try await spy?.exists(at: url) + case let .isItemExists(location, exists): + try await spy?.isItemExists(at: location) return exists default: return false @@ -114,8 +114,8 @@ private extension FileServiceMock { extension FileServiceMock { enum Action { case createFolder(URL) - case delete(URL) + case deleteItem(URL) case error(FileServiceError) - case exists(URL, Bool) + case isItemExists(URL, Bool) } } diff --git a/Tests/Library/Helpers/Spies/FileServiceSpy.swift b/Tests/Library/Helpers/Spies/FileServiceSpy.swift index 7472218..3906733 100644 --- a/Tests/Library/Helpers/Spies/FileServiceSpy.swift +++ b/Tests/Library/Helpers/Spies/FileServiceSpy.swift @@ -13,21 +13,24 @@ final class FileServiceSpy { // MARK: - FileServicing extension FileServiceSpy: FileServicing { + var currentFolder: URL { get async { .someCurrentFolder } } - func createFolder(at url: URL) async throws (FileServiceError) { - actions.append(.folderCreated(url)) } - func delete(at url: URL) async throws (FileServiceError) { - actions.append(.itemDeleted(url)) + func createFolder(at location: URL) async throws (FileServiceError) { + actions.append(.folderCreated(location)) + } + + func deleteItem(at location: URL) async throws (FileServiceError) { + actions.append(.itemDeleted(location)) } @discardableResult - func exists(at url: URL) async throws (FileServiceError) -> Bool { - actions.append(.itemExists(url)) + func isItemExists(at location: URL) async throws (FileServiceError) -> Bool { + actions.append(.itemExists(location)) return .random() } @@ -38,8 +41,8 @@ extension FileServiceSpy: FileServicing { extension FileServiceSpy { enum Action: Equatable { - case folderCreated(_ url: URL) - case itemDeleted(_ url: URL) - case itemExists(_ url: URL) + case folderCreated(_ location: URL) + case itemDeleted(_ location: URL) + case itemExists(_ location: URL) } } -- 2.47.1 From 489cf3d780c947e2d97f24c634ca65549f4ae708 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 14 Jan 2025 23:52:33 +0100 Subject: [PATCH 28/80] Implemented the "copyItem(from: to: )" function for the FileService service in the library target. --- Sources/Library/Protocols/FileServicing.swift | 2 + Sources/Library/Services/FileService.swift | 15 +++++++ .../Cases/Services/FileServiceTests.swift | 39 +++++++++++++++++++ .../Helpers/Mocks/FileServiceMock.swift | 14 +++++++ .../Helpers/Spies/FileServiceSpy.swift | 3 ++ 5 files changed, 73 insertions(+) diff --git a/Sources/Library/Protocols/FileServicing.swift b/Sources/Library/Protocols/FileServicing.swift index 011c47a..674499e 100644 --- a/Sources/Library/Protocols/FileServicing.swift +++ b/Sources/Library/Protocols/FileServicing.swift @@ -8,6 +8,7 @@ public protocol FileServicing { // MARK: Functions + func copyItem(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 @@ -18,6 +19,7 @@ public protocol FileServicing { public enum FileServiceError: Error, Equatable { case folderNotCreated + case itemNotCopied case itemAlreadyExists case itemNotDeleted case itemNotExists diff --git a/Sources/Library/Services/FileService.swift b/Sources/Library/Services/FileService.swift index acc4e7c..3df4be9 100644 --- a/Sources/Library/Services/FileService.swift +++ b/Sources/Library/Services/FileService.swift @@ -21,6 +21,21 @@ public struct FileService: FileServicing { } // MARK: Functions + + public func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) { + guard try await isItemExists(at: source) else { + throw FileServiceError.itemNotExists + } + guard try await !isItemExists(at: destination) else { + throw FileServiceError.itemAlreadyExists + } + + do { + try fileManager.copyItem(at: source, to: destination) + } catch { + throw FileServiceError.itemNotCopied + } + } public func createFolder(at location: URL) async throws (FileServiceError) { guard try await !isItemExists(at: location) else { diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Tests/Library/Cases/Services/FileServiceTests.swift index 5df3b97..2edbc48 100644 --- a/Tests/Library/Cases/Services/FileServiceTests.swift +++ b/Tests/Library/Cases/Services/FileServiceTests.swift @@ -26,6 +26,45 @@ struct FileServiceTests { // MARK: Functions tests + @Test(arguments: zip([URL.someExistingFile, .someExistingFolder], + [URL.someNewFile, .someNewFolder])) + func copyItem(from source: URL, to destination: URL) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .copyItem(source, destination), + spy: spy + ) + + // WHEN + try await service.copyItem(from: source, to: destination) + + // THENn + #expect(spy.actions.count == 1) + + let action = try #require(spy.actions.last) + + #expect(action == .itemCopied(source, destination)) + } + + @Test(arguments: [FileServiceError.itemNotExists, .itemAlreadyExists, .itemNotCopied]) + func copyItem(throws error: FileServiceError) async throws { + // GIVEN + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + action: .error(error), + spy: spy + ) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.copyItem(from: .someExistingFile, to: .someNewFile) + } + + #expect(spy.actions.isEmpty == true) + } + @Test(arguments: [URL.someNewFolder, .someNewFile]) func createFolder(with location: URL) async throws { // GIVEN diff --git a/Tests/Library/Helpers/Mocks/FileServiceMock.swift b/Tests/Library/Helpers/Mocks/FileServiceMock.swift index dbc7368..8d11b64 100644 --- a/Tests/Library/Helpers/Mocks/FileServiceMock.swift +++ b/Tests/Library/Helpers/Mocks/FileServiceMock.swift @@ -51,6 +51,19 @@ extension FileServiceMock: FileServicing { // MARK: Functions + func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) { + guard let nextAction else { return } + + switch nextAction { + case .error(let error): + throw error + case let .copyItem(source, destination): + try await spy?.copyItem(from: source, to: destination) + default: + break + } + } + func createFolder(at location: URL) async throws (FileServiceError) { guard let nextAction else { return } @@ -113,6 +126,7 @@ private extension FileServiceMock { extension FileServiceMock { enum Action { + case copyItem(URL, URL) case createFolder(URL) case deleteItem(URL) case error(FileServiceError) diff --git a/Tests/Library/Helpers/Spies/FileServiceSpy.swift b/Tests/Library/Helpers/Spies/FileServiceSpy.swift index 3906733..3b21653 100644 --- a/Tests/Library/Helpers/Spies/FileServiceSpy.swift +++ b/Tests/Library/Helpers/Spies/FileServiceSpy.swift @@ -18,6 +18,8 @@ extension FileServiceSpy: FileServicing { get async { .someCurrentFolder } } + func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) { + actions.append(.itemCopied(source, destination)) } func createFolder(at location: URL) async throws (FileServiceError) { @@ -42,6 +44,7 @@ extension FileServiceSpy: FileServicing { extension FileServiceSpy { enum Action: Equatable { case folderCreated(_ location: URL) + case itemCopied(_ source: URL, _ destination: URL) case itemDeleted(_ location: URL) case itemExists(_ location: URL) } -- 2.47.1 From 7cd17777354f2fcbd3aafcd8eeb3c9db597c8869 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 16 Jan 2025 00:31:53 +0100 Subject: [PATCH 29/80] Moved the source files under the "CLI" folder in the executable target. --- Sources/Executable/{ => CLI}/Colibri.swift | 0 Sources/Executable/{ => CLI}/Commands/CreateCommand.swift | 3 ++- Sources/Executable/{ => CLI}/Options/CreateOptions.swift | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename Sources/Executable/{ => CLI}/Colibri.swift (100%) rename Sources/Executable/{ => CLI}/Commands/CreateCommand.swift (98%) rename Sources/Executable/{ => CLI}/Options/CreateOptions.swift (100%) diff --git a/Sources/Executable/Colibri.swift b/Sources/Executable/CLI/Colibri.swift similarity index 100% rename from Sources/Executable/Colibri.swift rename to Sources/Executable/CLI/Colibri.swift diff --git a/Sources/Executable/Commands/CreateCommand.swift b/Sources/Executable/CLI/Commands/CreateCommand.swift similarity index 98% rename from Sources/Executable/Commands/CreateCommand.swift rename to Sources/Executable/CLI/Commands/CreateCommand.swift index 12d62a4..f15cca4 100644 --- a/Sources/Executable/Commands/CreateCommand.swift +++ b/Sources/Executable/CLI/Commands/CreateCommand.swift @@ -19,8 +19,9 @@ extension Colibri { mutating func run() async throws { let fileService = FileService() - let createRootFolder = CreateRootFolderTask(fileService: fileService) + let createFolders = CreateFoldersTask(fileService: fileService) + let createRootFolder = CreateRootFolderTask(fileService: fileService) let rootFolder = try await createRootFolder( name: options.name, diff --git a/Sources/Executable/Options/CreateOptions.swift b/Sources/Executable/CLI/Options/CreateOptions.swift similarity index 100% rename from Sources/Executable/Options/CreateOptions.swift rename to Sources/Executable/CLI/Options/CreateOptions.swift -- 2.47.1 From 95698a28247aa07367de6821813be841f4615971 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 16 Jan 2025 00:36:56 +0100 Subject: [PATCH 30/80] Added some resource files to the executable target. --- Package.swift | 5 +- .../Executable/Resources/Files/.dockerignore | 2 + Sources/Executable/Resources/Files/.gitignore | 10 + Sources/Executable/Resources/Files/LICENSE | 201 ++++++++++++++++++ Sources/Executable/Resources/Files/README.md | 10 + 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 Sources/Executable/Resources/Files/.dockerignore create mode 100644 Sources/Executable/Resources/Files/.gitignore create mode 100644 Sources/Executable/Resources/Files/LICENSE create mode 100644 Sources/Executable/Resources/Files/README.md diff --git a/Package.swift b/Package.swift index d8870c1..e6333af 100644 --- a/Package.swift +++ b/Package.swift @@ -33,7 +33,10 @@ let package = Package( ), .target(name: "ColibriLibrary") ], - path: "Sources/Executable" + path: "Sources/Executable", + resources: [ + .copy("Resources/") + ] ), .target( name: "ColibriLibrary", diff --git a/Sources/Executable/Resources/Files/.dockerignore b/Sources/Executable/Resources/Files/.dockerignore new file mode 100644 index 0000000..4e05543 --- /dev/null +++ b/Sources/Executable/Resources/Files/.dockerignore @@ -0,0 +1,2 @@ +.build +.git diff --git a/Sources/Executable/Resources/Files/.gitignore b/Sources/Executable/Resources/Files/.gitignore new file mode 100644 index 0000000..edcd2d6 --- /dev/null +++ b/Sources/Executable/Resources/Files/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/.devContainer +/.swiftpm +/.vscode +/Packages +/*.xcodeproj +xcuserdata/ +.env.* +.env diff --git a/Sources/Executable/Resources/Files/LICENSE b/Sources/Executable/Resources/Files/LICENSE new file mode 100644 index 0000000..bea052c --- /dev/null +++ b/Sources/Executable/Resources/Files/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Adam Fowler + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Sources/Executable/Resources/Files/README.md b/Sources/Executable/Resources/Files/README.md new file mode 100644 index 0000000..e13e70d --- /dev/null +++ b/Sources/Executable/Resources/Files/README.md @@ -0,0 +1,10 @@ +

+ + + + +

+ +# Hummingbird project template + +This is a template for your new [Hummingbird](https://wwww.hummingbird.codes) project. -- 2.47.1 From 72230c5337140b7ede8edabfbd5576874860c91c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 16 Jan 2025 01:35:52 +0100 Subject: [PATCH 31/80] Implemented the CopyFilesTask task in the library target. --- Sources/Library/Tasks/CopyFilesTask.swift | 53 +++++++++++++++++++ .../Cases/Tasks/CopyFilesTaskTests.swift | 43 +++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 Sources/Library/Tasks/CopyFilesTask.swift create mode 100644 Tests/Library/Cases/Tasks/CopyFilesTaskTests.swift diff --git a/Sources/Library/Tasks/CopyFilesTask.swift b/Sources/Library/Tasks/CopyFilesTask.swift new file mode 100644 index 0000000..182f19a --- /dev/null +++ b/Sources/Library/Tasks/CopyFilesTask.swift @@ -0,0 +1,53 @@ +import Foundation + +public struct CopyFilesTask { + + // MARK: Properties + + private let fileService: FileServicing + + // MARK: Initialisers + + public init(fileService: FileServicing) { + self.fileService = fileService + } + + // MARK: Functions + + public func callAsFunction(to rootFolder: URL) async throws { + let filesFolder = URL(at: .folderFiles) + + for fileToCopy in Self.filesToCopy { + try await fileService.copyItem( + from: filesFolder.appendingPath(fileToCopy), + to: rootFolder.appendingPath(fileToCopy) + ) + } + } + +} + +// MARK: - Helpers + +extension CopyFilesTask { + + // MARK: Constants + + static let filesToCopy: [String] = [ + .fileDockerIgnore, + .fileGitIgnore, + .fileLicense, + .fileReadme + ] + +} + +// MARK: - URL+Constants + +private extension String { + static let folderFiles = "./Resources/Files" + static let fileDockerIgnore = ".dockerignore" + static let fileGitIgnore = ".gitignore" + static let fileLicense = "LICENSE" + static let fileReadme = "README.md" +} diff --git a/Tests/Library/Cases/Tasks/CopyFilesTaskTests.swift b/Tests/Library/Cases/Tasks/CopyFilesTaskTests.swift new file mode 100644 index 0000000..b473717 --- /dev/null +++ b/Tests/Library/Cases/Tasks/CopyFilesTaskTests.swift @@ -0,0 +1,43 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct CopyFilesTaskTests { + + // MARK: Properties + + private let spy = FileServiceSpy() + + // MARK: Functions tests + + @Test(arguments: zip([URL.someExistingFolder], [URL.someNewFolder])) + func copyFiles(from source: URL, to destination: URL) async throws { + // GIVEN + let filesToCopy = CopyFilesTask.filesToCopy + let destinations = filesToCopy.map { destination.appendingPath($0) } + let sources = filesToCopy.map { source.appendingPath($0) } + let actions = filesToCopy.indices.map { index -> FileServiceMock.Action in + .copyItem(sources[index], destinations[index]) + } + + let service = FileServiceMock( + currentFolder: .someCurrentFolder, + actions: actions, + spy: spy + ) + + let copyFiles = CopyFilesTask(fileService: service) + + // WHEN + try await copyFiles(to: destination) + + // THEN + #expect(spy.actions.count == actions.count) + + for index in actions.indices { + #expect(spy.actions[index] == .itemCopied(sources[index], destinations[index])) + } + } + +} -- 2.47.1 From fbb5d2d2a43d8cd3c5318022a1040c68956448d6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 16 Jan 2025 01:45:41 +0100 Subject: [PATCH 32/80] Improved upon the implementation of the "callAsFunction(at: )" function for the CreateFoldersTask task in the library target. --- Sources/Library/Tasks/CreateFoldersTask.swift | 29 +++++++++++++------ .../Cases/Tasks/CreateFoldersTaskTests.swift | 22 +++++--------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/Sources/Library/Tasks/CreateFoldersTask.swift b/Sources/Library/Tasks/CreateFoldersTask.swift index 4a512f2..a0a3531 100644 --- a/Sources/Library/Tasks/CreateFoldersTask.swift +++ b/Sources/Library/Tasks/CreateFoldersTask.swift @@ -15,19 +15,30 @@ public struct CreateFoldersTask { // MARK: Functions public func callAsFunction(at rootFolder: URL) async throws { - let folderApp = rootFolder.appendingPath(.folderApp) - let folderAppInfrastructure = rootFolder.appendingPath(.folderAppInfrastructure) - let folderAppTestCases = rootFolder.appendingPath(.folderAppTestCases) - let folderAppTestSources = rootFolder.appendingPath(.folderAppTestSources) - - try await fileService.createFolder(at: folderApp) - try await fileService.createFolder(at: folderAppInfrastructure) - try await fileService.createFolder(at: folderAppTestCases) - try await fileService.createFolder(at: folderAppTestSources) + let folders = Self.foldersToCreate.map { rootFolder.appendingPath($0) } + + for folder in folders { + try await fileService.createFolder(at: folder) + } } } +// MARK: - Helpers + +extension CreateFoldersTask { + + // MARK: Constants + + static let foldersToCreate: [String] = [ + .folderApp, + .folderAppInfrastructure, + .folderAppTestCases, + .folderAppTestSources + ] + +} + // MARK: - String+Constants private extension String { diff --git a/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift b/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift index 137f6c1..396ac89 100644 --- a/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift +++ b/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift @@ -14,19 +14,12 @@ struct CreateFoldersTaskTests { @Test(arguments: [URL.someCurrentFolder, .someDotFolder, .someTildeFolder]) func createFolders(with rootFolder: URL) async throws { // GIVEN - let folderApp = rootFolder.appendingPath("Sources/App") - let folderAppInfrastructure = rootFolder.appendingPath("Sources/AppInfrastructure") - let folderTestSources = rootFolder.appendingPath("Test/App/Sources") - let folderTestCases = rootFolder.appendingPath("Test/App/Cases") - + let folders = CreateFoldersTask.foldersToCreate.map { rootFolder.appendingPath($0) } + let actions: [FileServiceMock.Action] = folders.map { .createFolder($0) } + let service = FileServiceMock( currentFolder: .someCurrentFolder, - actions: [ - .createFolder(folderApp), - .createFolder(folderAppInfrastructure), - .createFolder(folderTestSources), - .createFolder(folderTestCases), - ], + actions: actions, spy: spy ) @@ -36,10 +29,9 @@ struct CreateFoldersTaskTests { try await createFolders(at: rootFolder) // THEN - #expect(spy.actions[0] == .folderCreated(folderApp)) - #expect(spy.actions[1] == .folderCreated(folderAppInfrastructure)) - #expect(spy.actions[2] == .folderCreated(folderTestSources)) - #expect(spy.actions[3] == .folderCreated(folderTestCases)) + for index in actions.indices { + #expect(spy.actions[index] == .folderCreated(folders[index])) + } } } -- 2.47.1 From 02299f1cc29091fbe5744170c203728fe9204c0e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 16 Jan 2025 01:46:42 +0100 Subject: [PATCH 33/80] Integrated the CopyFilesTask task into the "run()" function for the CreateCommand command in the executable target. --- Sources/Executable/CLI/Commands/CreateCommand.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Executable/CLI/Commands/CreateCommand.swift b/Sources/Executable/CLI/Commands/CreateCommand.swift index f15cca4..1a5899e 100644 --- a/Sources/Executable/CLI/Commands/CreateCommand.swift +++ b/Sources/Executable/CLI/Commands/CreateCommand.swift @@ -20,6 +20,7 @@ extension Colibri { mutating func run() async throws { let fileService = FileService() + let copyFiles = CopyFilesTask(fileService: fileService) let createFolders = CreateFoldersTask(fileService: fileService) let createRootFolder = CreateRootFolderTask(fileService: fileService) @@ -29,6 +30,7 @@ extension Colibri { ) try await createFolders(at: rootFolder) + try await copyFiles(to: rootFolder) } } -- 2.47.1 From 94e28a324bed6abd2471677fe27dcefc52272c20 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 17 Jan 2025 23:17:20 +0100 Subject: [PATCH 34/80] Moved the project files from the executable target to the library target. --- .../Resources/Files/.dockerignore | 0 .../Resources/Files/.gitignore | 0 .../Resources/Files/LICENSE | 0 Sources/Library/Resources/Files/Package | 47 +++++++++++++++++++ .../Resources/Files/README} | 0 5 files changed, 47 insertions(+) rename Sources/{Executable => Library}/Resources/Files/.dockerignore (100%) rename Sources/{Executable => Library}/Resources/Files/.gitignore (100%) rename Sources/{Executable => Library}/Resources/Files/LICENSE (100%) create mode 100644 Sources/Library/Resources/Files/Package rename Sources/{Executable/Resources/Files/README.md => Library/Resources/Files/README} (100%) diff --git a/Sources/Executable/Resources/Files/.dockerignore b/Sources/Library/Resources/Files/.dockerignore similarity index 100% rename from Sources/Executable/Resources/Files/.dockerignore rename to Sources/Library/Resources/Files/.dockerignore diff --git a/Sources/Executable/Resources/Files/.gitignore b/Sources/Library/Resources/Files/.gitignore similarity index 100% rename from Sources/Executable/Resources/Files/.gitignore rename to Sources/Library/Resources/Files/.gitignore diff --git a/Sources/Executable/Resources/Files/LICENSE b/Sources/Library/Resources/Files/LICENSE similarity index 100% rename from Sources/Executable/Resources/Files/LICENSE rename to Sources/Library/Resources/Files/LICENSE diff --git a/Sources/Library/Resources/Files/Package b/Sources/Library/Resources/Files/Package new file mode 100644 index 0000000..8043ab3 --- /dev/null +++ b/Sources/Library/Resources/Files/Package @@ -0,0 +1,47 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "{{HB_PACKAGE_NAME}}", + platforms: [ + .macOS(.v10_15) + ], + products: [ + .executable(name: "app", targets: ["App"]), + .library(name: "AppInfrastructure", targets: ["AppInfrastructure"]) + ], + dependencies: [ + .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + .byName(name: "AppInfrastructure"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird") + ], + path: "Sources/App" + ), + .target( + name: "AppInfrastructure", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird"), + ], + path: "Sources/AppInfrastructure" + ), + .testTarget( + name: "AppTests", + dependencies: [ + dependencies: [ + .byName(name: "AppInfrastructure"), + .product(name: "HummingbirdTesting", package: "hummingbird") + ] + ], + path: "Tests/App" + ) + ] +) diff --git a/Sources/Executable/Resources/Files/README.md b/Sources/Library/Resources/Files/README similarity index 100% rename from Sources/Executable/Resources/Files/README.md rename to Sources/Library/Resources/Files/README -- 2.47.1 From 6955d816ef8f1ebd1dfd03b1c7f01bfc401e6dd2 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 17 Jan 2025 23:19:01 +0100 Subject: [PATCH 35/80] Restructured the folder structure of the executable target. --- .../CLI => Executable/Sources}/Colibri.swift | 0 .../Sources}/Commands/CreateCommand.swift | 0 .../Sources}/Options/CreateOptions.swift | 0 Package.swift | 10 +++++----- 4 files changed, 5 insertions(+), 5 deletions(-) rename {Sources/Executable/CLI => Executable/Sources}/Colibri.swift (100%) rename {Sources/Executable/CLI => Executable/Sources}/Commands/CreateCommand.swift (100%) rename {Sources/Executable/CLI => Executable/Sources}/Options/CreateOptions.swift (100%) diff --git a/Sources/Executable/CLI/Colibri.swift b/Executable/Sources/Colibri.swift similarity index 100% rename from Sources/Executable/CLI/Colibri.swift rename to Executable/Sources/Colibri.swift diff --git a/Sources/Executable/CLI/Commands/CreateCommand.swift b/Executable/Sources/Commands/CreateCommand.swift similarity index 100% rename from Sources/Executable/CLI/Commands/CreateCommand.swift rename to Executable/Sources/Commands/CreateCommand.swift diff --git a/Sources/Executable/CLI/Options/CreateOptions.swift b/Executable/Sources/Options/CreateOptions.swift similarity index 100% rename from Sources/Executable/CLI/Options/CreateOptions.swift rename to Executable/Sources/Options/CreateOptions.swift diff --git a/Package.swift b/Package.swift index e6333af..67c92d0 100644 --- a/Package.swift +++ b/Package.swift @@ -33,15 +33,15 @@ let package = Package( ), .target(name: "ColibriLibrary") ], - path: "Sources/Executable", - resources: [ - .copy("Resources/") - ] + path: "Executable" ), .target( name: "ColibriLibrary", dependencies: [], - path: "Sources/Library" + path: "Sources/Library", + resources: [ + .copy("Resources/Files/LICENSE") + ] ), .testTarget( name: "ColibriTests", -- 2.47.1 From 04bd2a11605e655e68e591812a19166bb99a1e35 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 17 Jan 2025 23:21:25 +0100 Subject: [PATCH 36/80] Restructured the folder structure of the library target. --- {Sources/Library => Library}/Resources/Files/.dockerignore | 0 {Sources/Library => Library}/Resources/Files/.gitignore | 0 {Sources/Library => Library}/Resources/Files/LICENSE | 0 {Sources/Library => Library}/Resources/Files/Package | 0 {Sources/Library => Library}/Resources/Files/README | 0 .../Sources}/Extensions/URL+Extensions.swift | 0 .../Library => Library/Sources}/Protocols/FileServicing.swift | 0 .../Library => Library/Sources}/Services/FileService.swift | 0 .../Library => Library/Sources}/Tasks/CopyFilesTask.swift | 0 .../Library => Library/Sources}/Tasks/CreateFoldersTask.swift | 0 .../Sources}/Tasks/CreateRootFolderTask.swift | 0 Package.swift | 4 ++-- 12 files changed, 2 insertions(+), 2 deletions(-) rename {Sources/Library => Library}/Resources/Files/.dockerignore (100%) rename {Sources/Library => Library}/Resources/Files/.gitignore (100%) rename {Sources/Library => Library}/Resources/Files/LICENSE (100%) rename {Sources/Library => Library}/Resources/Files/Package (100%) rename {Sources/Library => Library}/Resources/Files/README (100%) rename {Sources/Library => Library/Sources}/Extensions/URL+Extensions.swift (100%) rename {Sources/Library => Library/Sources}/Protocols/FileServicing.swift (100%) rename {Sources/Library => Library/Sources}/Services/FileService.swift (100%) rename {Sources/Library => Library/Sources}/Tasks/CopyFilesTask.swift (100%) rename {Sources/Library => Library/Sources}/Tasks/CreateFoldersTask.swift (100%) rename {Sources/Library => Library/Sources}/Tasks/CreateRootFolderTask.swift (100%) diff --git a/Sources/Library/Resources/Files/.dockerignore b/Library/Resources/Files/.dockerignore similarity index 100% rename from Sources/Library/Resources/Files/.dockerignore rename to Library/Resources/Files/.dockerignore diff --git a/Sources/Library/Resources/Files/.gitignore b/Library/Resources/Files/.gitignore similarity index 100% rename from Sources/Library/Resources/Files/.gitignore rename to Library/Resources/Files/.gitignore diff --git a/Sources/Library/Resources/Files/LICENSE b/Library/Resources/Files/LICENSE similarity index 100% rename from Sources/Library/Resources/Files/LICENSE rename to Library/Resources/Files/LICENSE diff --git a/Sources/Library/Resources/Files/Package b/Library/Resources/Files/Package similarity index 100% rename from Sources/Library/Resources/Files/Package rename to Library/Resources/Files/Package diff --git a/Sources/Library/Resources/Files/README b/Library/Resources/Files/README similarity index 100% rename from Sources/Library/Resources/Files/README rename to Library/Resources/Files/README diff --git a/Sources/Library/Extensions/URL+Extensions.swift b/Library/Sources/Extensions/URL+Extensions.swift similarity index 100% rename from Sources/Library/Extensions/URL+Extensions.swift rename to Library/Sources/Extensions/URL+Extensions.swift diff --git a/Sources/Library/Protocols/FileServicing.swift b/Library/Sources/Protocols/FileServicing.swift similarity index 100% rename from Sources/Library/Protocols/FileServicing.swift rename to Library/Sources/Protocols/FileServicing.swift diff --git a/Sources/Library/Services/FileService.swift b/Library/Sources/Services/FileService.swift similarity index 100% rename from Sources/Library/Services/FileService.swift rename to Library/Sources/Services/FileService.swift diff --git a/Sources/Library/Tasks/CopyFilesTask.swift b/Library/Sources/Tasks/CopyFilesTask.swift similarity index 100% rename from Sources/Library/Tasks/CopyFilesTask.swift rename to Library/Sources/Tasks/CopyFilesTask.swift diff --git a/Sources/Library/Tasks/CreateFoldersTask.swift b/Library/Sources/Tasks/CreateFoldersTask.swift similarity index 100% rename from Sources/Library/Tasks/CreateFoldersTask.swift rename to Library/Sources/Tasks/CreateFoldersTask.swift diff --git a/Sources/Library/Tasks/CreateRootFolderTask.swift b/Library/Sources/Tasks/CreateRootFolderTask.swift similarity index 100% rename from Sources/Library/Tasks/CreateRootFolderTask.swift rename to Library/Sources/Tasks/CreateRootFolderTask.swift diff --git a/Package.swift b/Package.swift index 67c92d0..5f0c129 100644 --- a/Package.swift +++ b/Package.swift @@ -38,9 +38,9 @@ let package = Package( .target( name: "ColibriLibrary", dependencies: [], - path: "Sources/Library", + path: "Library", resources: [ - .copy("Resources/Files/LICENSE") + .copy("Resources/Files") ] ), .testTarget( -- 2.47.1 From 640683063e873a2bd8a47f7d692cf1016ce89821 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 17 Jan 2025 23:23:10 +0100 Subject: [PATCH 37/80] Restructured the folder structure of the test target. --- Package.swift | 6 ++++-- .../Sources}/Cases/Extensions/URL+ExtensionsTests.swift | 0 .../Sources}/Cases/Services/FileServiceTests.swift | 0 .../Sources}/Cases/Tasks/CopyFilesTaskTests.swift | 0 .../Sources}/Cases/Tasks/CreateFoldersTaskTests.swift | 0 .../Sources}/Cases/Tasks/CreateRootFolderTaskTests.swift | 0 .../Sources}/Helpers/Extensions/URL+Samples.swift | 0 .../Sources}/Helpers/Mocks/FileServiceMock.swift | 0 .../Sources}/Helpers/Spies/FileServiceSpy.swift | 0 9 files changed, 4 insertions(+), 2 deletions(-) rename {Tests/Library => Test/Sources}/Cases/Extensions/URL+ExtensionsTests.swift (100%) rename {Tests/Library => Test/Sources}/Cases/Services/FileServiceTests.swift (100%) rename {Tests/Library => Test/Sources}/Cases/Tasks/CopyFilesTaskTests.swift (100%) rename {Tests/Library => Test/Sources}/Cases/Tasks/CreateFoldersTaskTests.swift (100%) rename {Tests/Library => Test/Sources}/Cases/Tasks/CreateRootFolderTaskTests.swift (100%) rename {Tests/Library => Test/Sources}/Helpers/Extensions/URL+Samples.swift (100%) rename {Tests/Library => Test/Sources}/Helpers/Mocks/FileServiceMock.swift (100%) rename {Tests/Library => Test/Sources}/Helpers/Spies/FileServiceSpy.swift (100%) diff --git a/Package.swift b/Package.swift index 5f0c129..5d26455 100644 --- a/Package.swift +++ b/Package.swift @@ -45,8 +45,10 @@ let package = Package( ), .testTarget( name: "ColibriTests", - dependencies: ["ColibriLibrary"], - path: "Tests/Library" + dependencies: [ + .target(name: "ColibriLibrary") + ], + path: "Test" ) ] ) diff --git a/Tests/Library/Cases/Extensions/URL+ExtensionsTests.swift b/Test/Sources/Cases/Extensions/URL+ExtensionsTests.swift similarity index 100% rename from Tests/Library/Cases/Extensions/URL+ExtensionsTests.swift rename to Test/Sources/Cases/Extensions/URL+ExtensionsTests.swift diff --git a/Tests/Library/Cases/Services/FileServiceTests.swift b/Test/Sources/Cases/Services/FileServiceTests.swift similarity index 100% rename from Tests/Library/Cases/Services/FileServiceTests.swift rename to Test/Sources/Cases/Services/FileServiceTests.swift diff --git a/Tests/Library/Cases/Tasks/CopyFilesTaskTests.swift b/Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift similarity index 100% rename from Tests/Library/Cases/Tasks/CopyFilesTaskTests.swift rename to Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift diff --git a/Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift b/Test/Sources/Cases/Tasks/CreateFoldersTaskTests.swift similarity index 100% rename from Tests/Library/Cases/Tasks/CreateFoldersTaskTests.swift rename to Test/Sources/Cases/Tasks/CreateFoldersTaskTests.swift diff --git a/Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift b/Test/Sources/Cases/Tasks/CreateRootFolderTaskTests.swift similarity index 100% rename from Tests/Library/Cases/Tasks/CreateRootFolderTaskTests.swift rename to Test/Sources/Cases/Tasks/CreateRootFolderTaskTests.swift diff --git a/Tests/Library/Helpers/Extensions/URL+Samples.swift b/Test/Sources/Helpers/Extensions/URL+Samples.swift similarity index 100% rename from Tests/Library/Helpers/Extensions/URL+Samples.swift rename to Test/Sources/Helpers/Extensions/URL+Samples.swift diff --git a/Tests/Library/Helpers/Mocks/FileServiceMock.swift b/Test/Sources/Helpers/Mocks/FileServiceMock.swift similarity index 100% rename from Tests/Library/Helpers/Mocks/FileServiceMock.swift rename to Test/Sources/Helpers/Mocks/FileServiceMock.swift diff --git a/Tests/Library/Helpers/Spies/FileServiceSpy.swift b/Test/Sources/Helpers/Spies/FileServiceSpy.swift similarity index 100% rename from Tests/Library/Helpers/Spies/FileServiceSpy.swift rename to Test/Sources/Helpers/Spies/FileServiceSpy.swift -- 2.47.1 From 6720464026dbc19b06434c1e9cea159aeb946730 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 17 Jan 2025 23:27:19 +0100 Subject: [PATCH 38/80] Renamed the project file assets in the library target. --- Library/Resources/Files/{.dockerignore => docker_ignore} | 0 Library/Resources/Files/{.gitignore => git_ignore} | 0 Library/Resources/Files/{LICENSE => license_apache} | 0 Library/Resources/Files/{Package => package} | 0 Library/Resources/Files/{README => readme} | 0 Package.swift | 2 +- 6 files changed, 1 insertion(+), 1 deletion(-) rename Library/Resources/Files/{.dockerignore => docker_ignore} (100%) rename Library/Resources/Files/{.gitignore => git_ignore} (100%) rename Library/Resources/Files/{LICENSE => license_apache} (100%) rename Library/Resources/Files/{Package => package} (100%) rename Library/Resources/Files/{README => readme} (100%) diff --git a/Library/Resources/Files/.dockerignore b/Library/Resources/Files/docker_ignore similarity index 100% rename from Library/Resources/Files/.dockerignore rename to Library/Resources/Files/docker_ignore diff --git a/Library/Resources/Files/.gitignore b/Library/Resources/Files/git_ignore similarity index 100% rename from Library/Resources/Files/.gitignore rename to Library/Resources/Files/git_ignore diff --git a/Library/Resources/Files/LICENSE b/Library/Resources/Files/license_apache similarity index 100% rename from Library/Resources/Files/LICENSE rename to Library/Resources/Files/license_apache diff --git a/Library/Resources/Files/Package b/Library/Resources/Files/package similarity index 100% rename from Library/Resources/Files/Package rename to Library/Resources/Files/package diff --git a/Library/Resources/Files/README b/Library/Resources/Files/readme similarity index 100% rename from Library/Resources/Files/README rename to Library/Resources/Files/readme diff --git a/Package.swift b/Package.swift index 5d26455..6e1ada2 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let package = Package( dependencies: [], path: "Library", resources: [ - .copy("Resources/Files") + .copy("Resources") ] ), .testTarget( -- 2.47.1 From 5a3654a68fe7b21715eb1af6101979a50fb26a0f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 00:18:11 +0100 Subject: [PATCH 39/80] Implemented the BundleServicing protocol in the library target. --- Library/Sources/Protocols/BundleServicing.swift | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Library/Sources/Protocols/BundleServicing.swift diff --git a/Library/Sources/Protocols/BundleServicing.swift b/Library/Sources/Protocols/BundleServicing.swift new file mode 100644 index 0000000..f5a4734 --- /dev/null +++ b/Library/Sources/Protocols/BundleServicing.swift @@ -0,0 +1,9 @@ +import Foundation + +public protocol BundleServicing { + + // MARK: Functions + + func url(forResource name: String?, withExtension ext: String?, subdirectory subpath: String?) -> URL? + +} -- 2.47.1 From af4958a5e8474fc9bfe2e15aa0935adc69c6f027 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 00:19:59 +0100 Subject: [PATCH 40/80] Conformed the Bundle representation of the Foundation framework to the BundleServicing protocol in the library target. --- Library/Sources/Extensions/Bundle+Conformances.swift | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Library/Sources/Extensions/Bundle+Conformances.swift diff --git a/Library/Sources/Extensions/Bundle+Conformances.swift b/Library/Sources/Extensions/Bundle+Conformances.swift new file mode 100644 index 0000000..1dcd2e6 --- /dev/null +++ b/Library/Sources/Extensions/Bundle+Conformances.swift @@ -0,0 +1,5 @@ +import Foundation + +// MARK: - BundleServicing + +extension Bundle: BundleServicing {} -- 2.47.1 From b8d5bea7ae42ba21c3bc7f7bef08a058188f88fc Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 01:07:52 +0100 Subject: [PATCH 41/80] Implemented the "copyFile(from: to: )" function for the FileService service in the library target. --- Library/Sources/Protocols/FileServicing.swift | 5 +++-- Library/Sources/Services/FileService.swift | 12 ++++++++++-- Test/Sources/Cases/Services/FileServiceTests.swift | 12 ++++++------ Test/Sources/Helpers/Mocks/FileServiceMock.swift | 8 ++++---- Test/Sources/Helpers/Spies/FileServiceSpy.swift | 6 +++--- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Library/Sources/Protocols/FileServicing.swift b/Library/Sources/Protocols/FileServicing.swift index 674499e..4de66e3 100644 --- a/Library/Sources/Protocols/FileServicing.swift +++ b/Library/Sources/Protocols/FileServicing.swift @@ -8,7 +8,7 @@ public protocol FileServicing { // MARK: Functions - func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) + 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 @@ -19,8 +19,9 @@ public protocol FileServicing { public enum FileServiceError: Error, Equatable { case folderNotCreated - case itemNotCopied case itemAlreadyExists + case itemEmptyData + case itemNotCopied case itemNotDeleted case itemNotExists case itemNotFileURL diff --git a/Library/Sources/Services/FileService.swift b/Library/Sources/Services/FileService.swift index 3df4be9..9269340 100644 --- a/Library/Sources/Services/FileService.swift +++ b/Library/Sources/Services/FileService.swift @@ -22,7 +22,7 @@ public struct FileService: FileServicing { // MARK: Functions - public func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) { + public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) { guard try await isItemExists(at: source) else { throw FileServiceError.itemNotExists } @@ -30,8 +30,16 @@ public struct FileService: FileServicing { throw FileServiceError.itemAlreadyExists } + var itemData: Data? + do { - try fileManager.copyItem(at: source, to: destination) + itemData = try Data(contentsOf: source) + } catch { + throw FileServiceError.itemEmptyData + } + + do { + try itemData?.write(to: destination, options: .atomic) } catch { throw FileServiceError.itemNotCopied } diff --git a/Test/Sources/Cases/Services/FileServiceTests.swift b/Test/Sources/Cases/Services/FileServiceTests.swift index 2edbc48..e66dd0b 100644 --- a/Test/Sources/Cases/Services/FileServiceTests.swift +++ b/Test/Sources/Cases/Services/FileServiceTests.swift @@ -28,26 +28,26 @@ struct FileServiceTests { @Test(arguments: zip([URL.someExistingFile, .someExistingFolder], [URL.someNewFile, .someNewFolder])) - func copyItem(from source: URL, to destination: URL) async throws { + func copyFile(from source: URL, to destination: URL) async throws { // GIVEN let service = FileServiceMock( currentFolder: .someCurrentFolder, - action: .copyItem(source, destination), + action: .copyFile(source, destination), spy: spy ) // WHEN - try await service.copyItem(from: source, to: destination) + try await service.copyFile(from: source, to: destination) // THENn #expect(spy.actions.count == 1) let action = try #require(spy.actions.last) - #expect(action == .itemCopied(source, destination)) + #expect(action == .fileCopied(source, destination)) } - @Test(arguments: [FileServiceError.itemNotExists, .itemAlreadyExists, .itemNotCopied]) + @Test(arguments: [FileServiceError.itemNotExists, .itemAlreadyExists, .itemEmptyData, .itemNotCopied]) func copyItem(throws error: FileServiceError) async throws { // GIVEN let service = FileServiceMock( @@ -59,7 +59,7 @@ struct FileServiceTests { // WHEN // THEN await #expect(throws: error) { - try await service.copyItem(from: .someExistingFile, to: .someNewFile) + try await service.copyFile(from: .someExistingFile, to: .someNewFile) } #expect(spy.actions.isEmpty == true) diff --git a/Test/Sources/Helpers/Mocks/FileServiceMock.swift b/Test/Sources/Helpers/Mocks/FileServiceMock.swift index 8d11b64..99bd117 100644 --- a/Test/Sources/Helpers/Mocks/FileServiceMock.swift +++ b/Test/Sources/Helpers/Mocks/FileServiceMock.swift @@ -51,14 +51,14 @@ extension FileServiceMock: FileServicing { // MARK: Functions - func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) { + func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) { guard let nextAction else { return } switch nextAction { case .error(let error): throw error - case let .copyItem(source, destination): - try await spy?.copyItem(from: source, to: destination) + case let .copyFile(source, destination): + try await spy?.copyFile(from: source, to: destination) default: break } @@ -126,7 +126,7 @@ private extension FileServiceMock { extension FileServiceMock { enum Action { - case copyItem(URL, URL) + case copyFile(URL, URL) case createFolder(URL) case deleteItem(URL) case error(FileServiceError) diff --git a/Test/Sources/Helpers/Spies/FileServiceSpy.swift b/Test/Sources/Helpers/Spies/FileServiceSpy.swift index 3b21653..34ccd63 100644 --- a/Test/Sources/Helpers/Spies/FileServiceSpy.swift +++ b/Test/Sources/Helpers/Spies/FileServiceSpy.swift @@ -18,8 +18,8 @@ extension FileServiceSpy: FileServicing { get async { .someCurrentFolder } } - func copyItem(from source: URL, to destination: URL) async throws (FileServiceError) { - actions.append(.itemCopied(source, destination)) + func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) { + actions.append(.fileCopied(source, destination)) } func createFolder(at location: URL) async throws (FileServiceError) { @@ -43,8 +43,8 @@ extension FileServiceSpy: FileServicing { extension FileServiceSpy { enum Action: Equatable { + case fileCopied(_ source: URL, _ destination: URL) case folderCreated(_ location: URL) - case itemCopied(_ source: URL, _ destination: URL) case itemDeleted(_ location: URL) case itemExists(_ location: URL) } -- 2.47.1 From 647c5bd32a24f0ccbec4a49510692f20f3656377 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 01:45:29 +0100 Subject: [PATCH 42/80] Defined the ResourceFile enumeration in the library target and also, implemented its "fileName" computed property. --- .../Files/{license_apache => license} | 0 .../Sources/Enumerations/ResourceFile.swift | 29 +++++++++++++++++++ .../Enumerations/ResourceFileTests.swift | 20 +++++++++++++ 3 files changed, 49 insertions(+) rename Library/Resources/Files/{license_apache => license} (100%) create mode 100644 Library/Sources/Enumerations/ResourceFile.swift create mode 100644 Test/Sources/Cases/Enumerations/ResourceFileTests.swift diff --git a/Library/Resources/Files/license_apache b/Library/Resources/Files/license similarity index 100% rename from Library/Resources/Files/license_apache rename to Library/Resources/Files/license diff --git a/Library/Sources/Enumerations/ResourceFile.swift b/Library/Sources/Enumerations/ResourceFile.swift new file mode 100644 index 0000000..7180349 --- /dev/null +++ b/Library/Sources/Enumerations/ResourceFile.swift @@ -0,0 +1,29 @@ +enum ResourceFile: String { + case dockerIgnore = "docker_ignore" + case gitIgnore = "git_ignore" + case license + case package + case readme +} + +// MARK: - Properties + +extension ResourceFile { + + // MARK: Computed + + var fileName: String { + switch self { + case .dockerIgnore: return ".dockerignore" + case .gitIgnore: return ".gitignore" + case .license: return "LICENSE" + case .readme: return "README.md" + case .package: return "Package.swift" + } + } + +} + +// MARK: - CaseIterable + +extension ResourceFile: CaseIterable {} diff --git a/Test/Sources/Cases/Enumerations/ResourceFileTests.swift b/Test/Sources/Cases/Enumerations/ResourceFileTests.swift new file mode 100644 index 0000000..13a176c --- /dev/null +++ b/Test/Sources/Cases/Enumerations/ResourceFileTests.swift @@ -0,0 +1,20 @@ +import Testing + +@testable import ColibriLibrary + +struct ResourceFileTests { + + // MARK: Properties tests + + @Test(arguments: zip(ResourceFile.allCases, + [".dockerignore", ".gitignore", "LICENSE", "Package.swift", "README.md"])) + func fileName(for resource: ResourceFile, expects fileName: String) async throws { + // GIVEN + // WHEN + let result = resource.fileName + + // THEN + #expect(result == fileName) + } + +} -- 2.47.1 From 1466bff2508c127561c1bf04c2ebf3aad8078879 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 03:06:39 +0100 Subject: [PATCH 43/80] Implemented the CopyFilesTask task in the library module. --- .../Sources/Extensions/URL+Extensions.swift | 2 +- Library/Sources/Services/FileService.swift | 3 - Library/Sources/Tasks/CopyFilesTask.swift | 54 ++++++--------- Library/Sources/Tasks/CreateFoldersTask.swift | 17 ++--- .../Cases/Services/FileServiceTests.swift | 2 +- .../Cases/Tasks/CopyFilesTaskTests.swift | 67 ++++++++++++++----- 6 files changed, 78 insertions(+), 67 deletions(-) diff --git a/Library/Sources/Extensions/URL+Extensions.swift b/Library/Sources/Extensions/URL+Extensions.swift index 04b5999..01d4ea3 100644 --- a/Library/Sources/Extensions/URL+Extensions.swift +++ b/Library/Sources/Extensions/URL+Extensions.swift @@ -16,7 +16,7 @@ extension URL { var pathString: String { if #available(macOS 13.0, *) { - path() + path(percentEncoded: true) } else { path } diff --git a/Library/Sources/Services/FileService.swift b/Library/Sources/Services/FileService.swift index 9269340..d7e1fdb 100644 --- a/Library/Sources/Services/FileService.swift +++ b/Library/Sources/Services/FileService.swift @@ -23,9 +23,6 @@ public struct FileService: FileServicing { // MARK: Functions public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) { - guard try await isItemExists(at: source) else { - throw FileServiceError.itemNotExists - } guard try await !isItemExists(at: destination) else { throw FileServiceError.itemAlreadyExists } diff --git a/Library/Sources/Tasks/CopyFilesTask.swift b/Library/Sources/Tasks/CopyFilesTask.swift index 182f19a..f91212e 100644 --- a/Library/Sources/Tasks/CopyFilesTask.swift +++ b/Library/Sources/Tasks/CopyFilesTask.swift @@ -4,50 +4,36 @@ public struct CopyFilesTask { // MARK: Properties + private let bundleService: BundleServicing private let fileService: FileServicing // MARK: Initialisers - public init(fileService: FileServicing) { + public init( + bundleService: BundleServicing? = nil, + fileService: FileServicing + ) { + self.bundleService = bundleService ?? Bundle.module self.fileService = fileService } // MARK: Functions - public func callAsFunction(to rootFolder: URL) async throws { - let filesFolder = URL(at: .folderFiles) - - for fileToCopy in Self.filesToCopy { - try await fileService.copyItem( - from: filesFolder.appendingPath(fileToCopy), - to: rootFolder.appendingPath(fileToCopy) - ) + public func callAsFunction(to rootFolder: URL) async throws (FileServiceError) { + for resource in ResourceFile.allCases { + guard let source = bundleService.url( + forResource: resource.rawValue, + withExtension: nil, + subdirectory: "Resources/Files" + ) else { + assertionFailure("URL should have been initialized.") + return + } + + let destination = rootFolder.appendingPath(resource.fileName) + + try await fileService.copyFile(from: source, to: destination) } } } - -// MARK: - Helpers - -extension CopyFilesTask { - - // MARK: Constants - - static let filesToCopy: [String] = [ - .fileDockerIgnore, - .fileGitIgnore, - .fileLicense, - .fileReadme - ] - -} - -// MARK: - URL+Constants - -private extension String { - static let folderFiles = "./Resources/Files" - static let fileDockerIgnore = ".dockerignore" - static let fileGitIgnore = ".gitignore" - static let fileLicense = "LICENSE" - static let fileReadme = "README.md" -} diff --git a/Library/Sources/Tasks/CreateFoldersTask.swift b/Library/Sources/Tasks/CreateFoldersTask.swift index a0a3531..2db9de9 100644 --- a/Library/Sources/Tasks/CreateFoldersTask.swift +++ b/Library/Sources/Tasks/CreateFoldersTask.swift @@ -31,19 +31,10 @@ extension CreateFoldersTask { // MARK: Constants static let foldersToCreate: [String] = [ - .folderApp, - .folderAppInfrastructure, - .folderAppTestCases, - .folderAppTestSources + "Sources/App", + "Sources/AppInfrastructure", + "Tests/App/Cases", + "Tests/App/Sources" ] } - -// MARK: - String+Constants - -private extension String { - static let folderApp = "Sources/App" - static let folderAppInfrastructure = "Sources/AppInfrastructure" - static let folderAppTestCases = "Tests/App/Cases" - static let folderAppTestSources = "Tests/App/Sources" -} diff --git a/Test/Sources/Cases/Services/FileServiceTests.swift b/Test/Sources/Cases/Services/FileServiceTests.swift index e66dd0b..7cc9e5f 100644 --- a/Test/Sources/Cases/Services/FileServiceTests.swift +++ b/Test/Sources/Cases/Services/FileServiceTests.swift @@ -47,7 +47,7 @@ struct FileServiceTests { #expect(action == .fileCopied(source, destination)) } - @Test(arguments: [FileServiceError.itemNotExists, .itemAlreadyExists, .itemEmptyData, .itemNotCopied]) + @Test(arguments: [FileServiceError.itemAlreadyExists, .itemEmptyData, .itemNotCopied]) func copyItem(throws error: FileServiceError) async throws { // GIVEN let service = FileServiceMock( diff --git a/Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift b/Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift index b473717..fc1a18e 100644 --- a/Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift +++ b/Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift @@ -7,37 +7,74 @@ struct CopyFilesTaskTests { // MARK: Properties + private let resourceFolder = URL.someExistingFolder + private let rootFolder = URL.someNewFolder + private let spy = FileServiceSpy() // MARK: Functions tests - @Test(arguments: zip([URL.someExistingFolder], [URL.someNewFolder])) - func copyFiles(from source: URL, to destination: URL) async throws { + @Test func copyFiles() async throws { // GIVEN - let filesToCopy = CopyFilesTask.filesToCopy - let destinations = filesToCopy.map { destination.appendingPath($0) } - let sources = filesToCopy.map { source.appendingPath($0) } - let actions = filesToCopy.indices.map { index -> FileServiceMock.Action in - .copyItem(sources[index], destinations[index]) - } + let files = files(of: ResourceFile.allCases) + let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) } - let service = FileServiceMock( + let copyFiles = CopyFilesTask(fileService: FileServiceMock( currentFolder: .someCurrentFolder, actions: actions, spy: spy - ) - - let copyFiles = CopyFilesTask(fileService: service) + )) // WHEN - try await copyFiles(to: destination) + try await copyFiles(to: rootFolder) // THEN #expect(spy.actions.count == actions.count) - for index in actions.indices { - #expect(spy.actions[index] == .itemCopied(sources[index], destinations[index])) + files.enumerated().forEach { index, file in + #expect(spy.actions[index] == .fileCopied(file.source, file.destination)) + } + } + + @Test(arguments: [FileServiceError.itemAlreadyExists, .itemEmptyData, .itemNotCopied]) + func copyFiles(throws error: FileServiceError) async throws { + // GIVEN + let files = files(of: Array(ResourceFile.allCases[0...2])) + let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) } + + let copyFiles = CopyFilesTask(fileService: FileServiceMock( + currentFolder: .someCurrentFolder, + actions: actions + [.error(error)], + spy: spy + )) + + // WHEN + // THEN + await #expect(throws: error) { + try await copyFiles(to: rootFolder) + } + + #expect(spy.actions.count == actions.count) + + files.enumerated().forEach { index, file in + #expect(spy.actions[index] == .fileCopied(file.source, file.destination)) } } } + +// MARK: - Helpers + +private extension CopyFilesTaskTests { + + // MARK: Type aliases + + typealias File = (source: URL, destination: URL) + + // MARK: Functions + + func files(of resourceFiles: [ResourceFile]) -> [File] { + resourceFiles.map { (resourceFolder.appendingPath($0.rawValue), rootFolder.appendingPath($0.fileName)) } + } + +} -- 2.47.1 From 1094bbb6c8eacda53d6fe70aaf1a8f60e835370e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 03:50:18 +0100 Subject: [PATCH 44/80] Restructured the folder structure in the library and test targets. --- Library/Sources/{ => Internal}/Enumerations/ResourceFile.swift | 0 .../Sources/{ => Internal}/Extensions/Bundle+Conformances.swift | 0 Library/Sources/{ => Internal}/Extensions/URL+Extensions.swift | 0 Library/Sources/{ => Public}/Protocols/BundleServicing.swift | 0 Library/Sources/{ => Public}/Protocols/FileServicing.swift | 0 Library/Sources/{ => Public}/Services/FileService.swift | 0 Library/Sources/{ => Public}/Tasks/CopyFilesTask.swift | 0 Library/Sources/{ => Public}/Tasks/CreateFoldersTask.swift | 0 Library/Sources/{ => Public}/Tasks/CreateRootFolderTask.swift | 0 .../Cases/{ => Internal}/Enumerations/ResourceFileTests.swift | 0 .../Cases/{ => Internal}/Extensions/URL+ExtensionsTests.swift | 0 Test/Sources/Cases/{ => Public}/Services/FileServiceTests.swift | 0 Test/Sources/Cases/{ => Public}/Tasks/CopyFilesTaskTests.swift | 0 .../Sources/Cases/{ => Public}/Tasks/CreateFoldersTaskTests.swift | 0 .../Cases/{ => Public}/Tasks/CreateRootFolderTaskTests.swift | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename Library/Sources/{ => Internal}/Enumerations/ResourceFile.swift (100%) rename Library/Sources/{ => Internal}/Extensions/Bundle+Conformances.swift (100%) rename Library/Sources/{ => Internal}/Extensions/URL+Extensions.swift (100%) rename Library/Sources/{ => Public}/Protocols/BundleServicing.swift (100%) rename Library/Sources/{ => Public}/Protocols/FileServicing.swift (100%) rename Library/Sources/{ => Public}/Services/FileService.swift (100%) rename Library/Sources/{ => Public}/Tasks/CopyFilesTask.swift (100%) rename Library/Sources/{ => Public}/Tasks/CreateFoldersTask.swift (100%) rename Library/Sources/{ => Public}/Tasks/CreateRootFolderTask.swift (100%) rename Test/Sources/Cases/{ => Internal}/Enumerations/ResourceFileTests.swift (100%) rename Test/Sources/Cases/{ => Internal}/Extensions/URL+ExtensionsTests.swift (100%) rename Test/Sources/Cases/{ => Public}/Services/FileServiceTests.swift (100%) rename Test/Sources/Cases/{ => Public}/Tasks/CopyFilesTaskTests.swift (100%) rename Test/Sources/Cases/{ => Public}/Tasks/CreateFoldersTaskTests.swift (100%) rename Test/Sources/Cases/{ => Public}/Tasks/CreateRootFolderTaskTests.swift (100%) diff --git a/Library/Sources/Enumerations/ResourceFile.swift b/Library/Sources/Internal/Enumerations/ResourceFile.swift similarity index 100% rename from Library/Sources/Enumerations/ResourceFile.swift rename to Library/Sources/Internal/Enumerations/ResourceFile.swift diff --git a/Library/Sources/Extensions/Bundle+Conformances.swift b/Library/Sources/Internal/Extensions/Bundle+Conformances.swift similarity index 100% rename from Library/Sources/Extensions/Bundle+Conformances.swift rename to Library/Sources/Internal/Extensions/Bundle+Conformances.swift diff --git a/Library/Sources/Extensions/URL+Extensions.swift b/Library/Sources/Internal/Extensions/URL+Extensions.swift similarity index 100% rename from Library/Sources/Extensions/URL+Extensions.swift rename to Library/Sources/Internal/Extensions/URL+Extensions.swift diff --git a/Library/Sources/Protocols/BundleServicing.swift b/Library/Sources/Public/Protocols/BundleServicing.swift similarity index 100% rename from Library/Sources/Protocols/BundleServicing.swift rename to Library/Sources/Public/Protocols/BundleServicing.swift diff --git a/Library/Sources/Protocols/FileServicing.swift b/Library/Sources/Public/Protocols/FileServicing.swift similarity index 100% rename from Library/Sources/Protocols/FileServicing.swift rename to Library/Sources/Public/Protocols/FileServicing.swift diff --git a/Library/Sources/Services/FileService.swift b/Library/Sources/Public/Services/FileService.swift similarity index 100% rename from Library/Sources/Services/FileService.swift rename to Library/Sources/Public/Services/FileService.swift diff --git a/Library/Sources/Tasks/CopyFilesTask.swift b/Library/Sources/Public/Tasks/CopyFilesTask.swift similarity index 100% rename from Library/Sources/Tasks/CopyFilesTask.swift rename to Library/Sources/Public/Tasks/CopyFilesTask.swift diff --git a/Library/Sources/Tasks/CreateFoldersTask.swift b/Library/Sources/Public/Tasks/CreateFoldersTask.swift similarity index 100% rename from Library/Sources/Tasks/CreateFoldersTask.swift rename to Library/Sources/Public/Tasks/CreateFoldersTask.swift diff --git a/Library/Sources/Tasks/CreateRootFolderTask.swift b/Library/Sources/Public/Tasks/CreateRootFolderTask.swift similarity index 100% rename from Library/Sources/Tasks/CreateRootFolderTask.swift rename to Library/Sources/Public/Tasks/CreateRootFolderTask.swift diff --git a/Test/Sources/Cases/Enumerations/ResourceFileTests.swift b/Test/Sources/Cases/Internal/Enumerations/ResourceFileTests.swift similarity index 100% rename from Test/Sources/Cases/Enumerations/ResourceFileTests.swift rename to Test/Sources/Cases/Internal/Enumerations/ResourceFileTests.swift diff --git a/Test/Sources/Cases/Extensions/URL+ExtensionsTests.swift b/Test/Sources/Cases/Internal/Extensions/URL+ExtensionsTests.swift similarity index 100% rename from Test/Sources/Cases/Extensions/URL+ExtensionsTests.swift rename to Test/Sources/Cases/Internal/Extensions/URL+ExtensionsTests.swift diff --git a/Test/Sources/Cases/Services/FileServiceTests.swift b/Test/Sources/Cases/Public/Services/FileServiceTests.swift similarity index 100% rename from Test/Sources/Cases/Services/FileServiceTests.swift rename to Test/Sources/Cases/Public/Services/FileServiceTests.swift diff --git a/Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift similarity index 100% rename from Test/Sources/Cases/Tasks/CopyFilesTaskTests.swift rename to Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift diff --git a/Test/Sources/Cases/Tasks/CreateFoldersTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift similarity index 100% rename from Test/Sources/Cases/Tasks/CreateFoldersTaskTests.swift rename to Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift diff --git a/Test/Sources/Cases/Tasks/CreateRootFolderTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CreateRootFolderTaskTests.swift similarity index 100% rename from Test/Sources/Cases/Tasks/CreateRootFolderTaskTests.swift rename to Test/Sources/Cases/Public/Tasks/CreateRootFolderTaskTests.swift -- 2.47.1 From 9ee75929020320d85e9e13ed6eb700ffb7c3b279 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 04:03:07 +0100 Subject: [PATCH 45/80] Refactored some test cases in the tests target. --- .../Public/Services/FileServiceTests.swift | 64 +++++++------------ .../Public/Tasks/CopyFilesTaskTests.swift | 20 +++--- .../Tasks/CreateRootFolderTaskTests.swift | 18 ++---- 3 files changed, 41 insertions(+), 61 deletions(-) diff --git a/Test/Sources/Cases/Public/Services/FileServiceTests.swift b/Test/Sources/Cases/Public/Services/FileServiceTests.swift index 7cc9e5f..bf8f909 100644 --- a/Test/Sources/Cases/Public/Services/FileServiceTests.swift +++ b/Test/Sources/Cases/Public/Services/FileServiceTests.swift @@ -30,11 +30,7 @@ struct FileServiceTests { [URL.someNewFile, .someNewFolder])) func copyFile(from source: URL, to destination: URL) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .copyFile(source, destination), - spy: spy - ) + let service = service(action: .copyFile(source, destination)) // WHEN try await service.copyFile(from: source, to: destination) @@ -50,11 +46,7 @@ struct FileServiceTests { @Test(arguments: [FileServiceError.itemAlreadyExists, .itemEmptyData, .itemNotCopied]) func copyItem(throws error: FileServiceError) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .error(error), - spy: spy - ) + let service = service(action: .error(error)) // WHEN // THEN @@ -68,11 +60,7 @@ struct FileServiceTests { @Test(arguments: [URL.someNewFolder, .someNewFile]) func createFolder(with location: URL) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .createFolder(location), - spy: spy - ) + let service = service(action: .createFolder(location)) // WHEN try await service.createFolder(at: location) @@ -92,11 +80,7 @@ struct FileServiceTests { throws error: FileServiceError ) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .error(error), - spy: spy - ) + let service = service(action: .error(error)) // WHEN // THEN @@ -110,11 +94,7 @@ struct FileServiceTests { @Test(arguments: [URL.someNewFolder, .someNewFile]) func deleteItem(with location: URL) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .deleteItem(location), - spy: spy - ) + let service = service(action: .deleteItem(location)) // WHEN try await service.deleteItem(at: location) @@ -134,11 +114,7 @@ struct FileServiceTests { throws error: FileServiceError ) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .error(error), - spy: spy - ) + let service = service(action: .error(error)) // WHEN // THEN @@ -156,11 +132,7 @@ struct FileServiceTests { expects outcome: Bool ) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .isItemExists(location, outcome), - spy: spy - ) + let service = service(action: .isItemExists(location, outcome)) // WHEN let result = try await service.isItemExists(at: location) @@ -179,11 +151,7 @@ struct FileServiceTests { throws error: FileServiceError ) async throws { // GIVEN - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - action: .error(error), - spy: spy - ) + let service = service(action: .error(error)) // WHEN // THEN @@ -195,3 +163,19 @@ struct FileServiceTests { } } + +// MARK: - Helpers + +private extension FileServiceTests { + + // MARK: Functions + + func service(action: FileServiceMock.Action) -> FileServiceMock { + .init( + currentFolder: .someCurrentFolder, + action: action, + spy: spy + ) + } + +} diff --git a/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift index fc1a18e..782ae7f 100644 --- a/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift +++ b/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift @@ -19,11 +19,7 @@ struct CopyFilesTaskTests { let files = files(of: ResourceFile.allCases) let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) } - let copyFiles = CopyFilesTask(fileService: FileServiceMock( - currentFolder: .someCurrentFolder, - actions: actions, - spy: spy - )) + let copyFiles = task(actions: actions) // WHEN try await copyFiles(to: rootFolder) @@ -42,11 +38,7 @@ struct CopyFilesTaskTests { let files = files(of: Array(ResourceFile.allCases[0...2])) let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) } - let copyFiles = CopyFilesTask(fileService: FileServiceMock( - currentFolder: .someCurrentFolder, - actions: actions + [.error(error)], - spy: spy - )) + let copyFiles = task(actions: actions + [.error(error)]) // WHEN // THEN @@ -76,5 +68,13 @@ private extension CopyFilesTaskTests { func files(of resourceFiles: [ResourceFile]) -> [File] { resourceFiles.map { (resourceFolder.appendingPath($0.rawValue), rootFolder.appendingPath($0.fileName)) } } + + func task(actions: [FileServiceMock.Action]) -> CopyFilesTask { + .init(fileService: FileServiceMock( + currentFolder: .someCurrentFolder, + actions: actions, + spy: spy + )) + } } diff --git a/Test/Sources/Cases/Public/Tasks/CreateRootFolderTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CreateRootFolderTaskTests.swift index 1b9b5f1..2ba499d 100644 --- a/Test/Sources/Cases/Public/Tasks/CreateRootFolderTaskTests.swift +++ b/Test/Sources/Cases/Public/Tasks/CreateRootFolderTaskTests.swift @@ -20,12 +20,10 @@ struct CreateRootFolderTaskTests { default: nil } - let fileService = FileServiceMock( + let task = CreateRootFolderTask(fileService: FileServiceMock( currentFolder: .someCurrentFolder, action: .createFolder(folder) - ) - - let task = CreateRootFolderTask(fileService: fileService) + )) // WHEN let result = try await task(name: name, @@ -42,12 +40,10 @@ struct CreateRootFolderTaskTests { throws error: FileServiceError ) async throws { // GIVEN - let fileService = FileServiceMock( + let task = CreateRootFolderTask(fileService: FileServiceMock( currentFolder: .someCurrentFolder, action: .error(error) - ) - - let task = CreateRootFolderTask(fileService: fileService) + )) // WHEN // THEN @@ -62,9 +58,9 @@ struct CreateRootFolderTaskTests { throws error: CreateRootFolderError ) async throws { // GIVEN - let fileService = FileServiceMock(currentFolder: .someCurrentFolder) - - let task = CreateRootFolderTask(fileService: fileService) + let task = CreateRootFolderTask(fileService: FileServiceMock( + currentFolder: .someCurrentFolder + )) // WHEN // THEN -- 2.47.1 From 026251ad6d11a6725fe65244642fbdf842fd0599 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 04:25:01 +0100 Subject: [PATCH 46/80] Improved the implementation for the CreateFolderTask task in the library target. --- .../Internal/Enumerations/Folder.swift | 12 ++++++++ .../Public/Tasks/CreateFoldersTask.swift | 17 +---------- .../Public/Tasks/CreateFoldersTaskTests.swift | 30 +++++++++++++------ 3 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 Library/Sources/Internal/Enumerations/Folder.swift diff --git a/Library/Sources/Internal/Enumerations/Folder.swift b/Library/Sources/Internal/Enumerations/Folder.swift new file mode 100644 index 0000000..ce66129 --- /dev/null +++ b/Library/Sources/Internal/Enumerations/Folder.swift @@ -0,0 +1,12 @@ +enum Folder: String { + case app = "App/Sources" + case libraryPublic = "Library/Sources/Public" + case libraryInternal = "Library/Sources/Internal" + case testCasesPublic = "Test/Sources/Cases/Public" + case testCasesInternal = "Test/Sources/Cases/Internal" + case testHelpers = "Test/Sources/Helpers" +} + +// MARK: - CaseIterable + +extension Folder: CaseIterable {} diff --git a/Library/Sources/Public/Tasks/CreateFoldersTask.swift b/Library/Sources/Public/Tasks/CreateFoldersTask.swift index 2db9de9..3895fe6 100644 --- a/Library/Sources/Public/Tasks/CreateFoldersTask.swift +++ b/Library/Sources/Public/Tasks/CreateFoldersTask.swift @@ -15,7 +15,7 @@ public struct CreateFoldersTask { // MARK: Functions public func callAsFunction(at rootFolder: URL) async throws { - let folders = Self.foldersToCreate.map { rootFolder.appendingPath($0) } + let folders = Folder.allCases.map { rootFolder.appendingPath($0.rawValue) } for folder in folders { try await fileService.createFolder(at: folder) @@ -23,18 +23,3 @@ public struct CreateFoldersTask { } } - -// MARK: - Helpers - -extension CreateFoldersTask { - - // MARK: Constants - - static let foldersToCreate: [String] = [ - "Sources/App", - "Sources/AppInfrastructure", - "Tests/App/Cases", - "Tests/App/Sources" - ] - -} diff --git a/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift index 396ac89..ddbfa29 100644 --- a/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift +++ b/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift @@ -14,24 +14,36 @@ struct CreateFoldersTaskTests { @Test(arguments: [URL.someCurrentFolder, .someDotFolder, .someTildeFolder]) func createFolders(with rootFolder: URL) async throws { // GIVEN - let folders = CreateFoldersTask.foldersToCreate.map { rootFolder.appendingPath($0) } - let actions: [FileServiceMock.Action] = folders.map { .createFolder($0) } + let folders = Folder.allCases.map { rootFolder.appendingPath($0.rawValue) } + let actions = folders.map { FileServiceMock.Action.createFolder($0) } - let service = FileServiceMock( - currentFolder: .someCurrentFolder, - actions: actions, - spy: spy - ) - - let createFolders = CreateFoldersTask(fileService: service) + let createFolders = task(actions: actions) // WHEN try await createFolders(at: rootFolder) // THEN + #expect(spy.actions.count == actions.count) + for index in actions.indices { #expect(spy.actions[index] == .folderCreated(folders[index])) } } } + +// MARK: - Helpers + +private extension CreateFoldersTaskTests { + + // MARK: Functions + + func task(actions: [FileServiceMock.Action]) -> CreateFoldersTask { + .init(fileService: FileServiceMock( + currentFolder: .someCurrentFolder, + actions: actions, + spy: spy + )) + } + +} -- 2.47.1 From 4d55fadfb86df71fa0ed48a0f225cc1d409ced15 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 04:45:58 +0100 Subject: [PATCH 47/80] Improved the Package file from the project. --- Package.swift | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/Package.swift b/Package.swift index 6e1ada2..9a07581 100644 --- a/Package.swift +++ b/Package.swift @@ -8,29 +8,17 @@ let package = Package( .macOS(.v10_15) ], products: [ - .executable( - name: "colibri", - targets: ["Colibri"] - ), - .library( - name: "ColibriLibrary", - targets: ["ColibriLibrary"] - ) + .executable(name: "colibri", targets: ["Colibri"]), + .library(name: "ColibriLibrary", targets: ["ColibriLibrary"]) ], dependencies: [ - .package( - url: "https://github.com/apple/swift-argument-parser", - from: "1.0.0" - ) + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") ], targets: [ .executableTarget( name: "Colibri", dependencies: [ - .product( - name: "ArgumentParser", - package: "swift-argument-parser" - ), + .product(name: "ArgumentParser", package: "swift-argument-parser"), .target(name: "ColibriLibrary") ], path: "Executable" -- 2.47.1 From 504b3be447053dcf9b27d11cd5c02b657824904e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 04:46:59 +0100 Subject: [PATCH 48/80] Fixed the package resource file in the library target. --- Library/Resources/Files/package | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Library/Resources/Files/package b/Library/Resources/Files/package index 8043ab3..a06a7a1 100644 --- a/Library/Resources/Files/package +++ b/Library/Resources/Files/package @@ -3,13 +3,13 @@ import PackageDescription let package = Package( - name: "{{HB_PACKAGE_NAME}}", + name: "App", platforms: [ .macOS(.v10_15) ], products: [ .executable(name: "app", targets: ["App"]), - .library(name: "AppInfrastructure", targets: ["AppInfrastructure"]) + .library(name: "AppLibrary", targets: ["AppLibrary"]) ], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), @@ -19,29 +19,27 @@ let package = Package( .executableTarget( name: "App", dependencies: [ - .byName(name: "AppInfrastructure"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird"), + .target(name: "AppLibrary") + ], + path: "App" + ), + .target( + name: "AppLibrary", + dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Hummingbird", package: "hummingbird") ], - path: "Sources/App" - ), - .target( - name: "AppInfrastructure", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Hummingbird", package: "hummingbird"), - ], - path: "Sources/AppInfrastructure" + path: "Library" ), .testTarget( name: "AppTests", dependencies: [ - dependencies: [ - .byName(name: "AppInfrastructure"), - .product(name: "HummingbirdTesting", package: "hummingbird") - ] + .product(name: "HummingbirdTesting", package: "hummingbird"), + .target(name: "AppLibrary") ], - path: "Tests/App" + path: "Test" ) ] ) -- 2.47.1 From 720ad687fbc4a4205cb6fa5a470bd153b97ea606 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 11:38:01 +0100 Subject: [PATCH 49/80] Implemented the "path" property for the Folder enumeration in the library target. --- .../Internal/Enumerations/Folder.swift | 33 ++++++++++++++---- .../Public/Tasks/CreateFoldersTask.swift | 2 +- .../Internal/Enumerations/FolderTests.swift | 34 +++++++++++++++++++ .../Public/Tasks/CreateFoldersTaskTests.swift | 2 +- 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 Test/Sources/Cases/Internal/Enumerations/FolderTests.swift diff --git a/Library/Sources/Internal/Enumerations/Folder.swift b/Library/Sources/Internal/Enumerations/Folder.swift index ce66129..fc9c448 100644 --- a/Library/Sources/Internal/Enumerations/Folder.swift +++ b/Library/Sources/Internal/Enumerations/Folder.swift @@ -1,10 +1,29 @@ -enum Folder: String { - case app = "App/Sources" - case libraryPublic = "Library/Sources/Public" - case libraryInternal = "Library/Sources/Internal" - case testCasesPublic = "Test/Sources/Cases/Public" - case testCasesInternal = "Test/Sources/Cases/Internal" - case testHelpers = "Test/Sources/Helpers" +enum Folder { + case app + case libraryPublic + case libraryInternal + 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 .testCasesPublic: "Test/Sources/Cases/Public" + case .testCasesInternal: "Test/Sources/Cases/Internal" + case .testHelpers: "Test/Sources/Helpers" + } + } + } // MARK: - CaseIterable diff --git a/Library/Sources/Public/Tasks/CreateFoldersTask.swift b/Library/Sources/Public/Tasks/CreateFoldersTask.swift index 3895fe6..2a39327 100644 --- a/Library/Sources/Public/Tasks/CreateFoldersTask.swift +++ b/Library/Sources/Public/Tasks/CreateFoldersTask.swift @@ -15,7 +15,7 @@ public struct CreateFoldersTask { // MARK: Functions public func callAsFunction(at rootFolder: URL) async throws { - let folders = Folder.allCases.map { rootFolder.appendingPath($0.rawValue) } + let folders = Folder.allCases.map { rootFolder.appendingPath($0.path) } for folder in folders { try await fileService.createFolder(at: folder) diff --git a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift new file mode 100644 index 0000000..31196f5 --- /dev/null +++ b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift @@ -0,0 +1,34 @@ +import Testing + +@testable import ColibriLibrary + +struct FolderTests { + + // MARK: Properties tests + + @Test(arguments: zip(Folder.allCases, Expectation.paths)) + func paths(for folder: Folder, expects path: String) async throws { + // GIVEN + // WHEN + let result = folder.path + + // THEN + #expect(result == path) + } + +} + +// MARK: - Expectations + +private extension FolderTests { + enum Expectation { + static let paths: [String] = [ + "App/Sources", + "Library/Sources/Public", + "Library/Sources/Internal", + "Test/Sources/Cases/Public", + "Test/Sources/Cases/Internal", + "Test/Sources/Helpers" + ] + } +} diff --git a/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift index ddbfa29..af361a4 100644 --- a/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift +++ b/Test/Sources/Cases/Public/Tasks/CreateFoldersTaskTests.swift @@ -14,7 +14,7 @@ struct CreateFoldersTaskTests { @Test(arguments: [URL.someCurrentFolder, .someDotFolder, .someTildeFolder]) func createFolders(with rootFolder: URL) async throws { // GIVEN - let folders = Folder.allCases.map { rootFolder.appendingPath($0.rawValue) } + let folders = Folder.allCases.map { rootFolder.appendingPath($0.path) } let actions = folders.map { FileServiceMock.Action.createFolder($0) } let createFolders = task(actions: actions) -- 2.47.1 From f558465b625dcbeae51d91e99dcf04e02874908f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 11:50:26 +0100 Subject: [PATCH 50/80] Renamed the ResourceFile enumeration in the library target as File and also, implemented its "filePath" and "resourcePath" properties. --- .../Sources/Internal/Enumerations/File.swift | 41 +++++++++++ .../Internal/Enumerations/ResourceFile.swift | 29 -------- .../Sources/Public/Tasks/CopyFilesTask.swift | 6 +- .../Internal/Enumerations/FileTests.swift | 70 +++++++++++++++++++ .../Enumerations/ResourceFileTests.swift | 20 ------ .../Public/Tasks/CopyFilesTaskTests.swift | 8 +-- 6 files changed, 118 insertions(+), 56 deletions(-) create mode 100644 Library/Sources/Internal/Enumerations/File.swift delete mode 100644 Library/Sources/Internal/Enumerations/ResourceFile.swift create mode 100644 Test/Sources/Cases/Internal/Enumerations/FileTests.swift delete mode 100644 Test/Sources/Cases/Internal/Enumerations/ResourceFileTests.swift diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift new file mode 100644 index 0000000..6fd4fb9 --- /dev/null +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -0,0 +1,41 @@ +enum File: String { + case dockerIgnore = "docker_ignore" + case gitIgnore = "git_ignore" + case license + case package + case readme +} + +// MARK: - Properties + +extension File { + + // MARK: Computed + + var fileName: String { + switch self { + case .dockerIgnore: ".dockerignore" + case .gitIgnore: ".gitignore" + case .license: "LICENSE" + case .readme: "README.md" + case .package: "Package.swift" + } + } + + var filePath: String { + switch self { + default: fileName + } + } + + var resourcePath: String { + switch self { + default: "Resources/Files" + } + } + +} + +// MARK: - CaseIterable + +extension File: CaseIterable {} diff --git a/Library/Sources/Internal/Enumerations/ResourceFile.swift b/Library/Sources/Internal/Enumerations/ResourceFile.swift deleted file mode 100644 index 7180349..0000000 --- a/Library/Sources/Internal/Enumerations/ResourceFile.swift +++ /dev/null @@ -1,29 +0,0 @@ -enum ResourceFile: String { - case dockerIgnore = "docker_ignore" - case gitIgnore = "git_ignore" - case license - case package - case readme -} - -// MARK: - Properties - -extension ResourceFile { - - // MARK: Computed - - var fileName: String { - switch self { - case .dockerIgnore: return ".dockerignore" - case .gitIgnore: return ".gitignore" - case .license: return "LICENSE" - case .readme: return "README.md" - case .package: return "Package.swift" - } - } - -} - -// MARK: - CaseIterable - -extension ResourceFile: CaseIterable {} diff --git a/Library/Sources/Public/Tasks/CopyFilesTask.swift b/Library/Sources/Public/Tasks/CopyFilesTask.swift index f91212e..774651c 100644 --- a/Library/Sources/Public/Tasks/CopyFilesTask.swift +++ b/Library/Sources/Public/Tasks/CopyFilesTask.swift @@ -20,17 +20,17 @@ public struct CopyFilesTask { // MARK: Functions public func callAsFunction(to rootFolder: URL) async throws (FileServiceError) { - for resource in ResourceFile.allCases { + for resource in File.allCases { guard let source = bundleService.url( forResource: resource.rawValue, withExtension: nil, - subdirectory: "Resources/Files" + subdirectory: resource.resourcePath ) else { assertionFailure("URL should have been initialized.") return } - let destination = rootFolder.appendingPath(resource.fileName) + let destination = rootFolder.appendingPath(resource.filePath) try await fileService.copyFile(from: source, to: destination) } diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift new file mode 100644 index 0000000..5c0960e --- /dev/null +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -0,0 +1,70 @@ +import Testing + +@testable import ColibriLibrary + +struct FileTests { + + // MARK: Properties tests + + @Test(arguments: zip(File.allCases, Expectation.fileNames)) + func fileName(for file: File, expects fileName: String) async throws { + // GIVEN + // WHEN + let result = file.fileName + + // THEN + #expect(result == fileName) + } + + @Test(arguments: zip(File.allCases, Expectation.filePaths)) + func filePath(for file: File, expects filePath: String) async throws { + // GIVEN + // WHEN + let result = file.filePath + + // THEN + #expect(result == filePath) + } + + @Test(arguments: zip(File.allCases, Expectation.resourcePaths)) + func resourcePath(for file: File, expects resourcePath: String) async throws { + // GIVEN + // WHEN + let result = file.resourcePath + + // THEN + #expect(result == resourcePath) + } + +} + +// MARK: - Expectations + +private extension FileTests { + enum Expectation { + static let fileNames: [String] = [ + ".dockerignore", + ".gitignore", + "LICENSE", + "Package.swift", + "README.md" + ] + + static let filePaths: [String] = [ + ".dockerignore", + ".gitignore", + "LICENSE", + "Package.swift", + "README.md" + ] + + static let resourcePaths: [String] = [ + "Resources/Files", + "Resources/Files", + "Resources/Files", + "Resources/Files", + "Resources/Files" + ] + } +} + diff --git a/Test/Sources/Cases/Internal/Enumerations/ResourceFileTests.swift b/Test/Sources/Cases/Internal/Enumerations/ResourceFileTests.swift deleted file mode 100644 index 13a176c..0000000 --- a/Test/Sources/Cases/Internal/Enumerations/ResourceFileTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Testing - -@testable import ColibriLibrary - -struct ResourceFileTests { - - // MARK: Properties tests - - @Test(arguments: zip(ResourceFile.allCases, - [".dockerignore", ".gitignore", "LICENSE", "Package.swift", "README.md"])) - func fileName(for resource: ResourceFile, expects fileName: String) async throws { - // GIVEN - // WHEN - let result = resource.fileName - - // THEN - #expect(result == fileName) - } - -} diff --git a/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift b/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift index 782ae7f..dad2ad0 100644 --- a/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift +++ b/Test/Sources/Cases/Public/Tasks/CopyFilesTaskTests.swift @@ -16,7 +16,7 @@ struct CopyFilesTaskTests { @Test func copyFiles() async throws { // GIVEN - let files = files(of: ResourceFile.allCases) + let files = files(of: File.allCases) let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) } let copyFiles = task(actions: actions) @@ -35,7 +35,7 @@ struct CopyFilesTaskTests { @Test(arguments: [FileServiceError.itemAlreadyExists, .itemEmptyData, .itemNotCopied]) func copyFiles(throws error: FileServiceError) async throws { // GIVEN - let files = files(of: Array(ResourceFile.allCases[0...2])) + let files = files(of: Array(File.allCases[0...2])) let actions = files.map { FileServiceMock.Action.copyFile($0.source, $0.destination) } let copyFiles = task(actions: actions + [.error(error)]) @@ -61,11 +61,11 @@ private extension CopyFilesTaskTests { // MARK: Type aliases - typealias File = (source: URL, destination: URL) + typealias FileURL = (source: URL, destination: URL) // MARK: Functions - func files(of resourceFiles: [ResourceFile]) -> [File] { + func files(of resourceFiles: [File]) -> [FileURL] { resourceFiles.map { (resourceFolder.appendingPath($0.rawValue), rootFolder.appendingPath($0.fileName)) } } -- 2.47.1 From 29dad1d6882fecba0defbbc65d232b6a42bccb62 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 12:07:57 +0100 Subject: [PATCH 51/80] Added the "dockerFile" case to the File enumeration in the library target. --- Library/Resources/Files/docker_file | 87 +++++++++++++++++++ Library/Resources/Files/package | 2 +- .../Sources/Internal/Enumerations/File.swift | 2 + .../Internal/Enumerations/FileTests.swift | 3 + 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 Library/Resources/Files/docker_file diff --git a/Library/Resources/Files/docker_file b/Library/Resources/Files/docker_file new file mode 100644 index 0000000..a20ddca --- /dev/null +++ b/Library/Resources/Files/docker_file @@ -0,0 +1,87 @@ +# ================================ +# Build image +# ================================ +FROM swift:6.0-noble as build + +# Install OS updates +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set up a build area +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ +RUN swift package resolve + +# Copy entire repo into container +COPY . . + +# Build the application, with optimizations, with static linking, and using jemalloc +RUN swift build -c release \ + --product "App" \ + --static-swift-stdlib \ + -Xlinker -ljemalloc + +# Switch to the staging area +WORKDIR /staging + +# Copy main executable to staging area +RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ + +# Copy static swift backtracer binary to staging area +RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ + +# Copy resources bundled by SPM to staging area +RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + +# Copy any resources from the public directory and views directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d /build/public ] && { mv /build/public ./public && chmod -R a-w ./public; } || true + +# ================================ +# Run image +# ================================ +FROM ubuntu:noble + +# Make sure all system packages are up to date, and install only essential packages. +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ +# If your app or its dependencies import FoundationNetworking, also install `libcurl4`. + # libcurl4 \ +# If your app or its dependencies import FoundationXML, also install `libxml2`. + # libxml2 \ + && rm -r /var/lib/apt/lists/* + +# Create a hummingbird user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app hummingbird + +# Switch to the new home directory +WORKDIR /app + +# Copy built executable and any staged resources from builder +COPY --from=build --chown=hummingbird:hummingbird /staging /app + +# Provide configuration needed by the built-in crash reporter and some sensible default behaviors. +ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static + +# Ensure all further commands run as the hummingbird user +USER hummingbird:hummingbird + +# Let Docker bind to port 8080 +EXPOSE 8080 + +# Start the Hummingbird service when the image is run, default to listening on 8080 in production environment +ENTRYPOINT ["./App] +CMD ["--hostname", "0.0.0.0", "--port", "8080"] diff --git a/Library/Resources/Files/package b/Library/Resources/Files/package index a06a7a1..9d28cb6 100644 --- a/Library/Resources/Files/package +++ b/Library/Resources/Files/package @@ -8,7 +8,7 @@ let package = Package( .macOS(.v10_15) ], products: [ - .executable(name: "app", targets: ["App"]), + .executable(name: "App", targets: ["App"]), .library(name: "AppLibrary", targets: ["AppLibrary"]) ], dependencies: [ diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift index 6fd4fb9..eccac90 100644 --- a/Library/Sources/Internal/Enumerations/File.swift +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -1,4 +1,5 @@ enum File: String { + case dockerFile = "docker_file" case dockerIgnore = "docker_ignore" case gitIgnore = "git_ignore" case license @@ -14,6 +15,7 @@ extension File { var fileName: String { switch self { + case .dockerFile: "Dockerfile" case .dockerIgnore: ".dockerignore" case .gitIgnore: ".gitignore" case .license: "LICENSE" diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift index 5c0960e..35f1d5c 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -43,6 +43,7 @@ struct FileTests { private extension FileTests { enum Expectation { static let fileNames: [String] = [ + "Dockerfile", ".dockerignore", ".gitignore", "LICENSE", @@ -51,6 +52,7 @@ private extension FileTests { ] static let filePaths: [String] = [ + "Dockerfile", ".dockerignore", ".gitignore", "LICENSE", @@ -63,6 +65,7 @@ private extension FileTests { "Resources/Files", "Resources/Files", "Resources/Files", + "Resources/Files", "Resources/Files" ] } -- 2.47.1 From 5f958c6f4c7ce5296bdce799cc03f55c14b77e7e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 12:14:42 +0100 Subject: [PATCH 52/80] Added the "root" case to the Folder enumeration. --- Library/Sources/Internal/Enumerations/Folder.swift | 2 ++ Test/Sources/Cases/Internal/Enumerations/FolderTests.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Library/Sources/Internal/Enumerations/Folder.swift b/Library/Sources/Internal/Enumerations/Folder.swift index fc9c448..6ab0a90 100644 --- a/Library/Sources/Internal/Enumerations/Folder.swift +++ b/Library/Sources/Internal/Enumerations/Folder.swift @@ -2,6 +2,7 @@ enum Folder { case app case libraryPublic case libraryInternal + case root case testCasesPublic case testCasesInternal case testHelpers @@ -18,6 +19,7 @@ extension Folder { 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" diff --git a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift index 31196f5..1278e42 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift @@ -26,6 +26,7 @@ private extension FolderTests { "App/Sources", "Library/Sources/Public", "Library/Sources/Internal", + "", "Test/Sources/Cases/Public", "Test/Sources/Cases/Internal", "Test/Sources/Helpers" -- 2.47.1 From 6fe45db4f10becb7349788c7a3c70bb9e9bb1eff Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 12:19:29 +0100 Subject: [PATCH 53/80] Implemented the "folder" property for the File enumeration in the library target. --- .../Sources/Internal/Enumerations/File.swift | 8 +++++++- .../Internal/Enumerations/FileTests.swift | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift index eccac90..3463a6e 100644 --- a/Library/Sources/Internal/Enumerations/File.swift +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -26,7 +26,13 @@ extension File { var filePath: String { switch self { - default: fileName + default: folder.path + fileName + } + } + + var folder: Folder { + switch self { + default: .root } } diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift index 35f1d5c..48b71ea 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -26,6 +26,16 @@ struct FileTests { #expect(result == filePath) } + @Test(arguments: zip(File.allCases, Expectation.folders)) + func folder(for file: File, expects folder: Folder) async throws { + // GIVEN + // WHEN + let result = file.folder + + // THEN + #expect(result == folder) + } + @Test(arguments: zip(File.allCases, Expectation.resourcePaths)) func resourcePath(for file: File, expects resourcePath: String) async throws { // GIVEN @@ -59,6 +69,16 @@ private extension FileTests { "Package.swift", "README.md" ] + + static let folders: [Folder] = [ + .root, + .root, + .root, + .root, + .root, + .root + ] + static let resourcePaths: [String] = [ "Resources/Files", -- 2.47.1 From 3f8651ca52f87a2a899fdc340b3804cc87e92453 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 12:31:13 +0100 Subject: [PATCH 54/80] Implemented the "allCasesWithRoot" static property for the Folder enumeration in the library target. --- .../Sources/Internal/Enumerations/Folder.swift | 17 ++++++++++++++++- .../Internal/Enumerations/FolderTests.swift | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Library/Sources/Internal/Enumerations/Folder.swift b/Library/Sources/Internal/Enumerations/Folder.swift index 6ab0a90..a732658 100644 --- a/Library/Sources/Internal/Enumerations/Folder.swift +++ b/Library/Sources/Internal/Enumerations/Folder.swift @@ -30,4 +30,19 @@ extension Folder { // MARK: - CaseIterable -extension Folder: CaseIterable {} +extension Folder: CaseIterable { + + // MARK: Properties + + static var allCases: [Folder] {[ + .app, + .libraryPublic, + .libraryInternal, + .testCasesPublic, + .testCasesInternal, + .testHelpers + ]} + + static var allCasesWithRoot: [Folder] { [.root] + Folder.allCases } + +} diff --git a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift index 1278e42..1caba0d 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift @@ -6,7 +6,7 @@ struct FolderTests { // MARK: Properties tests - @Test(arguments: zip(Folder.allCases, Expectation.paths)) + @Test(arguments: zip(Folder.allCasesWithRoot, Expectation.paths)) func paths(for folder: Folder, expects path: String) async throws { // GIVEN // WHEN @@ -23,10 +23,10 @@ struct FolderTests { private extension FolderTests { enum Expectation { static let paths: [String] = [ + "", "App/Sources", "Library/Sources/Public", "Library/Sources/Internal", - "", "Test/Sources/Cases/Public", "Test/Sources/Cases/Internal", "Test/Sources/Helpers" -- 2.47.1 From 0b69973537d1adf17476e7f64efe899fe6d87a29 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 12:41:09 +0100 Subject: [PATCH 55/80] Moved the files under the "Resources/Files" folder in the library target to the "Resources/Files/Sources" folder. --- .../Files/{docker_file => Sources/DockerFile} | 0 .../Files/{docker_ignore => Sources/DockerIgnore} | 0 .../Files/{git_ignore => Sources/GitIgnore} | 0 .../Resources/Files/{license => Sources/License} | 0 .../Resources/Files/{package => Sources/Package} | 0 Library/Resources/Files/{readme => Sources/Readme} | 0 Library/Sources/Internal/Enumerations/File.swift | 14 +++++++------- Library/Sources/Public/Tasks/CopyFilesTask.swift | 8 ++++---- .../Cases/Internal/Enumerations/FileTests.swift | 12 ++++++------ 9 files changed, 17 insertions(+), 17 deletions(-) rename Library/Resources/Files/{docker_file => Sources/DockerFile} (100%) rename Library/Resources/Files/{docker_ignore => Sources/DockerIgnore} (100%) rename Library/Resources/Files/{git_ignore => Sources/GitIgnore} (100%) rename Library/Resources/Files/{license => Sources/License} (100%) rename Library/Resources/Files/{package => Sources/Package} (100%) rename Library/Resources/Files/{readme => Sources/Readme} (100%) diff --git a/Library/Resources/Files/docker_file b/Library/Resources/Files/Sources/DockerFile similarity index 100% rename from Library/Resources/Files/docker_file rename to Library/Resources/Files/Sources/DockerFile diff --git a/Library/Resources/Files/docker_ignore b/Library/Resources/Files/Sources/DockerIgnore similarity index 100% rename from Library/Resources/Files/docker_ignore rename to Library/Resources/Files/Sources/DockerIgnore diff --git a/Library/Resources/Files/git_ignore b/Library/Resources/Files/Sources/GitIgnore similarity index 100% rename from Library/Resources/Files/git_ignore rename to Library/Resources/Files/Sources/GitIgnore diff --git a/Library/Resources/Files/license b/Library/Resources/Files/Sources/License similarity index 100% rename from Library/Resources/Files/license rename to Library/Resources/Files/Sources/License diff --git a/Library/Resources/Files/package b/Library/Resources/Files/Sources/Package similarity index 100% rename from Library/Resources/Files/package rename to Library/Resources/Files/Sources/Package diff --git a/Library/Resources/Files/readme b/Library/Resources/Files/Sources/Readme similarity index 100% rename from Library/Resources/Files/readme rename to Library/Resources/Files/Sources/Readme diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift index 3463a6e..203ff5e 100644 --- a/Library/Sources/Internal/Enumerations/File.swift +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -1,10 +1,10 @@ enum File: String { - case dockerFile = "docker_file" - case dockerIgnore = "docker_ignore" - case gitIgnore = "git_ignore" - case license - case package - case readme + case dockerFile = "DockerFile" + case dockerIgnore = "DockerIgnore" + case gitIgnore = "GitIgnore" + case license = "License" + case package = "Package" + case readme = "Readme" } // MARK: - Properties @@ -38,7 +38,7 @@ extension File { var resourcePath: String { switch self { - default: "Resources/Files" + default: "Resources/Files/Sources" } } diff --git a/Library/Sources/Public/Tasks/CopyFilesTask.swift b/Library/Sources/Public/Tasks/CopyFilesTask.swift index 774651c..65fce3d 100644 --- a/Library/Sources/Public/Tasks/CopyFilesTask.swift +++ b/Library/Sources/Public/Tasks/CopyFilesTask.swift @@ -20,17 +20,17 @@ public struct CopyFilesTask { // MARK: Functions public func callAsFunction(to rootFolder: URL) async throws (FileServiceError) { - for resource in File.allCases { + for file in File.allCases { guard let source = bundleService.url( - forResource: resource.rawValue, + forResource: file.rawValue, withExtension: nil, - subdirectory: resource.resourcePath + subdirectory: file.resourcePath ) else { assertionFailure("URL should have been initialized.") return } - let destination = rootFolder.appendingPath(resource.filePath) + let destination = rootFolder.appendingPath(file.filePath) try await fileService.copyFile(from: source, to: destination) } diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift index 48b71ea..f28e8d6 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -81,12 +81,12 @@ private extension FileTests { static let resourcePaths: [String] = [ - "Resources/Files", - "Resources/Files", - "Resources/Files", - "Resources/Files", - "Resources/Files", - "Resources/Files" + "Resources/Files/Sources", + "Resources/Files/Sources", + "Resources/Files/Sources", + "Resources/Files/Sources", + "Resources/Files/Sources", + "Resources/Files/Sources" ] } } -- 2.47.1 From c08dbe5602111c3b527dfa76674abbe053924651 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 13:32:59 +0100 Subject: [PATCH 56/80] Added some more cases to the File enumeration in the library target. --- Library/Resources/Files/Sources/App/App | 20 ++++++ .../Resources/Files/Sources/App/AppOptions | 20 ++++++ .../Files/Sources/Library/AppArguments | 11 +++ .../Files/Sources/Library/AppBuilder | 69 +++++++++++++++++++ .../Files/Sources/Library/Environment | 11 +++ .../Files/Sources/Library/LoggerLevel | 9 +++ Library/Resources/Files/Sources/Package | 2 +- Library/Resources/Files/Sources/Test/AppTests | 33 +++++++++ .../Files/Sources/Test/TestArguments | 12 ++++ .../Sources/Internal/Enumerations/File.swift | 35 ++++++++-- .../Internal/Enumerations/Folder.swift | 12 ++-- .../Internal/Enumerations/FileTests.swift | 40 +++++++++-- 12 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 Library/Resources/Files/Sources/App/App create mode 100644 Library/Resources/Files/Sources/App/AppOptions create mode 100644 Library/Resources/Files/Sources/Library/AppArguments create mode 100644 Library/Resources/Files/Sources/Library/AppBuilder create mode 100644 Library/Resources/Files/Sources/Library/Environment create mode 100644 Library/Resources/Files/Sources/Library/LoggerLevel create mode 100644 Library/Resources/Files/Sources/Test/AppTests create mode 100644 Library/Resources/Files/Sources/Test/TestArguments diff --git a/Library/Resources/Files/Sources/App/App b/Library/Resources/Files/Sources/App/App new file mode 100644 index 0000000..9183ae7 --- /dev/null +++ b/Library/Resources/Files/Sources/App/App @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser + +@main +struct App: AsyncParsableCommand { + + // MARK: Properties + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let builder = AppBuilder(name: "App") + let app = try await builder(options) + + try await app.runService() + } + +} diff --git a/Library/Resources/Files/Sources/App/AppOptions b/Library/Resources/Files/Sources/App/AppOptions new file mode 100644 index 0000000..a6d835f --- /dev/null +++ b/Library/Resources/Files/Sources/App/AppOptions @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser +import Logging + +extension App { + struct Options: AppArguments, ParsableArguments { + + // MARK: Properties + + @Option(name: .shortAndLong) + var hostname: String = "127.0.0.1" + + @Option(name: .shortAndLong) + var port: Int = 8080 + + @Option(name: .shortAndLong) + var logLevel: Logger.Level? + + } +} diff --git a/Library/Resources/Files/Sources/Library/AppArguments b/Library/Resources/Files/Sources/Library/AppArguments new file mode 100644 index 0000000..40039e1 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/AppArguments @@ -0,0 +1,11 @@ +import Logging + +public protocol AppArguments { + + // MARK: Properties + + var hostname: String { get } + var logLevel: Logger.Level? { get } + var port: Int { get } + +} diff --git a/Library/Resources/Files/Sources/Library/AppBuilder b/Library/Resources/Files/Sources/Library/AppBuilder new file mode 100644 index 0000000..1998089 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/AppBuilder @@ -0,0 +1,69 @@ +import Hummingbird +import Logging + +public struct AppBuilder { + + // MARK: Properties + + private let environment: Environment + private let name: String + + // MARK: Initialisers + + public init(name: String) { + self.environment = Environment() + self.name = name + } + + // MARK: Functions + + public func callAsFunction( + _ arguments: some AppArguments + ) async throws -> some ApplicationProtocol { + let logger = { + var logger = Logger(label: name) + + logger.logLevel = arguments.logLevel + ?? environment.logLevel.flatMap { Logger.Level(rawValue: $0) ?? .info } + ?? .info + + return logger + }() + + let router = router(logger: logger) + + return Application( + router: router, + configuration: .init( + address: .hostname(arguments.hostname, port: arguments.port), + serverName: name + ), + logger: logger + ) + } + +} + +// MARK: - Helpers + +private extension AppBuilder { + + // MARK: Type aliases + + typealias AppRequestContext = BasicRequestContext + + // MARK: Functions + + func router(logger: Logger) -> Router { + let router = Router() + + router.add(middleware: LogRequestsMiddleware(logger.logLevel)) + + router.get("/") { _,_ in + "" + } + + return router + } + +} diff --git a/Library/Resources/Files/Sources/Library/Environment b/Library/Resources/Files/Sources/Library/Environment new file mode 100644 index 0000000..99d4d41 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/Environment @@ -0,0 +1,11 @@ +import Hummingbird + +extension Environment { + + // MARK: Computed + + public var logLevel: String? { + self.get("LOG_LEVEL") + } + +} diff --git a/Library/Resources/Files/Sources/Library/LoggerLevel b/Library/Resources/Files/Sources/Library/LoggerLevel new file mode 100644 index 0000000..0d1abb6 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/LoggerLevel @@ -0,0 +1,9 @@ +import ArgumentParser +import Logging + +/// Extend `Logger.Level` so it can be used as an argument +#if hasFeature(RetroactiveAttribute) +extension Logger.Level: @retroactive ExpressibleByArgument {} +#else +extension Logger.Level: ExpressibleByArgument {} +#endif diff --git a/Library/Resources/Files/Sources/Package b/Library/Resources/Files/Sources/Package index 9d28cb6..0c804dc 100644 --- a/Library/Resources/Files/Sources/Package +++ b/Library/Resources/Files/Sources/Package @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "App", platforms: [ - .macOS(.v10_15) + .macOS(.v14) ], products: [ .executable(name: "App", targets: ["App"]), diff --git a/Library/Resources/Files/Sources/Test/AppTests b/Library/Resources/Files/Sources/Test/AppTests new file mode 100644 index 0000000..31f1868 --- /dev/null +++ b/Library/Resources/Files/Sources/Test/AppTests @@ -0,0 +1,33 @@ +import AppLibrary +import Hummingbird +import HummingbirdTesting +import Testing + +struct AppTests { + + // MARK: Properties + + private let arguments = TestArguments() + private let builder = AppBuilder(name: "App") + + // MARK: Route tests + + @Test(arguments: ["/"]) + func routes(_ uri: String) async throws { + let app = try await builder(arguments) + + try await app.test(.router) { client in + try await client.execute(uri: uri, method: .get) { response in + #expect(response.status == .ok) + #expect(response.body == .empty) + } + } + } + +} + +// MARK: ByteBuffer+Constants + +private extension ByteBuffer { + static let empty = ByteBuffer(string: "") +} diff --git a/Library/Resources/Files/Sources/Test/TestArguments b/Library/Resources/Files/Sources/Test/TestArguments new file mode 100644 index 0000000..c2f0b21 --- /dev/null +++ b/Library/Resources/Files/Sources/Test/TestArguments @@ -0,0 +1,12 @@ +import AppLibrary +import Logging + +struct TestArguments: AppArguments { + + // MARK: Properties + + let hostname = "127.0.0.1" + let port = 0 + let logLevel: Logger.Level? = .trace + +} diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift index 203ff5e..861b114 100644 --- a/Library/Sources/Internal/Enumerations/File.swift +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -1,10 +1,19 @@ 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 @@ -15,30 +24,46 @@ extension File { 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 { - switch self { - default: folder.path + fileName - } + 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 { - switch self { - default: "Resources/Files/Sources" + 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 } } diff --git a/Library/Sources/Internal/Enumerations/Folder.swift b/Library/Sources/Internal/Enumerations/Folder.swift index a732658..ba0bad6 100644 --- a/Library/Sources/Internal/Enumerations/Folder.swift +++ b/Library/Sources/Internal/Enumerations/Folder.swift @@ -16,13 +16,13 @@ extension Folder { var path: String { switch self { - case .app: "App/Sources" - case .libraryPublic: "Library/Sources/Public" - case .libraryInternal: "Library/Sources/Internal" + 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" + case .testCasesPublic: "Test/Sources/Cases/Public/" + case .testCasesInternal: "Test/Sources/Cases/Internal/" + case .testHelpers: "Test/Sources/Helpers/" } } diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift index f28e8d6..8e3b884 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -53,40 +53,72 @@ struct FileTests { private extension FileTests { enum Expectation { static let fileNames: [String] = [ + "App.swift", + "AppArguments.swift", + "AppBuilder.swift", + "AppOptions.swift", + "AppTests.swift", "Dockerfile", ".dockerignore", + "Environment+Properties.swift", ".gitignore", "LICENSE", + "LoggerLevel+Conformances.swift", "Package.swift", - "README.md" + "README.md", + "TestArguments.swift" ] static let filePaths: [String] = [ + "App/Sources/App.swift", + "Library/Sources/Public/AppArguments.swift", + "Library/Sources/Public/AppBuilder.swift", + "App/Sources/AppOptions.swift", + "Test/Sources/Cases/Public/AppTests.swift", "Dockerfile", ".dockerignore", + "Library/Sources/Internal/Environment+Properties.swift", ".gitignore", "LICENSE", + "Library/Sources/Internal/LoggerLevel+Conformances.swift", "Package.swift", - "README.md" + "README.md", + "Test/Sources/Helpers/TestArguments.swift" ] static let folders: [Folder] = [ + .app, + .libraryPublic, + .libraryPublic, + .app, + .testCasesPublic, .root, .root, + .libraryInternal, .root, .root, + .libraryInternal, .root, - .root + .root, + .testHelpers ] static let resourcePaths: [String] = [ + "Resources/Files/Sources/App", + "Resources/Files/Sources/Library", + "Resources/Files/Sources/Library", + "Resources/Files/Sources/App", + "Resources/Files/Sources/Test", "Resources/Files/Sources", "Resources/Files/Sources", + "Resources/Files/Sources/Library", "Resources/Files/Sources", "Resources/Files/Sources", + "Resources/Files/Sources/Library", "Resources/Files/Sources", - "Resources/Files/Sources" + "Resources/Files/Sources", + "Resources/Files/Sources/Test" ] } } -- 2.47.1 From d3283c912f91b557f7e0d70eddc0c89b258dbbfa Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 19:52:40 +0100 Subject: [PATCH 57/80] Renamed the BundleServicing protocol in the library target as Bundleable. --- .../Sources/Internal/Extensions/Bundle+Conformances.swift | 4 ++-- .../Protocols/{BundleServicing.swift => Bundleable.swift} | 2 +- Library/Sources/Public/Tasks/CopyFilesTask.swift | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) rename Library/Sources/Public/Protocols/{BundleServicing.swift => Bundleable.swift} (82%) diff --git a/Library/Sources/Internal/Extensions/Bundle+Conformances.swift b/Library/Sources/Internal/Extensions/Bundle+Conformances.swift index 1dcd2e6..d8b5fbc 100644 --- a/Library/Sources/Internal/Extensions/Bundle+Conformances.swift +++ b/Library/Sources/Internal/Extensions/Bundle+Conformances.swift @@ -1,5 +1,5 @@ import Foundation -// MARK: - BundleServicing +// MARK: - Bundleable -extension Bundle: BundleServicing {} +extension Bundle: Bundleable {} diff --git a/Library/Sources/Public/Protocols/BundleServicing.swift b/Library/Sources/Public/Protocols/Bundleable.swift similarity index 82% rename from Library/Sources/Public/Protocols/BundleServicing.swift rename to Library/Sources/Public/Protocols/Bundleable.swift index f5a4734..308682e 100644 --- a/Library/Sources/Public/Protocols/BundleServicing.swift +++ b/Library/Sources/Public/Protocols/Bundleable.swift @@ -1,6 +1,6 @@ import Foundation -public protocol BundleServicing { +public protocol Bundleable { // MARK: Functions diff --git a/Library/Sources/Public/Tasks/CopyFilesTask.swift b/Library/Sources/Public/Tasks/CopyFilesTask.swift index 65fce3d..3888d05 100644 --- a/Library/Sources/Public/Tasks/CopyFilesTask.swift +++ b/Library/Sources/Public/Tasks/CopyFilesTask.swift @@ -4,16 +4,16 @@ public struct CopyFilesTask { // MARK: Properties - private let bundleService: BundleServicing + private let bundle: Bundleable private let fileService: FileServicing // MARK: Initialisers public init( - bundleService: BundleServicing? = nil, + bundle: Bundleable? = nil, fileService: FileServicing ) { - self.bundleService = bundleService ?? Bundle.module + self.bundle = bundle ?? Bundle.module self.fileService = fileService } @@ -21,7 +21,7 @@ public struct CopyFilesTask { public func callAsFunction(to rootFolder: URL) async throws (FileServiceError) { for file in File.allCases { - guard let source = bundleService.url( + guard let source = bundle.url( forResource: file.rawValue, withExtension: nil, subdirectory: file.resourcePath -- 2.47.1 From 02fb6b9345ac3dcab8b320002608ffd44b096e2d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 20:03:11 +0100 Subject: [PATCH 58/80] Implemented the "availableData" property for the Pipe+Properties extension in the library target and also, implemented its "append()" function that appends its data concurrently. --- .../Internal/Extensions/Pipe+Properties.swift | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 Library/Sources/Internal/Extensions/Pipe+Properties.swift diff --git a/Library/Sources/Internal/Extensions/Pipe+Properties.swift b/Library/Sources/Internal/Extensions/Pipe+Properties.swift new file mode 100644 index 0000000..94b27e7 --- /dev/null +++ b/Library/Sources/Internal/Extensions/Pipe+Properties.swift @@ -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.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() + } + +} -- 2.47.1 From 245529f88f6657c3ee991579cfbe6a813024ceb6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 20:04:24 +0100 Subject: [PATCH 59/80] Implemented the Processable protocol in the library target and conformed the Process object to it. --- .../Extensions/Process+Conformances.swift | 5 +++++ .../Internal/Protocols/Processable.swift | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Library/Sources/Internal/Extensions/Process+Conformances.swift create mode 100644 Library/Sources/Internal/Protocols/Processable.swift diff --git a/Library/Sources/Internal/Extensions/Process+Conformances.swift b/Library/Sources/Internal/Extensions/Process+Conformances.swift new file mode 100644 index 0000000..d5dfa7b --- /dev/null +++ b/Library/Sources/Internal/Extensions/Process+Conformances.swift @@ -0,0 +1,5 @@ +import Foundation + +// MARK: - Processable + +extension Process: Processable {} diff --git a/Library/Sources/Internal/Protocols/Processable.swift b/Library/Sources/Internal/Protocols/Processable.swift new file mode 100644 index 0000000..42cb1b9 --- /dev/null +++ b/Library/Sources/Internal/Protocols/Processable.swift @@ -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 + +} -- 2.47.1 From 7081f7b755a935b068875c03314cd6327e438195 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 20:05:37 +0100 Subject: [PATCH 60/80] Implemented the RunProcessTask task in the library target. --- .../Internal/Tasks/RunProcessTask.swift | 75 +++++++++++++++++++ .../Public/Tasks/CreateRootFolderTask.swift | 5 +- 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 Library/Sources/Internal/Tasks/RunProcessTask.swift diff --git a/Library/Sources/Internal/Tasks/RunProcessTask.swift b/Library/Sources/Internal/Tasks/RunProcessTask.swift new file mode 100644 index 0000000..7835b87 --- /dev/null +++ b/Library/Sources/Internal/Tasks/RunProcessTask.swift @@ -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) + } + } + +} + +// MARK: - Errors + +public enum RunProcessError: Error { + case captured(_ error: Error) + case output(_ output: String) + case unexpected +} diff --git a/Library/Sources/Public/Tasks/CreateRootFolderTask.swift b/Library/Sources/Public/Tasks/CreateRootFolderTask.swift index 6345aee..06f6643 100644 --- a/Library/Sources/Public/Tasks/CreateRootFolderTask.swift +++ b/Library/Sources/Public/Tasks/CreateRootFolderTask.swift @@ -14,10 +14,7 @@ public struct CreateRootFolderTask { // MARK: Functions - public func callAsFunction( - name: String, - at location: URL? = nil - ) async throws -> URL { + public func callAsFunction(name: String, at location: URL? = nil) async throws -> URL { guard !name.isEmpty else { throw CreateRootFolderError.nameIsEmpty } -- 2.47.1 From 1fcbc382536ad54b21c8737427785e8c3270c5b4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 20:33:26 +0100 Subject: [PATCH 61/80] Implemented the InitGitInFolderTask task in the library target. --- .../Public/Tasks/InitGitInFolderTask.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Library/Sources/Public/Tasks/InitGitInFolderTask.swift diff --git a/Library/Sources/Public/Tasks/InitGitInFolderTask.swift b/Library/Sources/Public/Tasks/InitGitInFolderTask.swift new file mode 100644 index 0000000..850e1c2 --- /dev/null +++ b/Library/Sources/Public/Tasks/InitGitInFolderTask.swift @@ -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"]) + } + +} -- 2.47.1 From c7a12582731ea312e26c39ab09ab287f689ac404 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 20:34:12 +0100 Subject: [PATCH 62/80] Integrated the InitGitInFolderTask task into the CreateCommand command in the executable target. --- Executable/Sources/Commands/CreateCommand.swift | 2 ++ .../Cases/Internal/Enumerations/FolderTests.swift | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Executable/Sources/Commands/CreateCommand.swift b/Executable/Sources/Commands/CreateCommand.swift index 1a5899e..9f35af7 100644 --- a/Executable/Sources/Commands/CreateCommand.swift +++ b/Executable/Sources/Commands/CreateCommand.swift @@ -23,6 +23,7 @@ extension Colibri { let copyFiles = CopyFilesTask(fileService: fileService) let createFolders = CreateFoldersTask(fileService: fileService) let createRootFolder = CreateRootFolderTask(fileService: fileService) + let initGitInFolder = InitGitInFolderTask() let rootFolder = try await createRootFolder( name: options.name, @@ -31,6 +32,7 @@ extension Colibri { try await createFolders(at: rootFolder) try await copyFiles(to: rootFolder) + try await initGitInFolder(at: rootFolder) } } diff --git a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift index 1caba0d..052c6d1 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FolderTests.swift @@ -24,12 +24,12 @@ private extension FolderTests { enum Expectation { static let paths: [String] = [ "", - "App/Sources", - "Library/Sources/Public", - "Library/Sources/Internal", - "Test/Sources/Cases/Public", - "Test/Sources/Cases/Internal", - "Test/Sources/Helpers" + "App/Sources/", + "Library/Sources/Public/", + "Library/Sources/Internal/", + "Test/Sources/Cases/Public/", + "Test/Sources/Cases/Internal/", + "Test/Sources/Helpers/" ] } } -- 2.47.1 From 36f1b8fe5c2a54932733e5fb0dd35e7e99bc72ba Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 18 Jan 2025 23:12:55 +0100 Subject: [PATCH 63/80] Implemented some tests for the RunProcessTaskTests case tests in the tests target. --- .../Internal/Tasks/RunProcessTask.swift | 6 +- .../Internal/Tasks/RunProcessTaskTests.swift | 68 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift diff --git a/Library/Sources/Internal/Tasks/RunProcessTask.swift b/Library/Sources/Internal/Tasks/RunProcessTask.swift index 7835b87..ea267b4 100644 --- a/Library/Sources/Internal/Tasks/RunProcessTask.swift +++ b/Library/Sources/Internal/Tasks/RunProcessTask.swift @@ -60,7 +60,7 @@ struct RunProcessTask { } catch let error as RunProcessError { throw error } catch { - throw RunProcessError.captured(error) + throw RunProcessError.captured(error.localizedDescription) } } @@ -68,8 +68,8 @@ struct RunProcessTask { // MARK: - Errors -public enum RunProcessError: Error { - case captured(_ error: Error) +public enum RunProcessError: Error, Equatable { + case captured(_ output: String) case output(_ output: String) case unexpected } diff --git a/Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift b/Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift new file mode 100644 index 0000000..f493e1f --- /dev/null +++ b/Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift @@ -0,0 +1,68 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct RunProcessTaskTests { + + // MARK: Properties + + private var process: Process + + // MARK: Initialisers + + init() { + self.process = Process() + } + + // MARK: Functions tests + + @Test(arguments: [Argument.empty, Argument.listAllInFolder]) + func run(with arguments: [String]) async throws { + // GIVEN + var task = RunProcessTask(process: process) + + // WHEN + let output = try await task(path: .ls, arguments: arguments) + + // THEN + #expect(output.isEmpty == false) + } + + @Test(arguments: zip([Argument.help, Argument.listAllInPWD], Throw.outputs)) + func runThrows(with arguments: [String], throws error: RunProcessError) async throws { + // GIVEN + var task = RunProcessTask(process: process) + + // WHEN + // THEN + await #expect(throws: error) { + try await task(path: .ls, arguments: arguments) + } + } + +} + +// MARK: - String+Constants + +private extension String { + static let ls = "/bin/ls" +} + +// MARK: - Parameters + +private extension RunProcessTaskTests { + enum Argument { + static let empty: [String] = [] + static let help: [String] = ["--help"] + static let listAllInFolder: [String] = ["-la", "."] + static let listAllInPWD: [String] = ["-la", "~"] + } + + enum Throw { + static let outputs: [RunProcessError] = [ + .output("ls: unrecognized option `--help\'\nusage: ls [-@ABCFGHILOPRSTUWXabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n"), + .output("ls: ~: No such file or directory\n") + ] + } +} -- 2.47.1 From 7dfddb8d4c3a4a6c870e1e064b9b3467c1c5005d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 19 Jan 2025 01:27:06 +0100 Subject: [PATCH 64/80] Added the Swift Mustache package dependency to the Package file, and attached its product to the library target. --- Package.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 9a07581..c42d31c 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,8 @@ let package = Package( .library(name: "ColibriLibrary", targets: ["ColibriLibrary"]) ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0") + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), + .package(url: "https://github.com/hummingbird-project/swift-mustache", from: "2.0.0") ], targets: [ .executableTarget( @@ -25,7 +26,9 @@ let package = Package( ), .target( name: "ColibriLibrary", - dependencies: [], + dependencies: [ + .product(name: "Mustache", package: "swift-mustache") + ], path: "Library", resources: [ .copy("Resources") -- 2.47.1 From 8bc446ca68c5d8249ac5749495868e5c5d3731e7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 24 Jan 2025 23:52:30 +0100 Subject: [PATCH 65/80] Added the "resourcePath" property to the Bundleable protocol in the library target. --- Library/Sources/Public/Protocols/Bundleable.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Library/Sources/Public/Protocols/Bundleable.swift b/Library/Sources/Public/Protocols/Bundleable.swift index 308682e..e6fe719 100644 --- a/Library/Sources/Public/Protocols/Bundleable.swift +++ b/Library/Sources/Public/Protocols/Bundleable.swift @@ -1,6 +1,10 @@ import Foundation public protocol Bundleable { + + // MARK: Computed + + var resourcePath: String? { get } // MARK: Functions -- 2.47.1 From bc765705a8b16a78ac5a361bf0a69abd10cab63b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 25 Jan 2025 00:27:47 +0100 Subject: [PATCH 66/80] Defined the "render(_: on: )" function for the TemplateServicing protocol in the library target. --- Library/Sources/Public/Protocols/TemplateServicing.swift | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Library/Sources/Public/Protocols/TemplateServicing.swift diff --git a/Library/Sources/Public/Protocols/TemplateServicing.swift b/Library/Sources/Public/Protocols/TemplateServicing.swift new file mode 100644 index 0000000..ec6ba22 --- /dev/null +++ b/Library/Sources/Public/Protocols/TemplateServicing.swift @@ -0,0 +1,7 @@ +public protocol TemplateServicing { + + // MARK: Functions + + func render(_ object: Any, on template: String) async throws (RenderServiceError) -> String + +} -- 2.47.1 From 1de9738e6e048babb8db089f8aeb1a9afbfbf580 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Feb 2025 20:58:14 +0100 Subject: [PATCH 67/80] Defined the "render(_: on: )" function for the TemplateServicing protocol in the library target. --- .../Sources/Public/Protocols/TemplateServicing.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Library/Sources/Public/Protocols/TemplateServicing.swift b/Library/Sources/Public/Protocols/TemplateServicing.swift index ec6ba22..24682e6 100644 --- a/Library/Sources/Public/Protocols/TemplateServicing.swift +++ b/Library/Sources/Public/Protocols/TemplateServicing.swift @@ -2,6 +2,15 @@ public protocol TemplateServicing { // MARK: Functions - func render(_ object: Any, on template: String) async throws (RenderServiceError) -> String + func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String } + +// MARK: - Errors + +public enum TemplateServiceError: Error { + case contentNotRendered + case resourcePathNotFound + case serviceNotInitialized + case templateNotFound +} -- 2.47.1 From a515747c21946cd3ffc5d27c9071ff0f8343c4dd Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Feb 2025 21:57:47 +0100 Subject: [PATCH 68/80] Implemented the TemplateService service in the library target. --- .../Sources/Public/Services/FileService.swift | 10 ++- .../Public/Services/TemplateService.swift | 50 +++++++++++++ .../Services/TemplateServiceTests.swift | 47 ++++++++++++ .../Helpers/Mocks/TemplateServiceMock.swift | 75 +++++++++++++++++++ .../Helpers/Spies/FileServiceSpy.swift | 3 +- .../Helpers/Spies/TemplateServiceSpy.swift | 38 ++++++++++ 6 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 Library/Sources/Public/Services/TemplateService.swift create mode 100644 Test/Sources/Cases/Public/Services/TemplateServiceTests.swift create mode 100644 Test/Sources/Helpers/Mocks/TemplateServiceMock.swift create mode 100644 Test/Sources/Helpers/Spies/TemplateServiceSpy.swift diff --git a/Library/Sources/Public/Services/FileService.swift b/Library/Sources/Public/Services/FileService.swift index d7e1fdb..d9f56c5 100644 --- a/Library/Sources/Public/Services/FileService.swift +++ b/Library/Sources/Public/Services/FileService.swift @@ -1,7 +1,7 @@ import Foundation -public struct FileService: FileServicing { - +public struct FileService { + // MARK: Properties private let fileManager: FileManager @@ -12,6 +12,12 @@ public struct FileService: FileServicing { self.fileManager = fileManager } +} + +// MARK: - FileServicing + +extension FileService: FileServicing { + // MARK: Computed public var currentFolder: URL { diff --git a/Library/Sources/Public/Services/TemplateService.swift b/Library/Sources/Public/Services/TemplateService.swift new file mode 100644 index 0000000..4afed81 --- /dev/null +++ b/Library/Sources/Public/Services/TemplateService.swift @@ -0,0 +1,50 @@ +import Foundation +import Mustache + +public struct TemplateService { + + // MARK: Properties + + private let mustacheRenderer: MustacheLibrary + + // MARK: Initialisers + + public init( + bundle: Bundleable? = nil, + templateFolder: String + ) async throws (TemplateServiceError) { + guard let pathResources = (bundle ?? Bundle.module).resourcePath else { + throw .resourcePathNotFound + } + + let pathTemplates = pathResources + "/" + templateFolder + + do { + self.mustacheRenderer = try await MustacheLibrary(directory: pathTemplates) + } catch { + throw .serviceNotInitialized + } + } + +} + +// MARK: - TemplateServicing + +extension TemplateService: TemplateServicing { + + // MARK: Functions + + public func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String { + guard mustacheRenderer.getTemplate(named: template) != nil else { + throw .templateNotFound + } + + guard let content = mustacheRenderer.render(object, withTemplate: template) else { + throw .contentNotRendered + } + + return content + } + +} + diff --git a/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift b/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift new file mode 100644 index 0000000..5da5fa3 --- /dev/null +++ b/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift @@ -0,0 +1,47 @@ +import ColibriLibrary +import Foundation +import Testing + +struct TemplateServiceTests { + + // MARK: Properties + + private let spy = TemplateServiceSpy() + + // MARK: Functions tests + + @Test(arguments: [String.content]) + func render(_ content: String) async throws { + // GIVEN + let service = TemplateServiceMock(action: .render(content), spy: spy) + + // WHEN + let result = try await service.render([:], on: .template) + + // THEN + #expect(result == content) + + #expect(spy.actions.isEmpty == false) + } + + @Test(arguments: [TemplateServiceError.serviceNotInitialized, .resourcePathNotFound, .templateNotFound, .contentNotRendered]) + func render(throws error: TemplateServiceError) async throws { + let service = TemplateServiceMock(action: .error(error), spy: spy) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.render([:], on: .template) + } + + #expect(spy.actions.isEmpty == true) + } + +} + +// MARK: - String+Constants + +private extension String { + static let content = "" + static let template = "" +} diff --git a/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift b/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift new file mode 100644 index 0000000..3b03345 --- /dev/null +++ b/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift @@ -0,0 +1,75 @@ +import ColibriLibrary +import Foundation + +final class TemplateServiceMock { + + // MARK: Properties + + private var actions: [Action] = [] + + private weak var spy: TemplateServiceSpy? + + // MARK: Initialisers + + init( + action: Action, + spy: TemplateServiceSpy? = nil + ) { + self.actions.append(action) + self.spy = spy + } + +} + +// MARK: - TemplateServicing + +extension TemplateServiceMock: TemplateServicing { + + // MARK: Functions + + @discardableResult + func render(_ object: Any, on template: String) async throws(TemplateServiceError) -> String { + guard let nextAction else { return .empty } + + switch nextAction { + case .error(let error): + throw error + case .render(let content): + try await spy?.render(object, on: template) + return content + } + } + +} + +// MARK: - Helpers + +private extension TemplateServiceMock { + + // MARK: Computed + + var nextAction: Action? { + guard !actions.isEmpty else { + return nil + } + + return actions.removeFirst() + } + +} + + +// MARK: - Actions + +extension TemplateServiceMock { + enum Action { + case error(TemplateServiceError) + case render(String) + } +} + +// MARK: - String+Constants + +private extension String { + static let empty = "" +} diff --git a/Test/Sources/Helpers/Spies/FileServiceSpy.swift b/Test/Sources/Helpers/Spies/FileServiceSpy.swift index 34ccd63..971e0a9 100644 --- a/Test/Sources/Helpers/Spies/FileServiceSpy.swift +++ b/Test/Sources/Helpers/Spies/FileServiceSpy.swift @@ -1,7 +1,6 @@ +import ColibriLibrary import Foundation -@testable import ColibriLibrary - final class FileServiceSpy { // MARK: Properties diff --git a/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift b/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift new file mode 100644 index 0000000..5922300 --- /dev/null +++ b/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift @@ -0,0 +1,38 @@ +import ColibriLibrary + +final class TemplateServiceSpy { + + // MARK: Properties + + private(set) var actions: [Action] = [] + +} + +// MARK: - TemplateServicing + +extension TemplateServiceSpy: TemplateServicing { + + // MARK: Functions + + @discardableResult + func render(_ object: Any, on template: String) async throws(TemplateServiceError) -> String { + actions.append(.rendered(object, template)) + + return .content + } + +} + +// MARK: - Actions + +extension TemplateServiceSpy { + enum Action { + case rendered(_ object: Any, _ template: String) + } +} + +// MARK: - String+Constants + +private extension String { + static let content = "" +} -- 2.47.1 From 7b1cb9ae2a31827c579fbd8ba45033115570744f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Feb 2025 23:24:35 +0100 Subject: [PATCH 69/80] Implemented the Project model in the library target. --- Library/Sources/Public/Models/Project.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Library/Sources/Public/Models/Project.swift diff --git a/Library/Sources/Public/Models/Project.swift b/Library/Sources/Public/Models/Project.swift new file mode 100644 index 0000000..9f34306 --- /dev/null +++ b/Library/Sources/Public/Models/Project.swift @@ -0,0 +1,13 @@ +public struct Project { + + // MARK: Properties + + let name: String + + // MARK: Initialisers + + public init(name: String) { + self.name = name + } + +} -- 2.47.1 From 87dc06991722ee5ccce9a1ca3c7e5f962dec7f79 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Feb 2025 23:49:56 +0100 Subject: [PATCH 70/80] Defined the Package, App, and AppTests mustache templates in the library target. --- Library/Resources/Files/Sources/License | 2 +- .../Files/Templates/App/App.mustache | 20 +++++++++ .../Files/Templates/Package.mustache | 45 +++++++++++++++++++ .../Files/Templates/Test/AppTests.mustache | 33 ++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 Library/Resources/Files/Templates/App/App.mustache create mode 100644 Library/Resources/Files/Templates/Package.mustache create mode 100644 Library/Resources/Files/Templates/Test/AppTests.mustache diff --git a/Library/Resources/Files/Sources/License b/Library/Resources/Files/Sources/License index bea052c..05b900f 100644 --- a/Library/Resources/Files/Sources/License +++ b/Library/Resources/Files/Sources/License @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Adam Fowler + Copyright 2025 Röck+Cöde Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Library/Resources/Files/Templates/App/App.mustache b/Library/Resources/Files/Templates/App/App.mustache new file mode 100644 index 0000000..c33bcd7 --- /dev/null +++ b/Library/Resources/Files/Templates/App/App.mustache @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser + +@main +struct App: AsyncParsableCommand { + + // MARK: Properties + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let builder = AppBuilder(name: "{{ name }}") + let app = try await builder(options) + + try await app.runService() + } + +} diff --git a/Library/Resources/Files/Templates/Package.mustache b/Library/Resources/Files/Templates/Package.mustache new file mode 100644 index 0000000..7d500db --- /dev/null +++ b/Library/Resources/Files/Templates/Package.mustache @@ -0,0 +1,45 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "{{ name }}", + platforms: [ + .macOS(.v14) + ], + products: [ + .executable(name: "App", targets: ["App"]), + .library(name: "AppLibrary", targets: ["AppLibrary"]) + ], + dependencies: [ + .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird"), + .target(name: "AppLibrary") + ], + path: "App" + ), + .target( + name: "AppLibrary", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Hummingbird", package: "hummingbird") + ], + path: "Library" + ), + .testTarget( + name: "AppTests", + dependencies: [ + .product(name: "HummingbirdTesting", package: "hummingbird"), + .target(name: "AppLibrary") + ], + path: "Test" + ) + ] +) diff --git a/Library/Resources/Files/Templates/Test/AppTests.mustache b/Library/Resources/Files/Templates/Test/AppTests.mustache new file mode 100644 index 0000000..1cf3444 --- /dev/null +++ b/Library/Resources/Files/Templates/Test/AppTests.mustache @@ -0,0 +1,33 @@ +import AppLibrary +import Hummingbird +import HummingbirdTesting +import Testing + +struct AppTests { + + // MARK: Properties + + private let arguments = TestArguments() + private let builder = AppBuilder(name: "{{ name }}") + + // MARK: Route tests + + @Test(arguments: ["/"]) + func routes(_ uri: String) async throws { + let app = try await builder(arguments) + + try await app.test(.router) { client in + try await client.execute(uri: uri, method: .get) { response in + #expect(response.status == .ok) + #expect(response.body == .empty) + } + } + } + +} + +// MARK: ByteBuffer+Constants + +private extension ByteBuffer { + static let empty = ByteBuffer(string: "") +} -- 2.47.1 From 91762b18cd0285319bc1d6aa644be23fe0c96771 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 5 Feb 2025 00:00:22 +0100 Subject: [PATCH 71/80] Implemented the Template enumeration in the library target. --- .../Internal/Enumerations/Template.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Library/Sources/Internal/Enumerations/Template.swift diff --git a/Library/Sources/Internal/Enumerations/Template.swift b/Library/Sources/Internal/Enumerations/Template.swift new file mode 100644 index 0000000..e43ac6d --- /dev/null +++ b/Library/Sources/Internal/Enumerations/Template.swift @@ -0,0 +1,37 @@ +enum Template: String { + case app = "App" + case appTests = "AppTests" + case package = "Package" +} + +// MARK: - Properties + +extension Template { + + // MARK: Computed + + var fileName: String { + switch self { + case .app: "App.swift" + case .appTests: "AppTests.swift" + case .package: "Package.swift" + } + } + + var filePath: String { + folder.path + fileName + } + + var folder: Folder { + switch self { + case .app: .app + case .appTests: .testCasesPublic + default: .root + } + } + +} + +// MARK: - CaseIterable + +extension Template: CaseIterable {} -- 2.47.1 From af73e6d1a2a1649322ec8fc8b9436fcc146133f1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 5 Feb 2025 00:16:09 +0100 Subject: [PATCH 72/80] Removed the "app", "appTests", and "package" cases from the File enumeration in the library target. --- Library/Resources/Files/Sources/App/App | 20 ------ Library/Resources/Files/Sources/Package | 45 ------------- Library/Resources/Files/Sources/Test/AppTests | 33 ---------- .../Sources/Internal/Enumerations/File.swift | 13 +--- .../Internal/Enumerations/FileTests.swift | 14 ---- .../Internal/Enumerations/TemplateTests.swift | 64 +++++++++++++++++++ 6 files changed, 67 insertions(+), 122 deletions(-) delete mode 100644 Library/Resources/Files/Sources/App/App delete mode 100644 Library/Resources/Files/Sources/Package delete mode 100644 Library/Resources/Files/Sources/Test/AppTests create mode 100644 Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift diff --git a/Library/Resources/Files/Sources/App/App b/Library/Resources/Files/Sources/App/App deleted file mode 100644 index 9183ae7..0000000 --- a/Library/Resources/Files/Sources/App/App +++ /dev/null @@ -1,20 +0,0 @@ -import AppLibrary -import ArgumentParser - -@main -struct App: AsyncParsableCommand { - - // MARK: Properties - - @OptionGroup var options: Options - - // MARK: Functions - - mutating func run() async throws { - let builder = AppBuilder(name: "App") - let app = try await builder(options) - - try await app.runService() - } - -} diff --git a/Library/Resources/Files/Sources/Package b/Library/Resources/Files/Sources/Package deleted file mode 100644 index 0c804dc..0000000 --- a/Library/Resources/Files/Sources/Package +++ /dev/null @@ -1,45 +0,0 @@ -// swift-tools-version:6.0 - -import PackageDescription - -let package = Package( - name: "App", - platforms: [ - .macOS(.v14) - ], - products: [ - .executable(name: "App", targets: ["App"]), - .library(name: "AppLibrary", targets: ["AppLibrary"]) - ], - dependencies: [ - .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") - ], - targets: [ - .executableTarget( - name: "App", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Hummingbird", package: "hummingbird"), - .target(name: "AppLibrary") - ], - path: "App" - ), - .target( - name: "AppLibrary", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Hummingbird", package: "hummingbird") - ], - path: "Library" - ), - .testTarget( - name: "AppTests", - dependencies: [ - .product(name: "HummingbirdTesting", package: "hummingbird"), - .target(name: "AppLibrary") - ], - path: "Test" - ) - ] -) diff --git a/Library/Resources/Files/Sources/Test/AppTests b/Library/Resources/Files/Sources/Test/AppTests deleted file mode 100644 index 31f1868..0000000 --- a/Library/Resources/Files/Sources/Test/AppTests +++ /dev/null @@ -1,33 +0,0 @@ -import AppLibrary -import Hummingbird -import HummingbirdTesting -import Testing - -struct AppTests { - - // MARK: Properties - - private let arguments = TestArguments() - private let builder = AppBuilder(name: "App") - - // MARK: Route tests - - @Test(arguments: ["/"]) - func routes(_ uri: String) async throws { - let app = try await builder(arguments) - - try await app.test(.router) { client in - try await client.execute(uri: uri, method: .get) { response in - #expect(response.status == .ok) - #expect(response.body == .empty) - } - } - } - -} - -// MARK: ByteBuffer+Constants - -private extension ByteBuffer { - static let empty = ByteBuffer(string: "") -} diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift index 861b114..50e68f0 100644 --- a/Library/Sources/Internal/Enumerations/File.swift +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -1,16 +1,13 @@ 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" @@ -24,11 +21,9 @@ extension File { 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" @@ -36,7 +31,6 @@ extension File { case .license: "LICENSE" case .loggerLevel: "LoggerLevel+Conformances.swift" case .readme: "README.md" - case .package: "Package.swift" case .testArguments: "TestArguments.swift" } } @@ -47,9 +41,8 @@ extension File { var folder: Folder { switch self { - case .app, .appOptions: .app + case .appOptions: .app case .appArguments, .appBuilder: .libraryPublic - case .appTests: .testCasesPublic case .environment, .loggerLevel: .libraryInternal case .testArguments: .testHelpers default: .root @@ -60,9 +53,9 @@ extension File { let basePath = "Resources/Files/Sources" return switch self { - case .app, .appOptions: "\(basePath)/App" + case .appOptions: "\(basePath)/App" case .appArguments, .appBuilder, .environment, .loggerLevel: "\(basePath)/Library" - case .appTests, .testArguments: "\(basePath)/Test" + case .testArguments: "\(basePath)/Test" default: basePath } } diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift index 8e3b884..0de6680 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -53,45 +53,37 @@ struct FileTests { private extension FileTests { enum Expectation { static let fileNames: [String] = [ - "App.swift", "AppArguments.swift", "AppBuilder.swift", "AppOptions.swift", - "AppTests.swift", "Dockerfile", ".dockerignore", "Environment+Properties.swift", ".gitignore", "LICENSE", "LoggerLevel+Conformances.swift", - "Package.swift", "README.md", "TestArguments.swift" ] static let filePaths: [String] = [ - "App/Sources/App.swift", "Library/Sources/Public/AppArguments.swift", "Library/Sources/Public/AppBuilder.swift", "App/Sources/AppOptions.swift", - "Test/Sources/Cases/Public/AppTests.swift", "Dockerfile", ".dockerignore", "Library/Sources/Internal/Environment+Properties.swift", ".gitignore", "LICENSE", "Library/Sources/Internal/LoggerLevel+Conformances.swift", - "Package.swift", "README.md", "Test/Sources/Helpers/TestArguments.swift" ] static let folders: [Folder] = [ - .app, .libraryPublic, .libraryPublic, .app, - .testCasesPublic, .root, .root, .libraryInternal, @@ -99,17 +91,13 @@ private extension FileTests { .root, .libraryInternal, .root, - .root, .testHelpers ] - static let resourcePaths: [String] = [ - "Resources/Files/Sources/App", "Resources/Files/Sources/Library", "Resources/Files/Sources/Library", "Resources/Files/Sources/App", - "Resources/Files/Sources/Test", "Resources/Files/Sources", "Resources/Files/Sources", "Resources/Files/Sources/Library", @@ -117,9 +105,7 @@ private extension FileTests { "Resources/Files/Sources", "Resources/Files/Sources/Library", "Resources/Files/Sources", - "Resources/Files/Sources", "Resources/Files/Sources/Test" ] } } - diff --git a/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift b/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift new file mode 100644 index 0000000..85b2486 --- /dev/null +++ b/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift @@ -0,0 +1,64 @@ +import Testing + +@testable import ColibriLibrary + +struct TemplateTests { + + // MARK: Properties tests + + @Test(arguments: zip(Template.allCases, Expectation.fileNames)) + func fileName(for template: Template, expects fileName: String) async throws { + // GIVEN + // WHEN + let result = template.fileName + + // THEN + #expect(result == fileName) + } + + @Test(arguments: zip(Template.allCases, Expectation.filePaths)) + func filePath(for template: Template, expects filePath: String) async throws { + // GIVEN + // WHEN + let result = template.filePath + + // THEN + #expect(result == filePath) + } + + @Test(arguments: zip(Template.allCases, Expectation.folders)) + func folder(for template: Template, expects folder: Folder) async throws { + // GIVEN + // WHEN + let result = template.folder + + // THEN + #expect(result == folder) + } + +} + +// MARK: - Expectations + +private extension TemplateTests { + enum Expectation { + static let fileNames: [String] = [ + "App.swift", + "AppTests.swift", + "Package.swift", + ] + + static let filePaths: [String] = [ + "App/Sources/App.swift", + "Test/Sources/Cases/Public/AppTests.swift", + "Package.swift", + ] + + static let folders: [Folder] = [ + .app, + .testCasesPublic, + .root, + ] + + } +} -- 2.47.1 From 3dcb110de158e50b912d5b901d710ef593e83240 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 5 Feb 2025 01:22:27 +0100 Subject: [PATCH 73/80] Fixed the names for some of the cases for the Template enumeration in the library target. --- Library/Sources/Internal/Enumerations/Template.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Library/Sources/Internal/Enumerations/Template.swift b/Library/Sources/Internal/Enumerations/Template.swift index e43ac6d..4d45a7c 100644 --- a/Library/Sources/Internal/Enumerations/Template.swift +++ b/Library/Sources/Internal/Enumerations/Template.swift @@ -1,6 +1,6 @@ enum Template: String { - case app = "App" - case appTests = "AppTests" + case app = "App/App" + case appTests = "Test/AppTests" case package = "Package" } -- 2.47.1 From 33ae67fc58543704f3515564e9c4263ef8520eca Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 7 Feb 2025 21:50:50 +0100 Subject: [PATCH 74/80] Implemented the "createFile(at: with: )" function for the FileService service in the library target. --- .../Public/Protocols/FileServicing.swift | 3 ++ .../Sources/Public/Services/FileService.swift | 32 +++++++++++++----- .../Public/Services/FileServiceTests.swift | 33 ++++++++++++++++++- .../Helpers/Mocks/FileServiceMock.swift | 14 ++++++++ .../Helpers/Spies/FileServiceSpy.swift | 5 +++ 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/Library/Sources/Public/Protocols/FileServicing.swift b/Library/Sources/Public/Protocols/FileServicing.swift index 4de66e3..db07292 100644 --- a/Library/Sources/Public/Protocols/FileServicing.swift +++ b/Library/Sources/Public/Protocols/FileServicing.swift @@ -9,6 +9,7 @@ public protocol FileServicing { // MARK: Functions func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) + func createFile(at location: URL, with data: Data) 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 @@ -18,6 +19,8 @@ public protocol FileServicing { // MARK: - Errors public enum FileServiceError: Error, Equatable { + case fileDataIsEmpty + case fileNotCreated case folderNotCreated case itemAlreadyExists case itemEmptyData diff --git a/Library/Sources/Public/Services/FileService.swift b/Library/Sources/Public/Services/FileService.swift index d9f56c5..47a3c0e 100644 --- a/Library/Sources/Public/Services/FileService.swift +++ b/Library/Sources/Public/Services/FileService.swift @@ -30,7 +30,7 @@ extension FileService: FileServicing { public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) { guard try await !isItemExists(at: destination) else { - throw FileServiceError.itemAlreadyExists + throw .itemAlreadyExists } var itemData: Data? @@ -38,43 +38,59 @@ extension FileService: FileServicing { do { itemData = try Data(contentsOf: source) } catch { - throw FileServiceError.itemEmptyData + throw .itemEmptyData } do { try itemData?.write(to: destination, options: .atomic) } catch { - throw FileServiceError.itemNotCopied + throw .itemNotCopied + } + } + + public func createFile(at location: URL, with data: Data) async throws (FileServiceError) { + guard try await !isItemExists(at: location) else { + throw .itemAlreadyExists + } + + guard !data.isEmpty else { + throw .fileDataIsEmpty + } + + do { + try data.write(to: location, options: .atomic) + } catch { + throw .fileNotCreated } } public func createFolder(at location: URL) async throws (FileServiceError) { guard try await !isItemExists(at: location) else { - throw FileServiceError.itemAlreadyExists + throw .itemAlreadyExists } do { try fileManager.createDirectory(at: location, withIntermediateDirectories: true) } catch { - throw FileServiceError.folderNotCreated + throw .folderNotCreated } } public func deleteItem(at location: URL) async throws (FileServiceError) { guard try await isItemExists(at: location) else { - throw FileServiceError.itemNotExists + throw .itemNotExists } do { try fileManager.removeItem(at: location) } catch { - throw FileServiceError.itemNotDeleted + throw .itemNotDeleted } } public func isItemExists(at location: URL) async throws (FileServiceError) -> Bool { guard location.isFileURL else { - throw FileServiceError.itemNotFileURL + throw .itemNotFileURL } let filePath = location.pathString diff --git a/Test/Sources/Cases/Public/Services/FileServiceTests.swift b/Test/Sources/Cases/Public/Services/FileServiceTests.swift index bf8f909..2292f71 100644 --- a/Test/Sources/Cases/Public/Services/FileServiceTests.swift +++ b/Test/Sources/Cases/Public/Services/FileServiceTests.swift @@ -35,7 +35,7 @@ struct FileServiceTests { // WHEN try await service.copyFile(from: source, to: destination) - // THENn + // THEN #expect(spy.actions.count == 1) let action = try #require(spy.actions.last) @@ -57,6 +57,37 @@ struct FileServiceTests { #expect(spy.actions.isEmpty == true) } + @Test(arguments: zip([URL.someNewFile], + [Data("some data goes here...".utf8)])) + func createFile(with location: URL, and data: Data) async throws { + // GIVEN + let service = service(action: .createFile(location, data)) + + // WHEN + try await service.createFile(at: location, with: data) + + // THEN + #expect(spy.actions.count == 1) + + let action = try #require(spy.actions.last) + + #expect(action == .fileCreated(location, data)) + } + + @Test(arguments: [FileServiceError.itemAlreadyExists, .fileDataIsEmpty, .fileNotCreated]) + func createFile(throws error: FileServiceError) async throws { + // GIVEN + let service = service(action: .error(error)) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.createFile(at: .someNewFile, with: .init()) + } + + #expect(spy.actions.isEmpty == true) + } + @Test(arguments: [URL.someNewFolder, .someNewFile]) func createFolder(with location: URL) async throws { // GIVEN diff --git a/Test/Sources/Helpers/Mocks/FileServiceMock.swift b/Test/Sources/Helpers/Mocks/FileServiceMock.swift index 99bd117..e0e70dd 100644 --- a/Test/Sources/Helpers/Mocks/FileServiceMock.swift +++ b/Test/Sources/Helpers/Mocks/FileServiceMock.swift @@ -64,6 +64,19 @@ extension FileServiceMock: FileServicing { } } + func createFile(at location: URL, with data: Data) async throws (FileServiceError) { + guard let nextAction else { return } + + switch nextAction { + case .error(let error): + throw error + case let .createFile(location, data): + try await spy?.createFile(at: location, with: data) + default: + break + } + } + func createFolder(at location: URL) async throws (FileServiceError) { guard let nextAction else { return } @@ -127,6 +140,7 @@ private extension FileServiceMock { extension FileServiceMock { enum Action { case copyFile(URL, URL) + case createFile(URL, Data) case createFolder(URL) case deleteItem(URL) case error(FileServiceError) diff --git a/Test/Sources/Helpers/Spies/FileServiceSpy.swift b/Test/Sources/Helpers/Spies/FileServiceSpy.swift index 971e0a9..3b9e90f 100644 --- a/Test/Sources/Helpers/Spies/FileServiceSpy.swift +++ b/Test/Sources/Helpers/Spies/FileServiceSpy.swift @@ -21,6 +21,10 @@ extension FileServiceSpy: FileServicing { actions.append(.fileCopied(source, destination)) } + func createFile(at location: URL, with data: Data) async throws (FileServiceError) { + actions.append(.fileCreated(location, data)) + } + func createFolder(at location: URL) async throws (FileServiceError) { actions.append(.folderCreated(location)) } @@ -42,6 +46,7 @@ extension FileServiceSpy: FileServicing { extension FileServiceSpy { enum Action: Equatable { + case fileCreated(_ location: URL, _ data: Data) case fileCopied(_ source: URL, _ destination: URL) case folderCreated(_ location: URL) case itemDeleted(_ location: URL) -- 2.47.1 From 07e8012faf017366dcae6e41272b35e7abccd034 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 8 Feb 2025 10:22:52 +0100 Subject: [PATCH 75/80] Implemented the TerminalService service for the library target --- .../Public/Protocols/TerminalServicing.swift | 19 +++++ .../Public/Services/TerminalService.swift | 60 +++++++++++++++ .../Services/TemplateServiceTests.swift | 1 + .../Services/TerminalServiceTests.swift | 51 +++++++++++++ .../Helpers/Mocks/TerminalServiceMock.swift | 73 +++++++++++++++++++ .../Helpers/Spies/TerminalServiceSpy.swift | 39 ++++++++++ 6 files changed, 243 insertions(+) create mode 100644 Library/Sources/Public/Protocols/TerminalServicing.swift create mode 100644 Library/Sources/Public/Services/TerminalService.swift create mode 100644 Test/Sources/Cases/Public/Services/TerminalServiceTests.swift create mode 100644 Test/Sources/Helpers/Mocks/TerminalServiceMock.swift create mode 100644 Test/Sources/Helpers/Spies/TerminalServiceSpy.swift diff --git a/Library/Sources/Public/Protocols/TerminalServicing.swift b/Library/Sources/Public/Protocols/TerminalServicing.swift new file mode 100644 index 0000000..b9f63e3 --- /dev/null +++ b/Library/Sources/Public/Protocols/TerminalServicing.swift @@ -0,0 +1,19 @@ +import Foundation + +public protocol TerminalServicing { + + // MARK: Functions + + @discardableResult + func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String + +} + +// MARK: - Errors + +public enum TerminalServiceError: Error, Equatable { + case captured(_ output: String) + case output(_ output: String) + case unexpected +} + diff --git a/Library/Sources/Public/Services/TerminalService.swift b/Library/Sources/Public/Services/TerminalService.swift new file mode 100644 index 0000000..78105e1 --- /dev/null +++ b/Library/Sources/Public/Services/TerminalService.swift @@ -0,0 +1,60 @@ +import Foundation + +public struct TerminalService { + + // MARK: Initialisers + + public init() {} + +} + +// MARK: - TerminalServicing + +extension TerminalService: TerminalServicing { + + // MARK: Functions + + public func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String { + let process = Process() + let standardError = Pipe() + let standardOutput = Pipe() + + process.executableURL = executableURL + process.arguments = arguments + process.standardError = standardError + process.standardOutput = standardOutput + + async let streamOutput = standardOutput.availableData.append() + async let streamError = standardError.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 TerminalServiceError.unexpected + } + + throw TerminalServiceError.output(errorOutput) + } + + guard let output = String(data: dataOutput, encoding: .utf8) else { + throw TerminalServiceError.unexpected + } + + return await withCheckedContinuation { continuation in + process.terminationHandler = { _ in + continuation.resume(returning: output) + } + } + } catch let error as TerminalServiceError { + throw error + } catch { + throw .captured(error.localizedDescription) + } + } + +} diff --git a/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift b/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift index 5da5fa3..3bd7852 100644 --- a/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift +++ b/Test/Sources/Cases/Public/Services/TemplateServiceTests.swift @@ -26,6 +26,7 @@ struct TemplateServiceTests { @Test(arguments: [TemplateServiceError.serviceNotInitialized, .resourcePathNotFound, .templateNotFound, .contentNotRendered]) func render(throws error: TemplateServiceError) async throws { + // GIVEN let service = TemplateServiceMock(action: .error(error), spy: spy) // WHEN diff --git a/Test/Sources/Cases/Public/Services/TerminalServiceTests.swift b/Test/Sources/Cases/Public/Services/TerminalServiceTests.swift new file mode 100644 index 0000000..f644c2b --- /dev/null +++ b/Test/Sources/Cases/Public/Services/TerminalServiceTests.swift @@ -0,0 +1,51 @@ +import ColibriLibrary +import Foundation +import Testing + +struct TerminalServiceTests { + + // MARK: Properties + + private let spy = TerminalServiceSpy() + + // MARK: Functions + + @Test(arguments: [URL.someNewFile], [[], ["--example"], ["--example", "--more", "--etc"]]) + func run(with executableURL: URL, and arguments: [String]) async throws { + // GIVEN + let service = TerminalServiceMock(action: .run(executableURL, arguments), spy: spy) + + // WHEN + let result = try await service.run(executableURL, arguments: arguments) + + // THEN + #expect(result == .content) + + #expect(spy.actions.isEmpty == false) + + let action = try #require(spy.actions.last) + + #expect(action == .ran(executableURL, arguments)) + } + + @Test(arguments: [TerminalServiceError.unexpected, .captured("Some captured error"), .output("Some output error")]) + func run(throws error: TerminalServiceError) async throws { + // GIVEN + let service = TerminalServiceMock(action: .error(error), spy: spy) + + // WHEN + // THEN + await #expect(throws: error) { + try await service.run(URL.someNewFile, arguments: []) + } + + #expect(spy.actions.isEmpty == true) + } + +} + +// MARK: - String+Constants + +private extension String { + static let content = "" +} diff --git a/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift b/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift new file mode 100644 index 0000000..3a1344d --- /dev/null +++ b/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift @@ -0,0 +1,73 @@ +import ColibriLibrary +import Foundation + +final class TerminalServiceMock { + + // MARK: Properties + + private var actions: [Action] = [] + + private weak var spy: TerminalServiceSpy? + + // MARK: Initialisers + + init( + action: Action, + spy: TerminalServiceSpy? = nil + ) { + self.actions.append(action) + self.spy = spy + } + +} + +// MARK: - TerminalServicing + +extension TerminalServiceMock: TerminalServicing { + + // MARK: Functions + + func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String { + guard let nextAction else { return .empty } + + switch nextAction { + case .error(let error): + throw error + case let .run(executableURL, arguments): + try await spy?.run(executableURL, arguments: arguments) + return .empty + } + } + +} + +// MARK: - Helpers + +private extension TerminalServiceMock { + + // MARK: Computed + + var nextAction: Action? { + guard !actions.isEmpty else { + return nil + } + + return actions.removeFirst() + } + +} + +// MARK: - Actions + +extension TerminalServiceMock { + enum Action { + case error(TerminalServiceError) + case run(URL, [String]) + } +} + +// MARK: - String+Constants + +private extension String { + static let empty = "" +} diff --git a/Test/Sources/Helpers/Spies/TerminalServiceSpy.swift b/Test/Sources/Helpers/Spies/TerminalServiceSpy.swift new file mode 100644 index 0000000..0c9a621 --- /dev/null +++ b/Test/Sources/Helpers/Spies/TerminalServiceSpy.swift @@ -0,0 +1,39 @@ +import ColibriLibrary +import Foundation + +final class TerminalServiceSpy { + + // MARK: Properties + + private(set) var actions: [Action] = [] + +} + +// MARK: - TerminalServicing + +extension TerminalServiceSpy: TerminalServicing { + + // MARK: Functions + + @discardableResult + func run(_ executableURL: URL, arguments: [String]) async throws(TerminalServiceError) -> String { + actions.append(.ran(executableURL, arguments)) + + return .content + } + +} + +// MARK: - Actions + +extension TerminalServiceSpy { + enum Action: Equatable { + case ran(_ executableURL: URL, _ arguments: [String]) + } +} + +// MARK: - String+Constants + +private extension String { + static let content = "" +} -- 2.47.1 From 2698e1e29ce11ec12e4092aa67c80032fb7923df Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 8 Feb 2025 12:20:53 +0100 Subject: [PATCH 76/80] Removed the RunProcessTask task from the library target. --- .../Internal/Tasks/RunProcessTask.swift | 75 ------------------- .../Internal/Tasks/RunProcessTaskTests.swift | 68 ----------------- 2 files changed, 143 deletions(-) delete mode 100644 Library/Sources/Internal/Tasks/RunProcessTask.swift delete mode 100644 Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift diff --git a/Library/Sources/Internal/Tasks/RunProcessTask.swift b/Library/Sources/Internal/Tasks/RunProcessTask.swift deleted file mode 100644 index ea267b4..0000000 --- a/Library/Sources/Internal/Tasks/RunProcessTask.swift +++ /dev/null @@ -1,75 +0,0 @@ -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 -} diff --git a/Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift b/Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift deleted file mode 100644 index f493e1f..0000000 --- a/Test/Sources/Cases/Internal/Tasks/RunProcessTaskTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import Testing - -@testable import ColibriLibrary - -struct RunProcessTaskTests { - - // MARK: Properties - - private var process: Process - - // MARK: Initialisers - - init() { - self.process = Process() - } - - // MARK: Functions tests - - @Test(arguments: [Argument.empty, Argument.listAllInFolder]) - func run(with arguments: [String]) async throws { - // GIVEN - var task = RunProcessTask(process: process) - - // WHEN - let output = try await task(path: .ls, arguments: arguments) - - // THEN - #expect(output.isEmpty == false) - } - - @Test(arguments: zip([Argument.help, Argument.listAllInPWD], Throw.outputs)) - func runThrows(with arguments: [String], throws error: RunProcessError) async throws { - // GIVEN - var task = RunProcessTask(process: process) - - // WHEN - // THEN - await #expect(throws: error) { - try await task(path: .ls, arguments: arguments) - } - } - -} - -// MARK: - String+Constants - -private extension String { - static let ls = "/bin/ls" -} - -// MARK: - Parameters - -private extension RunProcessTaskTests { - enum Argument { - static let empty: [String] = [] - static let help: [String] = ["--help"] - static let listAllInFolder: [String] = ["-la", "."] - static let listAllInPWD: [String] = ["-la", "~"] - } - - enum Throw { - static let outputs: [RunProcessError] = [ - .output("ls: unrecognized option `--help\'\nusage: ls [-@ABCFGHILOPRSTUWXabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n"), - .output("ls: ~: No such file or directory\n") - ] - } -} -- 2.47.1 From 5f5f9027735ead7c611a64f698a29d0a12863adc Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 8 Feb 2025 12:21:42 +0100 Subject: [PATCH 77/80] Implemented the InitGitInFolderTask task in the library target. --- .../Public/Tasks/InitGitInFolderTask.swift | 22 +++++++------ .../Tasks/InitGitInFolderTaskTests.swift | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift diff --git a/Library/Sources/Public/Tasks/InitGitInFolderTask.swift b/Library/Sources/Public/Tasks/InitGitInFolderTask.swift index 850e1c2..fda011f 100644 --- a/Library/Sources/Public/Tasks/InitGitInFolderTask.swift +++ b/Library/Sources/Public/Tasks/InitGitInFolderTask.swift @@ -1,24 +1,26 @@ import Foundation public struct InitGitInFolderTask { + + // MARK: Properties + + private let terminalService: TerminalServicing // MARK: Initialisers - public init() {} + public init(terminalService: TerminalServicing) { + self.terminalService = terminalService + } // MARK: Functions - public func callAsFunction(at rootFolder: URL) async throws (RunProcessError) { - let pathCommand = "/usr/bin/git" + public func callAsFunction(at rootFolder: URL) async throws (TerminalServiceError) { + let executableURL = URL(at: "/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"]) + try await terminalService.run(executableURL, arguments: ["init", pathFolder]) + try await terminalService.run(executableURL, arguments: ["-C", pathFolder, "add", "."]) + try await terminalService.run(executableURL, arguments: ["-C", pathFolder, "commit", "-m", "Initial commit"]) } } diff --git a/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift b/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift new file mode 100644 index 0000000..6f687de --- /dev/null +++ b/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift @@ -0,0 +1,32 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct InitGitInFolderTaskTests { + + // MARK: Properties + + private let spy = TerminalServiceSpy() + + // MARK: + + @Test(arguments: [URL.someCurrentFolder, .someNewFolder, .someDotFolder, .someTildeFolder]) + func task(at rootFolder: URL) async throws { + // GIVEN + let initGitInFolder = InitGitInFolderTask(terminalService: spy) + + // WHEN + try await initGitInFolder(at: rootFolder) + + // THEN + let executableURL = URL(at: "/usr/bin/git") + let pathFolder = rootFolder.pathString + + #expect(spy.actions.count == 3) + #expect(spy.actions[0] == .ran(executableURL, ["init", pathFolder])) + #expect(spy.actions[1] == .ran(executableURL, ["-C", pathFolder, "add", "."])) + #expect(spy.actions[2] == .ran(executableURL, ["-C", pathFolder, "commit", "-m", "Initial commit"])) + } + +} -- 2.47.1 From 94e60700918eb24313c3791b0c2eeda3c11fabcd Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 8 Feb 2025 12:55:31 +0100 Subject: [PATCH 78/80] Implemented the RenderFilesTask task in the library target. --- Library/Sources/Public/Models/Project.swift | 2 +- .../Public/Tasks/RenderFilesTask.swift | 34 +++++++++++++++++++ .../Internal/Enumerations/TemplateTests.swift | 1 - .../Tasks/InitGitInFolderTaskTests.swift | 20 +++++------ 4 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 Library/Sources/Public/Tasks/RenderFilesTask.swift diff --git a/Library/Sources/Public/Models/Project.swift b/Library/Sources/Public/Models/Project.swift index 9f34306..d1505e5 100644 --- a/Library/Sources/Public/Models/Project.swift +++ b/Library/Sources/Public/Models/Project.swift @@ -1,4 +1,4 @@ -public struct Project { +public struct Project: Sendable { // MARK: Properties diff --git a/Library/Sources/Public/Tasks/RenderFilesTask.swift b/Library/Sources/Public/Tasks/RenderFilesTask.swift new file mode 100644 index 0000000..ae9500e --- /dev/null +++ b/Library/Sources/Public/Tasks/RenderFilesTask.swift @@ -0,0 +1,34 @@ +import Foundation + +public struct RenderFilesTask { + + // MARK: Computed + + private let fileService: FileServicing + private let templateService: TemplateServicing + + // MARK: Initialisers + + public init( + fileService: FileServicing, + templateService: TemplateServicing + ) { + self.fileService = fileService + self.templateService = templateService + } + + // MARK: Functions + + public func callAsFunction( + at rootFolder: URL, + with model: Project + ) async throws { + for template in Template.allCases { + let content = try await templateService.render(model, on: template.rawValue) + let fileURL = rootFolder.appendingPath(template.filePath) + + try await fileService.createFile(at: fileURL, with: Data(content.utf8)) + } + } + +} diff --git a/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift b/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift index 85b2486..6476eed 100644 --- a/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift @@ -59,6 +59,5 @@ private extension TemplateTests { .testCasesPublic, .root, ] - } } diff --git a/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift b/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift index 6f687de..c7ef73e 100644 --- a/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift +++ b/Test/Sources/Cases/Public/Tasks/InitGitInFolderTaskTests.swift @@ -4,17 +4,15 @@ import Testing @testable import ColibriLibrary struct InitGitInFolderTaskTests { - - // MARK: Properties - - private let spy = TerminalServiceSpy() - - // MARK: + + // MARK: Functions tests @Test(arguments: [URL.someCurrentFolder, .someNewFolder, .someDotFolder, .someTildeFolder]) func task(at rootFolder: URL) async throws { // GIVEN - let initGitInFolder = InitGitInFolderTask(terminalService: spy) + let terminalService = TerminalServiceSpy() + + let initGitInFolder = InitGitInFolderTask(terminalService: terminalService) // WHEN try await initGitInFolder(at: rootFolder) @@ -23,10 +21,10 @@ struct InitGitInFolderTaskTests { let executableURL = URL(at: "/usr/bin/git") let pathFolder = rootFolder.pathString - #expect(spy.actions.count == 3) - #expect(spy.actions[0] == .ran(executableURL, ["init", pathFolder])) - #expect(spy.actions[1] == .ran(executableURL, ["-C", pathFolder, "add", "."])) - #expect(spy.actions[2] == .ran(executableURL, ["-C", pathFolder, "commit", "-m", "Initial commit"])) + #expect(terminalService.actions.count == 3) + #expect(terminalService.actions[0] == .ran(executableURL, ["init", pathFolder])) + #expect(terminalService.actions[1] == .ran(executableURL, ["-C", pathFolder, "add", "."])) + #expect(terminalService.actions[2] == .ran(executableURL, ["-C", pathFolder, "commit", "-m", "Initial commit"])) } } -- 2.47.1 From e27d72cdc245d084506266488d9c22dbbb99198c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 8 Feb 2025 12:57:35 +0100 Subject: [PATCH 79/80] Integrated the TerminalService service as well as the InitGitInFolderTask and RenderFilesTask tasks in the CreatedCommand command in the executable target. --- Executable/Sources/Commands/CreateCommand.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Executable/Sources/Commands/CreateCommand.swift b/Executable/Sources/Commands/CreateCommand.swift index 9f35af7..8bae799 100644 --- a/Executable/Sources/Commands/CreateCommand.swift +++ b/Executable/Sources/Commands/CreateCommand.swift @@ -19,19 +19,23 @@ extension Colibri { mutating func run() async throws { let fileService = FileService() - + let templateService = try await TemplateService(templateFolder: "Files/Templates") + let terminalService = TerminalService() + let copyFiles = CopyFilesTask(fileService: fileService) let createFolders = CreateFoldersTask(fileService: fileService) let createRootFolder = CreateRootFolderTask(fileService: fileService) - let initGitInFolder = InitGitInFolderTask() + let initGitInFolder = InitGitInFolderTask(terminalService: terminalService) + let renderFiles = RenderFilesTask(fileService: fileService, + templateService: templateService) - let rootFolder = try await createRootFolder( - name: options.name, - at: options.locationURL - ) + let rootFolder = try await createRootFolder(name: options.name, + at: options.locationURL) try await createFolders(at: rootFolder) try await copyFiles(to: rootFolder) + try await renderFiles(at: rootFolder, + with: Project(name: options.name)) try await initGitInFolder(at: rootFolder) } -- 2.47.1 From 75b806af3d086a536302c2fe87dea44a09d12c5a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 17 Feb 2025 23:01:27 +0100 Subject: [PATCH 80/80] Written test cases for the RenderFileTask task in the unit tests target. --- Library/Sources/Public/Models/Project.swift | 2 +- .../Public/Tasks/RenderFilesTaskTests.swift | 41 +++++++++++++++++++ .../Helpers/Spies/TemplateServiceSpy.swift | 2 +- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 Test/Sources/Cases/Public/Tasks/RenderFilesTaskTests.swift diff --git a/Library/Sources/Public/Models/Project.swift b/Library/Sources/Public/Models/Project.swift index d1505e5..730439c 100644 --- a/Library/Sources/Public/Models/Project.swift +++ b/Library/Sources/Public/Models/Project.swift @@ -1,4 +1,4 @@ -public struct Project: Sendable { +public struct Project: Equatable, Sendable { // MARK: Properties diff --git a/Test/Sources/Cases/Public/Tasks/RenderFilesTaskTests.swift b/Test/Sources/Cases/Public/Tasks/RenderFilesTaskTests.swift new file mode 100644 index 0000000..08cc1ed --- /dev/null +++ b/Test/Sources/Cases/Public/Tasks/RenderFilesTaskTests.swift @@ -0,0 +1,41 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct RenderFilesTaskTests { + + @Test(arguments: [URL.someCurrentFolder], [Project(name: "Some name goes here...")]) + func task(at rootFolder: URL, with project: Project) async throws { + // GIVEN + let fileService = FileServiceSpy() + let templateService = TemplateServiceSpy() + + let renderFiles = RenderFilesTask(fileService: fileService, + templateService: templateService) + + // WHEN + try await renderFiles(at: rootFolder, with: project) + + // THEN + let fileData = Data() + let templates = Template.allCases + + #expect(fileService.actions.count == 3) + #expect(templateService.actions.count == 3) + + fileService.actions.enumerated().forEach { index, action in + #expect(action == .fileCreated(rootFolder.appendingPath(templates[index].filePath), fileData)) + } + + templateService.actions.enumerated().forEach { index, action in + if case let .rendered(object, template) = action { + #expect(object as? Project == project) + #expect(template == templates[index].rawValue) + } else { + Issue.record("Action should have been a case of the `TemplateServiceSpy.Action` enumeration.") + } + } + } + +} diff --git a/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift b/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift index 5922300..cc8a0c5 100644 --- a/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift +++ b/Test/Sources/Helpers/Spies/TemplateServiceSpy.swift @@ -15,7 +15,7 @@ extension TemplateServiceSpy: TemplateServicing { // MARK: Functions @discardableResult - func render(_ object: Any, on template: String) async throws(TemplateServiceError) -> String { + func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String { actions.append(.rendered(object, template)) return .content -- 2.47.1