diff --git a/Package.swift b/Package.swift index ee96ab2..e7c098f 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,8 @@ let package = Package( ] ), .testTarget( - name: "PackageDSLKitTests" + name: "PackageDSLKitTests", + dependencies: ["PackageDSLKit"] ) ] ) diff --git a/Sources/PackageDSLKit/Component.swift b/Sources/PackageDSLKit/Component.swift index aa1411e..0b45cf0 100644 --- a/Sources/PackageDSLKit/Component.swift +++ b/Sources/PackageDSLKit/Component.swift @@ -27,8 +27,8 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal struct Component: Sendable, Hashable, Codable { - internal let name: String - internal let inheritedTypes: [String] - internal let properties: [String: Property] +public struct Component: Sendable, Hashable, Codable { + public let name: String + public let inheritedTypes: [String] + public let properties: [String: Property] } diff --git a/Sources/PackageDSLKit/ComponentWriter.swift b/Sources/PackageDSLKit/ComponentWriter.swift index 035a2e0..e917b6c 100644 --- a/Sources/PackageDSLKit/ComponentWriter.swift +++ b/Sources/PackageDSLKit/ComponentWriter.swift @@ -29,11 +29,18 @@ import SwiftSyntax -internal struct ComponentWriter: Sendable, Hashable, Codable { - private let propertyWriter = PropertyWriter() - internal func node(from component: Component) -> StructDeclSyntax { +public struct ComponentWriter: Sendable, StructureWriter { + private let propertyWriter: @Sendable (Property) -> VariableDeclSyntax + + public init( + propertyWriter: @escaping @Sendable (Property) -> VariableDeclSyntax = PropertyWriter.node + ) { + self.propertyWriter = propertyWriter + } + + public func node(from component: Component) -> StructDeclSyntax { let memberBlockList = MemberBlockItemListSyntax( - component.properties.values.map(propertyWriter.node(from:)).map { + component.properties.values.map(propertyWriter).map { MemberBlockItemSyntax(decl: $0) } ) diff --git a/Sources/PackageDSLKit/Dependency.swift b/Sources/PackageDSLKit/Dependency.swift index 679a258..b926458 100644 --- a/Sources/PackageDSLKit/Dependency.swift +++ b/Sources/PackageDSLKit/Dependency.swift @@ -40,7 +40,22 @@ public struct Dependency: TypeSource { public init(rawValue: Int) { self.rawValue = rawValue } - public init?(strings: [String]) { + internal struct InvalidValueError: Error { + internal init?(invalidCount: Int) { + guard invalidCount != 0 else { + return nil + } + assert(invalidCount > 0) + self.invalidCount = invalidCount + } + + internal init?(valuesCount: Int, indiciesCount: Int) { + self.init(invalidCount: indiciesCount - valuesCount) + } + + internal let invalidCount: Int + } + internal init?(stringsThrows strings: [String]) throws(InvalidValueError) { let indicies = strings.map { Self.strings.firstIndex(of: $0) } @@ -48,10 +63,22 @@ public struct Dependency: TypeSource { if rawValues.isEmpty { return nil } - assert(rawValues.count == indicies.count) + if let error = InvalidValueError(valuesCount: rawValues.count, indiciesCount: indicies.count) + { + assert(error.invalidCount > 0) + throw error + } let rawValue = rawValues.reduce(0) { $0 + $1 } self.init(rawValue: rawValue) } + public init?(strings: [String]) { + do { + try self.init(stringsThrows: strings) + } catch { + assertionFailure("Invalid Values Passed: \(error.invalidCount)") + return nil + } + } internal func asInheritedTypes() -> [String] { rawValue.powerOfTwoExponents().map { Self.strings[$0] } diff --git a/Sources/package/FileManager.swift b/Sources/PackageDSLKit/FileManager.swift similarity index 83% rename from Sources/package/FileManager.swift rename to Sources/PackageDSLKit/FileManager.swift index c15b833..9177030 100644 --- a/Sources/package/FileManager.swift +++ b/Sources/PackageDSLKit/FileManager.swift @@ -28,11 +28,74 @@ // import Foundation -import PackageDSLKit -@available(*, deprecated, message: "Migrate to separate protocol.") -extension FileManager { - internal func swiftVersion(from directoryURL: URL) -> SwiftVersion? { +extension FileManager: PackageFilesInterface { + public var currentDirectoryURL: URL { + URL(fileURLWithPath: currentDirectoryPath) + } + + private func readDirectoryContents(at path: String, fileExtension: String = "swift") throws + -> [String] + { + var contents: [String] = [] + let items = try contentsOfDirectory(atPath: path) + + // Process subdirectories (post-order) + for item in items { + let itemPath = (path as NSString).appendingPathComponent(item) + var isDirectory: ObjCBool = false + let fileExists = fileExists(atPath: itemPath, isDirectory: &isDirectory) + + if fileExists && isDirectory.boolValue { + contents += try readDirectoryContents(at: itemPath, fileExtension: fileExtension) + } + } + + // Process files + for item in items where item.hasSuffix(".\(fileExtension)") { + let itemPath = (path as NSString).appendingPathComponent(item) + + let fileContents = try String(contentsOfFile: itemPath, encoding: .utf8) + contents.append(fileContents) + } + + return contents + } + public func writePackageSwiftFile( + swiftVersion: SwiftVersion, + from dslSourcesURL: URL, + to pathURL: URL + ) throws { + let contents = try self.readDirectoryContents( + at: dslSourcesURL.path(), + fileExtension: "swift" + ) + + let packageFileURL = pathURL.appendingPathComponent("Package.swift") + let strings = + [ + "// swift-tools-version: \(swiftVersion)", + SupportCodeBlock.syntaxNode.trimmedDescription, + ] + contents + let data = Data(strings.joined(separator: "\n").utf8) + self.createFile(atPath: packageFileURL.path(), contents: data) + // TODO: log error if file creation fails + } + + public func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool) + throws + { + try self.createDirectory( + at: url, + withIntermediateDirectories: createIntermediates, + attributes: nil + ) + } + + public func createFile(at url: URL, text: String) { + self.createFile(atPath: url.path(), contents: Data(text.utf8)) + } + public func swiftVersion(from directoryURL: URL) -> SwiftVersion? { let swiftVersionURL = directoryURL.appending(component: ".swift-version") let packageSwiftURL = directoryURL.appending(component: "Package.swift") @@ -53,7 +116,7 @@ extension FileManager { return .readFrom(packageSwiftFileURL: packageSwiftURL) } - internal func createTargetSourceAt( + public func createTargetSourceAt( _ pathURL: URL, productName: String, _ productType: ProductType ) throws { let sourcesDirURL = pathURL.appendingPathComponent("Sources/\(productName)") @@ -82,27 +145,24 @@ extension FileManager { contents: Data(sourceCode.utf8) ) } - internal func createTargetSourceAt( - _ pathURL: URL, productName: String, _ packageType: PackageType + public func createFileStructure( + forPackageType packageType: PackageType, + forProductName productName: String, + at pathURL: URL ) throws { - let productType: ProductType? - - switch packageType { - case .empty: - productType = nil - case .library: - productType = .library - case .executable: - productType = .executable + guard packageType != .empty else { + return } - assert(productType != nil, "Unknown package type \(packageType)") - guard let productType else { + + try self.createTargetSourceAt(pathURL, productName: productName, packageType) + + guard packageType == .library else { return } - try self.createTargetSourceAt(pathURL, productName: productName, productType) - } - fileprivate func createTestTargetAt(_ pathURL: URL, _ productName: String) throws { + try createTestTargetAt(pathURL, productName) + } + private func createTestTargetAt(_ pathURL: URL, _ productName: String) throws { let testingDirURL = pathURL.appendingPathComponent("Tests/\(productName)Tests") try self.createDirectory(at: testingDirURL, withIntermediateDirectories: true) @@ -117,68 +177,23 @@ extension FileManager { """ self.createFile(atPath: testFileURL.path(), contents: Data(testCode.utf8)) } - - internal func createFileStructure( - forPackageType packageType: PackageType, - forProductName productName: String, - at pathURL: URL - ) throws { - guard packageType != .empty else { - return - } - - try self.createTargetSourceAt(pathURL, productName: productName, packageType) - - guard packageType == .library else { - return - } - - try createTestTargetAt(pathURL, productName) - } - internal func writePackageSwiftFile( - swiftVersion: SwiftVersion, - from dslSourcesURL: URL, - to pathURL: URL + private func createTargetSourceAt( + _ pathURL: URL, productName: String, _ packageType: PackageType ) throws { - let contents = try self.readDirectoryContents( - at: dslSourcesURL.path(), - fileExtension: "swift" - ) - - let packageFileURL = pathURL.appendingPathComponent("Package.swift") - let strings = - [ - "// swift-tools-version: \(swiftVersion)", - SupportCodeBlock.syntaxNode.trimmedDescription, - ] + contents - let data = Data(strings.joined(separator: "\n").utf8) - self.createFile(atPath: packageFileURL.path(), contents: data) - } - internal func readDirectoryContents(at path: String, fileExtension: String = "swift") throws - -> [String] - { - var contents: [String] = [] - let items = try contentsOfDirectory(atPath: path) - - // Process subdirectories (post-order) - for item in items { - let itemPath = (path as NSString).appendingPathComponent(item) - var isDirectory: ObjCBool = false - fileExists(atPath: itemPath, isDirectory: &isDirectory) + let productType: ProductType? - if isDirectory.boolValue { - contents += try readDirectoryContents(at: itemPath, fileExtension: fileExtension) - } + switch packageType { + case .empty: + productType = nil + case .library: + productType = .library + case .executable: + productType = .executable } - - // Process files - for item in items where item.hasSuffix(".\(fileExtension)") { - let itemPath = (path as NSString).appendingPathComponent(item) - - let fileContents = try String(contentsOfFile: itemPath, encoding: .utf8) - contents.append(fileContents) + assert(productType != nil, "Unknown package type \(packageType)") + guard let productType else { + return } - - return contents + try self.createTargetSourceAt(pathURL, productName: productName, productType) } } diff --git a/Sources/PackageDSLKit/IndexCodeWriter.swift b/Sources/PackageDSLKit/IndexCodeWriter.swift new file mode 100644 index 0000000..3be065c --- /dev/null +++ b/Sources/PackageDSLKit/IndexCodeWriter.swift @@ -0,0 +1,34 @@ +// +// IndexCodeWriter.swift +// PackageDSLKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +public protocol IndexCodeWriter: Sendable { + func writeIndex(_ index: Index) throws(PackageDSLError) -> String +} diff --git a/Sources/PackageDSLKit/PackageFiles.swift b/Sources/PackageDSLKit/PackageFiles.swift new file mode 100644 index 0000000..0c3ff9e --- /dev/null +++ b/Sources/PackageDSLKit/PackageFiles.swift @@ -0,0 +1,51 @@ +// +// PackageFiles.swift +// PackageDSLKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +public struct PackageFiles: PackageFilesFactory { + public static let `default`: PackageFilesFactory = PackageFiles() + + private static let defaultTypes: + [PackageFilesInterfaceType: @Sendable () -> any PackageFilesInterface] = [ + .fileManager: { FileManager.default } + ] + + private let types: [PackageFilesInterfaceType: @Sendable () -> any PackageFilesInterface] + + internal init( + types: [PackageFilesInterfaceType: @Sendable () -> any PackageFilesInterface]? = nil + ) { + self.types = types ?? Self.defaultTypes + } + + public func interface(for type: PackageFilesInterfaceType) -> any PackageFilesInterface { + self.types[type]!() + } +} diff --git a/Sources/PackageDSLKit/PackageFilesFactory.swift b/Sources/PackageDSLKit/PackageFilesFactory.swift new file mode 100644 index 0000000..cbe12e2 --- /dev/null +++ b/Sources/PackageDSLKit/PackageFilesFactory.swift @@ -0,0 +1,32 @@ +// +// PackageFilesFactory.swift +// PackageDSLKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public protocol PackageFilesFactory: Sendable { + func interface(for type: PackageFilesInterfaceType) -> any PackageFilesInterface +} diff --git a/Sources/PackageDSLKit/PackageFilesInterface.swift b/Sources/PackageDSLKit/PackageFilesInterface.swift new file mode 100644 index 0000000..f0f0cc7 --- /dev/null +++ b/Sources/PackageDSLKit/PackageFilesInterface.swift @@ -0,0 +1,59 @@ +// +// PackageFilesInterface.swift +// PackageDSLKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +public protocol PackageFilesInterface { + var currentDirectoryURL: URL { get } + + func createDirectory( + at url: URL, + withIntermediateDirectories createIntermediates: Bool + ) throws + + func createFile(at url: URL, text: String) + + func swiftVersion(from directoryURL: URL) -> SwiftVersion? + + func writePackageSwiftFile( + swiftVersion: SwiftVersion, + from dslSourcesURL: URL, + to pathURL: URL + ) throws + + func createFileStructure( + forPackageType packageType: PackageType, + forProductName productName: String, + at pathURL: URL + ) throws + + func createTargetSourceAt( + _ pathURL: URL, productName: String, _ productType: ProductType + ) throws +} diff --git a/Sources/PackageDSLKit/FileManagerType.swift b/Sources/PackageDSLKit/PackageFilesInterfaceType.swift similarity index 90% rename from Sources/PackageDSLKit/FileManagerType.swift rename to Sources/PackageDSLKit/PackageFilesInterfaceType.swift index 21400cf..1a8cec5 100644 --- a/Sources/PackageDSLKit/FileManagerType.swift +++ b/Sources/PackageDSLKit/PackageFilesInterfaceType.swift @@ -1,5 +1,5 @@ // -// FileManagerType.swift +// PackageFilesInterfaceType.swift // PackageDSLKit // // Created by Leo Dion. @@ -27,6 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public enum FileManagerType: String, CaseIterable, Sendable, Hashable, Codable { +public enum PackageFilesInterfaceType: String, CaseIterable, Sendable, Hashable, Codable { case fileManager } diff --git a/Sources/PackageDSLKit/PackageIndexStrategy.swift b/Sources/PackageDSLKit/PackageIndexStrategy.swift index b156966..ec2dade 100644 --- a/Sources/PackageDSLKit/PackageIndexStrategy.swift +++ b/Sources/PackageDSLKit/PackageIndexStrategy.swift @@ -37,8 +37,8 @@ import SwiftSyntax internal class PackageIndexStrategy: ParsingStrategy { internal struct Child: Sendable, Hashable, Codable { - let kind: ExpressionKind - let name: String + internal let kind: ExpressionKind + internal let name: String } internal enum ExpressionKind: String, Sendable, Hashable, Codable { case entries diff --git a/Sources/PackageDSLKit/PackageIndexWriter.swift b/Sources/PackageDSLKit/PackageIndexWriter.swift index 3938e76..5ce58ae 100644 --- a/Sources/PackageDSLKit/PackageIndexWriter.swift +++ b/Sources/PackageDSLKit/PackageIndexWriter.swift @@ -29,7 +29,9 @@ import SwiftSyntax -public struct PackageIndexWriter: Sendable, Hashable, Codable { +public struct PackageIndexWriter: IndexCodeWriter, Sendable, Hashable, Codable { + public init() { + } private func labeledExpression(for name: String, items: [String]) -> LabeledExprSyntax? { if items.isEmpty { return nil @@ -62,7 +64,7 @@ public struct PackageIndexWriter: Sendable, Hashable, Codable { ) } - public func writeIndex(_ index: Index) throws -> String { + public func writeIndex(_ index: Index) throws(PackageDSLError) -> String { let declSyntax: DeclSyntax = .init(ImportDeclSyntax.module("PackageDescription")) let labeledExpressions = [ diff --git a/Sources/PackageDSLKit/PackageParser.swift b/Sources/PackageDSLKit/PackageParser.swift index 6b69f48..5005f8e 100644 --- a/Sources/PackageDSLKit/PackageParser.swift +++ b/Sources/PackageDSLKit/PackageParser.swift @@ -48,7 +48,7 @@ public struct PackageParser: Sendable, Hashable, Codable { } let sourceCode: String do { - sourceCode = try String(contentsOf: directoryURL.appending(path: filePath)) + sourceCode = try String(contentsOf: directoryURL.appending(path: filePath), encoding: .utf8) } catch { throw .other(error) } diff --git a/Sources/PackageDSLKit/PackageWriter.swift b/Sources/PackageDSLKit/PackageWriter.swift index 409619c..72d53c4 100644 --- a/Sources/PackageDSLKit/PackageWriter.swift +++ b/Sources/PackageDSLKit/PackageWriter.swift @@ -38,16 +38,29 @@ public struct PackageWriter: Sendable { TestTarget.self, SupportedPlatformSet.self, ] - private let fileManager: @Sendable () -> FileManager = { .default } - private let indexWriter: PackageIndexWriter = .init() - private let componentWriter: ComponentWriter = .init() - public init() { + private let fileInterfaceType: PackageFilesInterfaceType + private let fileAccessor: PackageFilesFactory + private let indexWriter: IndexCodeWriter + private let componentWriter: StructureWriter + + public init( + fileAccessor: any PackageFilesFactory = PackageFiles.default, + fileInterfaceType: PackageFilesInterfaceType = .fileManager, + indexWriter: any IndexCodeWriter = PackageIndexWriter(), + componentWriter: any StructureWriter = ComponentWriter() + ) { + self.fileAccessor = fileAccessor + self.fileInterfaceType = fileInterfaceType + self.indexWriter = indexWriter + self.componentWriter = componentWriter } + public func write( _ specification: PackageSpecifications, to url: URL ) throws(PackageDSLError) { + let filesInterface = self.fileAccessor.interface(for: self.fileInterfaceType) let configuration = PackageDirectoryConfiguration(specifications: specification) let indexFileURL = url.appending(component: "Index.swift") @@ -74,7 +87,7 @@ public struct PackageWriter: Sendable { if directoryCreated[directoryURL] == nil { do { - try fileManager().createDirectory(at: directoryURL, withIntermediateDirectories: true) + try filesInterface.createDirectory(at: directoryURL, withIntermediateDirectories: true) } catch { throw .other(error) } diff --git a/Sources/PackageDSLKit/PropertyWriter.swift b/Sources/PackageDSLKit/PropertyWriter.swift index 0fc93fa..93f083a 100644 --- a/Sources/PackageDSLKit/PropertyWriter.swift +++ b/Sources/PackageDSLKit/PropertyWriter.swift @@ -29,8 +29,8 @@ import SwiftSyntax -internal struct PropertyWriter: Sendable, Hashable, Codable { - internal func node(from property: Property) -> VariableDeclSyntax { +public enum PropertyWriter { + public static func node(from property: Property) -> VariableDeclSyntax { let codeBlocks = property.code.map(CodeBlockItemSyntax.init) let codeBlockList = CodeBlockItemListSyntax(codeBlocks) // swiftlint:disable:next force_try diff --git a/Sources/PackageDSLKit/StructureWriter.swift b/Sources/PackageDSLKit/StructureWriter.swift new file mode 100644 index 0000000..2f9e6cd --- /dev/null +++ b/Sources/PackageDSLKit/StructureWriter.swift @@ -0,0 +1,34 @@ +// +// StructureWriter.swift +// PackageDSLKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +public protocol StructureWriter: Sendable { + func node(from component: Component) -> StructDeclSyntax +} diff --git a/Sources/PackageDSLKit/SupportCodeBlock.swift b/Sources/PackageDSLKit/SupportCodeBlock.swift index 4cb7a04..5be776d 100644 --- a/Sources/PackageDSLKit/SupportCodeBlock.swift +++ b/Sources/PackageDSLKit/SupportCodeBlock.swift @@ -39,7 +39,7 @@ public enum SupportCodeBlock { private static func readSyntaxNode() -> any SyntaxProtocol { // swiftlint:disable force_try force_unwrapping let url = Bundle.module.url(forResource: "PackageDSL.swift", withExtension: "txt")! - let text = try! String(contentsOf: url) + let text = try! String(contentsOf: url, encoding: .utf8) // swiftlint:enable force_try force_unwrapping return SourceFileSyntax(stringLiteral: text) } diff --git a/Sources/package/Commands/Initialize.swift b/Sources/package/Commands/Initialize.swift index caf5740..e4e96cd 100644 --- a/Sources/package/Commands/Initialize.swift +++ b/Sources/package/Commands/Initialize.swift @@ -64,8 +64,7 @@ extension Package { if shouldCreateDirectory { try self.settings.fileManager.createDirectory( at: self.settings.dslSourcesURL, - withIntermediateDirectories: true, - attributes: nil + withIntermediateDirectories: true ) } @@ -79,8 +78,8 @@ extension Package { let swiftVersionFile = settings.pathURL.appending(component: ".swift-version") settings.fileManager.createFile( - atPath: swiftVersionFile.path(), - contents: Data("\(self.swiftVersion)".utf8) + at: swiftVersionFile, + text: self.swiftVersion.description ) try settings.fileManager.writePackageSwiftFile( swiftVersion: swiftVersion, diff --git a/Sources/package/FileManagerContainer.swift b/Sources/package/FileManagerContainer.swift index 86caae0..d340c71 100644 --- a/Sources/package/FileManagerContainer.swift +++ b/Sources/package/FileManagerContainer.swift @@ -32,14 +32,19 @@ import Foundation import PackageDSLKit internal protocol FileManagerContainer { - var fileManagerType: FileManagerType { get } + var packageFiles: PackageFilesFactory { get } + var fileManagerType: PackageFilesInterfaceType { get } } extension FileManagerContainer { - internal var fileManager: FileManager { - FileManager.default + internal var packageFiles: PackageFilesFactory { + PackageFiles.default + } + + internal var fileManager: PackageFilesInterface { + self.packageFiles.interface(for: self.fileManagerType) } } -extension FileManagerType: ExpressibleByArgument { +extension PackageFilesInterfaceType: ExpressibleByArgument { } diff --git a/Sources/package/Settings.swift b/Sources/package/Settings.swift index dbef172..082d3c1 100644 --- a/Sources/package/Settings.swift +++ b/Sources/package/Settings.swift @@ -33,7 +33,7 @@ import PackageDSLKit internal struct Settings: ParsableArguments, FileManagerContainer { @Option(help: .hidden) - internal var fileManagerType: FileManagerType = .fileManager + internal var fileManagerType: PackageFilesInterfaceType = .fileManager @Option internal var path: String? @@ -42,7 +42,7 @@ internal struct Settings: ParsableArguments, FileManagerContainer { if let path = self.path { return URL(fileURLWithPath: path) } else { - return URL(fileURLWithPath: self.fileManager.currentDirectoryPath) + return self.fileManager.currentDirectoryURL } } diff --git a/Tests/PackageDSLKitTests/ComponentBuildableTests.swift b/Tests/PackageDSLKitTests/ComponentBuildableTests.swift new file mode 100644 index 0000000..410b6cf --- /dev/null +++ b/Tests/PackageDSLKitTests/ComponentBuildableTests.swift @@ -0,0 +1,60 @@ +// +// Test 2.swift +// PackageDSLKit +// +// Created by Leo Dion on 1/3/25. +// + +import Foundation +import Testing + +@testable import PackageDSLKit + +internal struct ComponentBuildableTests { + @Test(arguments: zip(1...100, [true, false])) internal func initialize( + index: Int, containsRequirements: Bool + ) throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + let name = UUID().uuidString + let component = MockComponentBuildable.component( + name: name, + containsRequirements: containsRequirements + ) + let result = MockComponentBuildable( + component: component + ) + + guard containsRequirements else { + try #require(result == nil) + return + } + + #expect(result?.component == component) + } + + @Test(arguments: 1...100) + internal func directoryURL(index: Int) async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + let packageDSLName = UUID().uuidString + let packageDSLURL = URL.temporaryDirectory.appending( + path: packageDSLName, directoryHint: .isDirectory) + let componentDirectoryURL = MockComponentBuildable.directoryURL(relativeTo: packageDSLURL) + + let expectedDirectoryName = componentDirectoryURL.lastPathComponent + let expectedPackageDSLURL = componentDirectoryURL.deletingLastPathComponent() + let expectedPackageDSLName = expectedPackageDSLURL.lastPathComponent + + #expect(MockComponentBuildable.directoryName == expectedDirectoryName) + #expect(expectedPackageDSLURL == packageDSLURL) + #expect(expectedPackageDSLName == packageDSLName) + } + + @Test(arguments: zip(1...100, [true, false])) + internal func isType(index: Int, containsRequirements: Bool) async throws { + let name = UUID().uuidString + let component = MockComponentBuildable.component( + name: name, containsRequirements: containsRequirements) + let isType = component.isType(of: MockComponentBuildable.self) + #expect(isType == containsRequirements) + } +} diff --git a/Tests/PackageDSLKitTests/ComponentWriterTests.swift b/Tests/PackageDSLKitTests/ComponentWriterTests.swift new file mode 100644 index 0000000..61cb72e --- /dev/null +++ b/Tests/PackageDSLKitTests/ComponentWriterTests.swift @@ -0,0 +1,62 @@ +// +// ComponentWriterTests.swift +// PackageDSLKit +// +// Created by Leo Dion on 1/3/25. +// + +import Testing + +@testable import PackageDSLKit + +internal struct ComponentWriterTests { + private final class Indicies: @unchecked Sendable { + private var set: Set = .init() + + fileprivate func contains(_ index: Int) -> Bool { + set.contains(index) + } + + fileprivate func insert(_ index: Int) { + set.insert(index) + } + } + + @Test(arguments: 1...100) + internal func testPropertyCalls(index: Int) async { + let propertyValues: [Property] = (1...5).map { _ in + .init( + name: .randomIdentifier(), + type: .randomIdentifier(), + code: [ + .randomIdentifier(), + .randomIdentifier(), + ] + ) + } + let propertyDictionary: [String: Property] = .init( + uniqueKeysWithValues: propertyValues.map { + ($0.name, $0) + } + ) + await confirmation(expectedCount: propertyValues.count) { confirmation in + let indicies = Indicies() + let writer = ComponentWriter { actualProperty in + // swiftlint:disable:next force_try + let actualIndex = try! #require(propertyValues.firstIndex(of: actualProperty)) + #expect(!indicies.contains(actualIndex)) + indicies.insert(actualIndex) + defer { + confirmation() + } + return PropertyWriter.node(from: actualProperty) + } + let component = Component( + name: .randomIdentifier(), + inheritedTypes: [.randomIdentifier(), .randomIdentifier()], + properties: propertyDictionary + ) + _ = writer.node(from: component) + } + } +} diff --git a/Tests/PackageDSLKitTests/DependencyTypeTests.swift b/Tests/PackageDSLKitTests/DependencyTypeTests.swift new file mode 100644 index 0000000..2b09b63 --- /dev/null +++ b/Tests/PackageDSLKitTests/DependencyTypeTests.swift @@ -0,0 +1,68 @@ +// +// DependencyTypeTests.swift +// PackageDSLKit +// +// Created by Leo Dion on 1/3/25. +// + +import Testing + +@testable import PackageDSLKit + +internal struct DependencyTypeTests { + internal enum ExpectedValue { + case rawValue(Int) + case none + case invalid(Int) + } + + internal struct TestRow: Sendable { + internal let strings: [String] + internal let expectedRawValue: ExpectedValue + } + + @Test(arguments: [ + TestRow( + strings: ["PackageDependency", "TargetDependency"], + expectedRawValue: .rawValue(3) + ), + TestRow( + strings: ["PackageDependency"], + expectedRawValue: .rawValue(1) + ), + TestRow( + strings: ["TargetDependency"], + expectedRawValue: .rawValue(2) + ), + TestRow( + strings: [], + expectedRawValue: .none + ), + TestRow( + strings: [String.randomIdentifier(), String.randomIdentifier()], + expectedRawValue: .none + ), + TestRow( + strings: ["PackageDependency", String.randomIdentifier()], + expectedRawValue: .invalid(1) + ), + ]) internal func initializeFromStrings(_ value: TestRow) { + let actualResult = Result { + try Dependency.DependencyType(stringsThrows: value.strings) + }.mapError { + // swiftlint:disable:next force_cast + $0 as! Dependency.DependencyType.InvalidValueError + } + + switch (value.expectedRawValue, actualResult) { + case (.invalid(let expected), .failure(let error)): + #expect(error.invalidCount == expected) + case (.none, .success(.none)): + break + case (.rawValue(let expectedRawValue), .success(.some(let actual))): + #expect(actual.rawValue == expectedRawValue) + default: + Issue.record("Result mismatch: \(value.expectedRawValue) != \(actualResult)") + } + } +} diff --git a/Tests/PackageDSLKitTests/MockComponentBuildable.swift b/Tests/PackageDSLKitTests/MockComponentBuildable.swift new file mode 100644 index 0000000..69bb731 --- /dev/null +++ b/Tests/PackageDSLKitTests/MockComponentBuildable.swift @@ -0,0 +1,49 @@ +// +// MockComponentBuildable.swift +// PackageDSLKit +// +// Created by Leo Dion on 1/3/25. +// + +import Foundation + +@testable import PackageDSLKit + +internal struct MockComponentBuildable: ComponentBuildable { + internal static let directoryName: String = UUID().uuidString + + internal let component: PackageDSLKit.Component + + internal init(component: PackageDSLKit.Component, requirements: ()) { + self.component = component + } + + internal static func requirements(from component: PackageDSLKit.Component) -> ()? { + guard component.properties["containsRequirements"] != nil else { + return nil + } + return () + } + + internal static func component(name: String, containsRequirements: Bool) -> Component { + var properties = [String: Property]() + if containsRequirements { + properties[ + "containsRequirements"] = + Property( + name: "containsRequirements", + type: "type", + code: ["code"] + ) + } + return Component( + name: name, + inheritedTypes: [], + properties: properties + ) + } + + internal func createComponent() -> PackageDSLKit.Component { + component + } +} diff --git a/Tests/PackageDSLKitTests/String.swift b/Tests/PackageDSLKitTests/String.swift new file mode 100644 index 0000000..689b10d --- /dev/null +++ b/Tests/PackageDSLKitTests/String.swift @@ -0,0 +1,23 @@ +// +// String.swift +// PackageDSLKit +// +// Created by Leo Dion on 1/3/25. +// + +extension String { + private static let validIdentifierCharacters = Array( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_" + ) + + internal static func randomIdentifier(minLength: Int = 3, maxLength: Int = 10) -> String { + let length = Int.random(in: minLength...maxLength) + var identifier = "" + + for _ in 0..