diff --git a/Sources/TemplateDescription/AttributeHelpers.swift b/Sources/TemplateDescription/AttributeHelpers.swift index a762fc3f6..a9ea03781 100644 --- a/Sources/TemplateDescription/AttributeHelpers.swift +++ b/Sources/TemplateDescription/AttributeHelpers.swift @@ -1,7 +1,7 @@ import Foundation /// Error that occurs when parsing attributes from input -enum ParsingError: Error, CustomStringConvertible { +enum ParsingError: Error, CustomStringConvertible, Equatable { /// Thrown when could not find attribute in input case attributeNotFound(String) /// Thrown when attributes not provided @@ -20,13 +20,14 @@ enum ParsingError: Error, CustomStringConvertible { /// Returns value for `ParsedAttribute` of `name` from user input /// - Parameters: /// - name: Name of `ParsedAttribute` +/// - arguments: Arguments to get attribute from (usually you should lead this at default) /// - Returns: Value of `ParsedAttribute` -public func getAttribute(for name: String) throws -> String { +public func getAttribute(for name: String, arguments: [String] = CommandLine.arguments) throws -> String { let jsonDecoder = JSONDecoder() guard - let attributesIndex = CommandLine.arguments.firstIndex(of: "--attributes"), - CommandLine.arguments.endIndex > attributesIndex + 1, - let data = CommandLine.arguments[attributesIndex + 1].data(using: .utf8) + let attributesIndex = arguments.firstIndex(of: "--attributes") ?? arguments.firstIndex(of: "-a"), + arguments.endIndex > attributesIndex + 1, + let data = arguments[attributesIndex + 1].data(using: .utf8) else { throw ParsingError.attributesNotProvided } let parsedAttributes = try jsonDecoder.decode([ParsedAttribute].self, from: data) diff --git a/Sources/TemplateDescription/ParsedAttribute.swift b/Sources/TemplateDescription/ParsedAttribute.swift index b662f7790..9674bc480 100644 --- a/Sources/TemplateDescription/ParsedAttribute.swift +++ b/Sources/TemplateDescription/ParsedAttribute.swift @@ -1,7 +1,7 @@ import Foundation /// Parsed attribute from user input -public struct ParsedAttribute: Codable { +public struct ParsedAttribute: Codable, Equatable { public init(name: String, value: String) { self.name = name self.value = value diff --git a/Sources/TemplateDescription/Template.swift b/Sources/TemplateDescription/Template.swift index 36134f877..ea1bb0af6 100644 --- a/Sources/TemplateDescription/Template.swift +++ b/Sources/TemplateDescription/Template.swift @@ -1,7 +1,7 @@ import Foundation /// Template manifest - used with `tuist scaffold` -public struct Template: Codable { +public struct Template: Codable, Equatable { /// Description of template public let description: String /// Attributes to be passed to template @@ -12,19 +12,19 @@ public struct Template: Codable { public let directories: [String] public init(description: String, - arguments: [Attribute] = [], + attributes: [Attribute] = [], files: [File] = [], directories: [String] = [], script: String? = nil) { self.description = description - self.attributes = arguments + self.attributes = attributes self.files = files self.directories = directories dumpIfNeeded(self) } /// Enum containing information about how to generate file - public enum Contents: Codable { + public enum Contents: Codable, Equatable { /// Static Contents is defined in `Template.swift` and contains a simple `String` /// Can not contain any additional logic apart from plain `String` from `arguments` case `static`(String) @@ -66,7 +66,7 @@ public struct Template: Codable { } /// File description for generating - public struct File: Codable { + public struct File: Codable, Equatable { public let path: String public let contents: Contents @@ -77,7 +77,7 @@ public struct Template: Codable { } /// Attribute to be passed to `tuist scaffold` for generating with `Template` - public enum Attribute: Codable { + public enum Attribute: Codable, Equatable { /// Required attribute with a given name case required(String) /// Optional attribute with a given name and a default value used when attribute not provided by user diff --git a/Sources/TuistKit/Commands/InitCommand.swift b/Sources/TuistKit/Commands/InitCommand.swift index 3beb4b2d0..1d3520e68 100644 --- a/Sources/TuistKit/Commands/InitCommand.swift +++ b/Sources/TuistKit/Commands/InitCommand.swift @@ -110,7 +110,7 @@ class InitCommand: NSObject, Command { let name = try self.name(arguments: arguments, path: path) try verifyDirectoryIsEmpty(path: path) - let directories = try templatesDirectoryLocator.templateDirectories() + let directories = try templatesDirectoryLocator.templateDirectories(at: FileHandler.shared.currentPath) if let template = arguments.get(templateArgument) { guard let templateDirectory = directories.first(where: { $0.basename == template }) diff --git a/Sources/TuistKit/Commands/ScaffoldCommand.swift b/Sources/TuistKit/Commands/ScaffoldCommand.swift index 5bc90b0ad..ae488a16e 100644 --- a/Sources/TuistKit/Commands/ScaffoldCommand.swift +++ b/Sources/TuistKit/Commands/ScaffoldCommand.swift @@ -83,7 +83,7 @@ class ScaffoldCommand: NSObject, Command { let path = self.path(arguments: arguments) try verifyDirectoryIsEmpty(path: path) - let directories = try templatesDirectoryLocator.templateDirectories() + let directories = try templatesDirectoryLocator.templateDirectories(at: FileHandler.shared.currentPath) let shouldList = arguments.get(listArgument) ?? false if shouldList { diff --git a/Sources/TuistLoader/Utils/RootDirectoryLocator.swift b/Sources/TuistLoader/Utils/RootDirectoryLocator.swift index c689bb4bc..7374bc313 100644 --- a/Sources/TuistLoader/Utils/RootDirectoryLocator.swift +++ b/Sources/TuistLoader/Utils/RootDirectoryLocator.swift @@ -2,7 +2,7 @@ import Basic import Foundation import TuistSupport -public protocol RootDirectoryLocating { +protocol RootDirectoryLocating { /// Given a path, it finds the root directory by traversing up the hierarchy. /// The root directory is considered the directory that contains a Tuist/ directory or the directory where the /// git repository is defined if no Tuist/ directory is found. @@ -10,13 +10,13 @@ public protocol RootDirectoryLocating { func locate(from path: AbsolutePath) -> AbsolutePath? } -public final class RootDirectoryLocator: RootDirectoryLocating { +final class RootDirectoryLocator: RootDirectoryLocating { private let fileHandler: FileHandling = FileHandler.shared /// This cache avoids having to traverse the directories hierarchy every time the locate method is called. fileprivate var cache: [AbsolutePath: AbsolutePath] = [:] /// Shared instance - public static var shared: RootDirectoryLocating = RootDirectoryLocator() + static var shared: RootDirectoryLocating = RootDirectoryLocator() /// Constructor internal init() {} diff --git a/Sources/TuistTemplate/Models/ParsedAttribute.swift b/Sources/TuistTemplate/Models/ParsedAttribute.swift index 094d635ae..f366a1a91 100644 --- a/Sources/TuistTemplate/Models/ParsedAttribute.swift +++ b/Sources/TuistTemplate/Models/ParsedAttribute.swift @@ -1,5 +1,5 @@ /// Parsed attribute from user input -public struct ParsedAttribute: Encodable { +public struct ParsedAttribute: Encodable, Equatable { public init(name: String, value: String) { self.name = name self.value = value diff --git a/Sources/TuistTemplate/Models/Template.swift b/Sources/TuistTemplate/Models/Template.swift index 2436bcee7..3a68cb491 100644 --- a/Sources/TuistTemplate/Models/Template.swift +++ b/Sources/TuistTemplate/Models/Template.swift @@ -7,9 +7,9 @@ public struct Template { public let directories: [RelativePath] public init(description: String, - attributes: [Attribute], - files: [(path: RelativePath, contents: Contents)], - directories: [RelativePath]) { + attributes: [Attribute] = [], + files: [(path: RelativePath, contents: Contents)] = [], + directories: [RelativePath] = []) { self.description = description self.attributes = attributes self.files = files diff --git a/Sources/TuistTemplate/TemplateGenerator.swift b/Sources/TuistTemplate/TemplateGenerator.swift index da06b04d0..c6381e305 100644 --- a/Sources/TuistTemplate/TemplateGenerator.swift +++ b/Sources/TuistTemplate/TemplateGenerator.swift @@ -3,6 +3,19 @@ import Foundation import TuistSupport import TuistLoader +enum TemplateGeneratorError: FatalError, Equatable { + var type: ErrorType { .abort } + + case attributeNotFound(String) + + var description: String { + switch self { + case let .attributeNotFound(attribute): + return "You must provide \(attribute) attribute" + } + } +} + /// Interface for generating content defined in template manifest public protocol TemplateGenerating { /// Generate files for template manifest at `path` @@ -17,19 +30,16 @@ public protocol TemplateGenerating { public final class TemplateGenerator: TemplateGenerating { private let templateLoader: TemplateLoading - private let templateDescriptionHelpersBuilder: TemplateDescriptionHelpersBuilding - public init(templateLoader: TemplateLoading = TemplateLoader(), - templateDescriptionHelpersBuilder: TemplateDescriptionHelpersBuilding = TemplateDescriptionHelpersBuilder()) { + public init(templateLoader: TemplateLoading = TemplateLoader()) { self.templateLoader = templateLoader - self.templateDescriptionHelpersBuilder = templateDescriptionHelpersBuilder } public func generate(at sourcePath: AbsolutePath, to destinationPath: AbsolutePath, attributes: [String]) throws { let template = try templateLoader.loadTemplate(at: sourcePath) - let templateAttributes = getTemplateAttributes(with: attributes, template: template) + let templateAttributes = try getTemplateAttributes(with: attributes, template: template) try generateDirectories(template: template, templateAttributes: templateAttributes, @@ -76,15 +86,17 @@ public final class TemplateGenerator: TemplateGenerating { } } - private func getTemplateAttributes(with attributes: [String], template: Template) -> [ParsedAttribute] { + private func getTemplateAttributes(with attributes: [String], template: Template) throws -> [ParsedAttribute] { let parsedAttributes = parseAttributes(attributes) - return template.attributes.map { + return try template.attributes.map { switch $0 { case let .optional(name, default: defaultValue): let value = parsedAttributes.first(where: { $0.name == name })?.value ?? defaultValue return ParsedAttribute(name: name, value: value) case let .required(name): - guard let value = parsedAttributes.first(where: { $0.name == name })?.value else { fatalError() } + guard + let value = parsedAttributes.first(where: { $0.name == name })?.value + else { throw TemplateGeneratorError.attributeNotFound(name) } return ParsedAttribute(name: name, value: value) } } diff --git a/Sources/TuistTemplate/TemplateLoader.swift b/Sources/TuistTemplate/TemplateLoader.swift index 7597af535..f82856f73 100644 --- a/Sources/TuistTemplate/TemplateLoader.swift +++ b/Sources/TuistTemplate/TemplateLoader.swift @@ -5,6 +5,21 @@ import TuistSupport import TemplateDescription import TuistLoader +public enum TemplateLoaderError: FatalError, Equatable { + public var type: ErrorType { .abort } + + case manifestNotFound(AbsolutePath) + case generateFileNotFound(AbsolutePath) + + public var description: String { + switch self { + case let .manifestNotFound(manifestPath): + return "Could not find template manifest at \(manifestPath.pathString)" + case let .generateFileNotFound(generateFilePath): + return "Could not find generate file at \(generateFilePath.pathString)" + } + } +} public protocol TemplateLoading { /// Load `TuistTemplate.Template` at given `path` @@ -47,7 +62,7 @@ public class TemplateLoader: TemplateLoading { public func loadTemplate(at path: AbsolutePath) throws -> TuistTemplate.Template { let manifestPath = path.appending(component: "Template.swift") guard FileHandler.shared.exists(manifestPath) else { - fatalError() + throw TemplateLoaderError.manifestNotFound(manifestPath) } let data = try loadManifestData(at: manifestPath) let manifest = try decoder.decode(TemplateDescription.Template.self, from: data) @@ -56,6 +71,9 @@ public class TemplateLoader: TemplateLoading { } public func loadGenerateFile(at path: AbsolutePath, parsedAttributes: [TuistTemplate.ParsedAttribute]) throws -> String { + guard FileHandler.shared.exists(path) else { + throw TemplateLoaderError.generateFileNotFound(path) + } var additionalArguments: [String] = [] if let attributes = try String(data: encoder.encode(parsedAttributes), encoding: .utf8) { additionalArguments.append("--attributes") diff --git a/Sources/TuistTemplate/TemplatesDirectoryLocator.swift b/Sources/TuistTemplate/TemplatesDirectoryLocator.swift index 884064085..29c573c47 100644 --- a/Sources/TuistTemplate/TemplatesDirectoryLocator.swift +++ b/Sources/TuistTemplate/TemplatesDirectoryLocator.swift @@ -1,7 +1,6 @@ import Basic import Foundation import TuistSupport -import TuistLoader public protocol TemplatesDirectoryLocating { /// Returns the path to the tuist built-in templates directory if it exists. @@ -12,27 +11,16 @@ public protocol TemplatesDirectoryLocating { /// - Returns: Path of templates directory up the three `from` func locate(from path: AbsolutePath) -> AbsolutePath? /// - Returns: All available directories with defined templates (custom and built-in) - func templateDirectories() throws -> [AbsolutePath] + func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] } public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { private let fileHandler: FileHandling = FileHandler.shared /// This cache avoids having to traverse the directories hierarchy every time the locate method is called. private var cache: [AbsolutePath: AbsolutePath] = [:] - - /// Instance to locate the root directory of the project. - let rootDirectoryLocator: RootDirectoryLocating /// Default constructor. - public convenience init() { - self.init(rootDirectoryLocator: RootDirectoryLocator.shared) - } - - /// Initializes the locator with its dependencies. - /// - Parameter rootDirectoryLocator: Instance to locate the root directory of the project. - init(rootDirectoryLocator: RootDirectoryLocating) { - self.rootDirectoryLocator = rootDirectoryLocator - } + public init() { } // MARK: - TemplatesDirectoryLocating @@ -56,9 +44,7 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { } public func locateCustom(at: AbsolutePath) -> AbsolutePath? { - guard let rootDirectory = rootDirectoryLocator.locate(from: at) else { return nil } - let customTemplatesDirectory = rootDirectory - .appending(components: Constants.tuistDirectoryName, Constants.templatesDirectoryName) + guard let customTemplatesDirectory = locate(from: at) else { return nil } if !FileHandler.shared.exists(customTemplatesDirectory) { return nil } return customTemplatesDirectory } @@ -67,10 +53,10 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating { locate(from: path, source: path) } - public func templateDirectories() throws -> [AbsolutePath] { + public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] { let templatesDirectory = locate() let templates = try templatesDirectory.map(FileHandler.shared.contentsOfDirectory) ?? [] - let customTemplatesDirectory = locateCustom(at: FileHandler.shared.currentPath) + let customTemplatesDirectory = locateCustom(at: path) let customTemplates = try customTemplatesDirectory.map(FileHandler.shared.contentsOfDirectory) ?? [] return (templates + customTemplates).filter { $0.basename != Constants.templateHelpersDirectoryName } } diff --git a/Sources/TuistTemplateTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift b/Sources/TuistTemplateTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift index c4921699f..064ffba31 100644 --- a/Sources/TuistTemplateTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift +++ b/Sources/TuistTemplateTesting/Utils/Mocks/MockTemplatesDirectoryLocator.swift @@ -7,7 +7,7 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating { public var locateStub: (() -> AbsolutePath?)? public var locateCustomStub: ((AbsolutePath) -> AbsolutePath?)? public var locateFromStub: ((AbsolutePath) -> AbsolutePath?)? - public var templateDirectoriesStub: (() throws -> [AbsolutePath])? + public var templateDirectoriesStub: ((AbsolutePath) throws -> [AbsolutePath])? public func locate() -> AbsolutePath? { locateStub?() @@ -21,8 +21,8 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating { locateFromStub?(path) } - public func templateDirectories() throws -> [AbsolutePath] { - try templateDirectoriesStub?() ?? [] + public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] { + try templateDirectoriesStub?(path) ?? [] } } diff --git a/Templates/default/Template.swift b/Templates/default/Template.swift index 39e3d9528..dc7810b6c 100644 --- a/Templates/default/Template.swift +++ b/Templates/default/Template.swift @@ -1,7 +1,7 @@ import TemplateDescription -let nameArgument: Template.Attribute = .required("name") -let platformArgument: Template.Attribute = .optional("platform", default: "iOS") +let nameAttribute: Template.Attribute = .required("name") +let platformAttribute: Template.Attribute = .optional("platform", default: "iOS") let setupContent = """ import ProjectDescription @@ -59,9 +59,9 @@ extension Project { """ let projectsPath = "Projects" -let appPath = projectsPath + "/\(nameArgument)" -let kitFrameworkPath = projectsPath + "/\(nameArgument)Kit" -let supportFrameworkPath = projectsPath + "/\(nameArgument)Support" +let appPath = projectsPath + "/\(nameAttribute)" +let kitFrameworkPath = projectsPath + "/\(nameAttribute)Kit" +let supportFrameworkPath = projectsPath + "/\(nameAttribute)Support" func directories(for projectPath: String) -> [String] { [ @@ -76,10 +76,10 @@ let workspaceContent = """ import ProjectDescription import ProjectDescriptionHelpers -let workspace = Workspace(name: "\(nameArgument)", projects: [ - "Projects/\(nameArgument)", - "Projects/\(nameArgument)Kit", - "Projects/\(nameArgument)Support" +let workspace = Workspace(name: "\(nameAttribute)", projects: [ + "Projects/\(nameAttribute)", + "Projects/\(nameAttribute)Kit", + "Projects/\(nameAttribute)Support" ]) """ @@ -98,15 +98,15 @@ func testsContent(_ name: String) -> String { let kitSourceContent = """ import Foundation -import \(nameArgument)Support +import \(nameAttribute)Support -public final class \(nameArgument)Kit {} +public final class \(nameAttribute)Kit {} """ let supportSourceContent = """ import Foundation -public final class \(nameArgument)Support {} +public final class \(nameAttribute)Support {} """ let playgroundContent = """ @@ -193,10 +193,10 @@ graph.dot """ let template = Template( - description: "Custom \(nameArgument)", - arguments: [ - nameArgument, - platformArgument, + description: "Custom \(nameAttribute)", + attributes: [ + nameAttribute, + platformAttribute, ], files: [ .static(path: "Setup.swift", @@ -213,23 +213,23 @@ let template = Template( generateFilePath: "SupportFrameworkProject.swift"), .generated(path: appPath + "/Sources/AppDelegate.swift", generateFilePath: "AppDelegate.swift"), - .static(path: appPath + "/Tests/\(nameArgument)Tests.swift", - contents: testsContent("\(nameArgument)")), - .static(path: kitFrameworkPath + "/Sources/\(nameArgument)Kit.swift", + .static(path: appPath + "/Tests/\(nameAttribute)Tests.swift", + contents: testsContent("\(nameAttribute)")), + .static(path: kitFrameworkPath + "/Sources/\(nameAttribute)Kit.swift", contents: kitSourceContent), - .static(path: kitFrameworkPath + "/Tests/\(nameArgument)KitTests.swift", - contents: testsContent("\(nameArgument)Kit")), - .static(path: supportFrameworkPath + "/Sources/\(nameArgument)Support.swift", + .static(path: kitFrameworkPath + "/Tests/\(nameAttribute)KitTests.swift", + contents: testsContent("\(nameAttribute)Kit")), + .static(path: supportFrameworkPath + "/Sources/\(nameAttribute)Support.swift", contents: supportSourceContent), - .static(path: supportFrameworkPath + "/Tests/\(nameArgument)SupportTests.swift", - contents: testsContent("\(nameArgument)Support")), - .static(path: kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground" + "/Contents.swift", + .static(path: supportFrameworkPath + "/Tests/\(nameAttribute)SupportTests.swift", + contents: testsContent("\(nameAttribute)Support")), + .static(path: kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground" + "/Contents.swift", contents: playgroundContent), - .generated(path: kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground" + "/contents.xcplayground", + .generated(path: kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground" + "/contents.xcplayground", generateFilePath: "Playground.swift"), - .static(path: supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground" + "/Contents.swift", + .static(path: supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground" + "/Contents.swift", contents: playgroundContent), - .generated(path: supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground" + "/contents.xcplayground", + .generated(path: supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground" + "/contents.xcplayground", generateFilePath: "Playground.swift"), .static(path: "TuistConfig.swift", contents: tuistConfigContent), @@ -238,8 +238,8 @@ let template = Template( ], directories: [ "Tuist/ProjectDescriptionHelpers", - supportFrameworkPath + "/Playgrounds/\(nameArgument)Support.playground", - kitFrameworkPath + "/Playgrounds/\(nameArgument)Kit.playground", + supportFrameworkPath + "/Playgrounds/\(nameAttribute)Support.playground", + kitFrameworkPath + "/Playgrounds/\(nameAttribute)Kit.playground", ] + directories(for: appPath) + directories(for: kitFrameworkPath) diff --git a/Tests/TemplateDescriptionTests/ParsedAttributeTests.swift b/Tests/TemplateDescriptionTests/ParsedAttributeTests.swift new file mode 100644 index 000000000..54ea085bd --- /dev/null +++ b/Tests/TemplateDescriptionTests/ParsedAttributeTests.swift @@ -0,0 +1,64 @@ +import TuistSupportTesting +import XCTest + +@testable import TemplateDescription + +class ParsedAttributeTests: XCTestCase { + func test_parsed_attribute_codable() throws { + // Given + let parsedAttribute = ParsedAttribute(name: "name", value: "value name") + + + // Then + XCTAssertCodable(parsedAttribute) + } + + func test_getAttributes_when_attribute_present() throws { + // Given + let parsedAttributes: [ParsedAttribute] = [ + ParsedAttribute(name: "a", value: "a value"), + ParsedAttribute(name: "b", value: "b value") + ] + let encoder = JSONEncoder() + let parsedAttributesString = String(data: try encoder.encode(parsedAttributes), encoding: .utf8) + let arguments = ["tuist", "something", "--attributes", parsedAttributesString ?? ""] + + // Then + XCTAssertEqual(try getAttribute(for: "a", arguments: arguments), "a value") + } + + func test_getAttributes_when_attribute_present_and_short_option() throws { + // Given + let parsedAttributes: [ParsedAttribute] = [ + ParsedAttribute(name: "a", value: "a value"), + ParsedAttribute(name: "b", value: "b value") + ] + let encoder = JSONEncoder() + let parsedAttributesString = String(data: try encoder.encode(parsedAttributes), encoding: .utf8) + let arguments = ["tuist", "something", "-a", parsedAttributesString ?? ""] + + // Then + XCTAssertEqual(try getAttribute(for: "a", arguments: arguments), "a value") + } + + func test_getAttributes_error_when_attribute_not_present() throws { + // Given + let parsedAttributes: [ParsedAttribute] = [ + ParsedAttribute(name: "b", value: "b value") + ] + let encoder = JSONEncoder() + let parsedAttributesString = String(data: try encoder.encode(parsedAttributes), encoding: .utf8) + let arguments = ["tuist", "something", "-a", parsedAttributesString ?? ""] + + // Then + XCTAssertThrowsSpecific(try getAttribute(for: "a", arguments: arguments), ParsingError.attributeNotFound("a")) + } + + func test_getAttributes_error_when_attributes_not_provided() throws { + // Given + let arguments = ["tuist", "something", "--not-attributes"] + + // Then + XCTAssertThrowsSpecific(try getAttribute(for: "a", arguments: arguments), ParsingError.attributesNotProvided) + } +} diff --git a/Tests/TemplateDescriptionTests/TemplateTests.swift b/Tests/TemplateDescriptionTests/TemplateTests.swift new file mode 100644 index 000000000..28d322654 --- /dev/null +++ b/Tests/TemplateDescriptionTests/TemplateTests.swift @@ -0,0 +1,28 @@ +import TemplateDescription +import TuistSupportTesting +import XCTest + +class TemplateTests: XCTestCase { + func test_template_codable() throws { + // Given + let template = Template( + description: "", + attributes: [ + .required("name"), + .optional("aName", default: "defaultName"), + .optional("bName", default: ""), + ], + files: [ + .static(path: "static.swift", contents: "content"), + .generated(path: "generated.swift", generateFilePath: "generate.swift") + ], + directories: [ + "{{ name }}", + "directory" + ]) + + + // Then + XCTAssertCodable(template) + } +} diff --git a/Tests/TuistKitTests/Commands/InitCommandTests.swift b/Tests/TuistKitTests/Commands/InitCommandTests.swift index 2580bff33..62f5bd699 100644 --- a/Tests/TuistKitTests/Commands/InitCommandTests.swift +++ b/Tests/TuistKitTests/Commands/InitCommandTests.swift @@ -56,7 +56,7 @@ final class InitCommandTests: TuistUnitTestCase { // Given let templateName = "template" let templatePath = try temporaryPath().appending(component: templateName) - templatesDirectoryLocator.templateDirectoriesStub = { + templatesDirectoryLocator.templateDirectoriesStub = { _ in [templatePath] } var generateSourcePath: AbsolutePath? @@ -81,7 +81,7 @@ final class InitCommandTests: TuistUnitTestCase { func test_init_default_when_no_template() throws { // Given let defaultTemplatePath = try temporaryPath().appending(component: "default") - templatesDirectoryLocator.templateDirectoriesStub = { + templatesDirectoryLocator.templateDirectoriesStub = { _ in [defaultTemplatePath] } let attributes = ["--name", "name", "--platform", "macos"] @@ -100,7 +100,7 @@ final class InitCommandTests: TuistUnitTestCase { func test_init_default_platform() throws { let defaultTemplatePath = try temporaryPath().appending(component: "default") - templatesDirectoryLocator.templateDirectoriesStub = { + templatesDirectoryLocator.templateDirectoriesStub = { _ in [defaultTemplatePath] } let attributes = ["--name", "name"] diff --git a/Tests/TuistKitTests/Commands/ScaffoldCommandTests.swift b/Tests/TuistKitTests/Commands/ScaffoldCommandTests.swift index e82744658..13d21c3cc 100644 --- a/Tests/TuistKitTests/Commands/ScaffoldCommandTests.swift +++ b/Tests/TuistKitTests/Commands/ScaffoldCommandTests.swift @@ -60,7 +60,7 @@ final class ScaffoldCommandTests: TuistUnitTestCase { // Given let templateName = "template" let templatePath = try temporaryPath().appending(component: templateName) - templatesDirectoryLocator.templateDirectoriesStub = { + templatesDirectoryLocator.templateDirectoriesStub = { _ in [templatePath] } var generateSourcePath: AbsolutePath? diff --git a/Tests/TuistLoaderIntegrationTests/Utis/ProjectDescriptionHelpersBuilderIntegrationTests.swift b/Tests/TuistLoaderIntegrationTests/Utils/ProjectDescriptionHelpersBuilderIntegrationTests.swift similarity index 100% rename from Tests/TuistLoaderIntegrationTests/Utis/ProjectDescriptionHelpersBuilderIntegrationTests.swift rename to Tests/TuistLoaderIntegrationTests/Utils/ProjectDescriptionHelpersBuilderIntegrationTests.swift diff --git a/Tests/TuistLoaderIntegrationTests/Utis/RootDirectoryLocatorIntegrationTests.swift b/Tests/TuistLoaderIntegrationTests/Utils/RootDirectoryLocatorIntegrationTests.swift similarity index 100% rename from Tests/TuistLoaderIntegrationTests/Utis/RootDirectoryLocatorIntegrationTests.swift rename to Tests/TuistLoaderIntegrationTests/Utils/RootDirectoryLocatorIntegrationTests.swift diff --git a/Tests/TuistTemplateIntegrationTests/Loaders/TemplateLoaderTests.swift b/Tests/TuistTemplateIntegrationTests/Loaders/TemplateLoaderTests.swift new file mode 100644 index 000000000..7285f7281 --- /dev/null +++ b/Tests/TuistTemplateIntegrationTests/Loaders/TemplateLoaderTests.swift @@ -0,0 +1,94 @@ +import Basic +import Foundation +import TuistSupport +import XCTest + +@testable import TuistTemplate +@testable import TuistSupportTesting + +final class TemplateLoaderTests: TuistTestCase { + var subject: TemplateLoader! + + override func setUp() { + super.setUp() + subject = TemplateLoader() + } + + override func tearDown() { + super.tearDown() + subject = nil + } + + func test_loadTemplate() throws { + // Given + let temporaryPath = try self.temporaryPath() + let content = """ + import TemplateDescription + + let template = Template( + description: "Template description" + ) + """ + + let manifestPath = temporaryPath.appending(component: "Template.swift") + try content.write(to: manifestPath.url, + atomically: true, + encoding: .utf8) + + // When + let got = try subject.loadTemplate(at: temporaryPath) + + // Then + XCTAssertEqual(got.description, "Template description") + } + + func test_loadGenerateFile() throws { + let temporaryPath = try self.temporaryPath() + let content = """ + import Foundation + import TemplateDescription + + let content = Content { + let name = try getAttribute(for: "name") + return "name: \\(name)" + } + + """ + let expectedContent = "name: test name" + let generateFilePath = temporaryPath.appending(component: "Generate.swift") + try content.write(to: generateFilePath.url, + atomically: true, + encoding: .utf8) + // When + let got = try subject.loadGenerateFile(at: generateFilePath, + parsedAttributes: [ParsedAttribute(name: "name", value: "test name")]) + + // Then + XCTAssertEqual(got, expectedContent) + } + + func test_load_invalidFormat() throws { + // Given + let temporaryPath = try self.temporaryPath() + let content = """ + import ABC + let template + """ + + let manifestPath = temporaryPath.appending(component: "Template.swift") + try content.write(to: manifestPath.url, + atomically: true, + encoding: .utf8) + + // When / Then + XCTAssertThrowsError( + try subject.loadTemplate(at: temporaryPath) + ) + } + + func test_load_missingManifest() throws { + let temporaryPath = try self.temporaryPath() + XCTAssertThrowsSpecific(try subject.loadTemplate(at: temporaryPath), + TemplateLoaderError.manifestNotFound(temporaryPath.appending(components: "Template.swift"))) + } +} diff --git a/Tests/TuistTemplateIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift b/Tests/TuistTemplateIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift new file mode 100644 index 000000000..628fc31b5 --- /dev/null +++ b/Tests/TuistTemplateIntegrationTests/Utils/TemplatesDirectoryLocatorIntegrationTests.swift @@ -0,0 +1,106 @@ +import Basic +import Foundation +import TuistSupport +import XCTest + +@testable import TuistTemplate +@testable import TuistSupportTesting + +final class TemplatesDirectoryLocatorIntegrationTests: TuistTestCase { + var subject: TemplatesDirectoryLocator! + + override func setUp() { + super.setUp() + subject = TemplatesDirectoryLocator() + } + + override func tearDown() { + subject = nil + super.tearDown() + } + + func test_locate_when_a_templates_and_git_directory_exists() throws { + // Given + let temporaryDirectory = try temporaryPath() + try createFolders(["this/is/a/very/nested/directory", "this/is/Tuist/Templates", "this/.git"]) + + // When + let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory"))) + + // Then + XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/is/Tuist/Templates"))) + } + + func test_locate_when_a_templates_directory_exists() throws { + // Given + let temporaryDirectory = try temporaryPath() + try createFolders(["this/is/a/very/nested/directory", "this/is/Tuist/Templates"]) + + // When + let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory"))) + + // Then + XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/is/Tuist/Templates"))) + } + + func test_locate_when_a_git_directory_exists() throws { + // Given + let temporaryDirectory = try temporaryPath() + try createFolders(["this/is/a/very/nested/directory", "this/.git"]) + + // When + let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory"))) + + // Then + XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/Tuist/Templates"))) + } + + func test_locate_when_multiple_tuist_directories_exists() throws { + // Given + let temporaryDirectory = try temporaryPath() + try createFolders(["this/is/a/very/nested/Tuist/", "this/is/Tuist/"]) + let paths = [ + "this/is/a/very/directory", + "this/is/a/very/nested/directory", + ] + + // When + let got = paths.map { + subject.locate(from: temporaryDirectory.appending(RelativePath($0))) + } + + // Then + XCTAssertEqual(got, [ + "this/is/Tuist/Templates", + "this/is/a/very/nested/Tuist/Templates", + ].map { temporaryDirectory.appending(RelativePath($0)) }) + } + + func test_locate_when_templates_directory_exist() throws { + // Given + let temporaryDirectory = try temporaryPath() + try createFolders(["this/is/a/very/nested/directory", "this/Templates"]) + // When + let got = subject.locate(from: temporaryDirectory.appending(RelativePath("this/is/a/very/nested/directory"))) + + // Then + XCTAssertEqual(got, temporaryDirectory.appending(RelativePath("this/Templates"))) + } + + func test_locate_all_templates() throws { + // Given + let temporaryDirectory = try temporaryPath() + try createFolders(["this/is/a/directory", "this/Templates/template_one", "this/Templates/template_two"]) + + // When + let got = try subject.templateDirectories(at: temporaryDirectory.appending(RelativePath("this/is/a/directory"))) + + // Then + XCTAssertEqual([ + AbsolutePath(#file.replacingOccurrences(of: "file://", with: "")) + .appending(RelativePath("../../../../Templates/default")), + temporaryDirectory.appending(RelativePath("this/Templates/template_one")), + temporaryDirectory.appending(RelativePath("this/Templates/template_two")), + ], got) + } +} diff --git a/Tests/TuistTemplateTests/TemplateGeneratorTests.swift b/Tests/TuistTemplateTests/TemplateGeneratorTests.swift new file mode 100644 index 000000000..bb942f5eb --- /dev/null +++ b/Tests/TuistTemplateTests/TemplateGeneratorTests.swift @@ -0,0 +1,234 @@ +import Basic +import Foundation +import TuistSupport +import XCTest + +@testable import TuistTemplateTesting +@testable import TuistSupportTesting +@testable import TuistTemplate + +final class TemplateGeneratorTests: TuistTestCase { + var subject: TemplateGenerator! + var templateLoader: MockTemplateLoader! + + override func setUp() { + super.setUp() + templateLoader = MockTemplateLoader() + subject = TemplateGenerator(templateLoader: templateLoader) + } + + override func tearDown() { + super.tearDown() + subject = nil + templateLoader = nil + } + + func test_directories_are_generated() throws { + // Given + let directories = [RelativePath("a"), RelativePath("a/b"), RelativePath("c")] + templateLoader.loadTemplateStub = { _ in + Template(description: "", + directories: directories) + } + let destinationPath = try temporaryPath() + let expectedDirectories = directories.map(destinationPath.appending) + + // When + try subject.generate(at: try temporaryPath(), + to: destinationPath, + attributes: []) + + // Then + XCTAssertTrue(expectedDirectories.allSatisfy(FileHandler.shared.exists)) + } + + func test_directories_with_attributes() throws { + // Given + let name = "GenName" + let bName = "BName" + let defaultName = "defaultName" + let directories = [RelativePath("{{ name }}"), RelativePath("{{ aName }}"), RelativePath("{{ bName }}")] + templateLoader.loadTemplateStub = { _ in + Template(description: "", + attributes: [ + .required("name"), + .optional("aName", default: defaultName), + .optional("bName", default: ""), + ], + directories: directories) + } + let destinationPath = try temporaryPath() + let expectedDirectories = [name, defaultName, bName].map(destinationPath.appending) + + // When + try subject.generate(at: try temporaryPath(), + to: destinationPath, + attributes: ["--name", name, "--bName", bName]) + + // Then + XCTAssertTrue(expectedDirectories.allSatisfy(FileHandler.shared.exists)) + } + + func test_fails_when_required_attribute_not_provided() throws { + // Given + templateLoader.loadTemplateStub = { _ in + Template(description: "", + attributes: [.required("required")] + ) + } + + // Then + XCTAssertThrowsSpecific(try subject.generate(at: try temporaryPath(), + to: try temporaryPath(), + attributes: []), + TemplateGeneratorError.attributeNotFound("required")) + } + + func test_files_are_generated() throws { + // Given + let files: [(path: RelativePath, contents: Template.Contents)] = [ + (path: RelativePath("a"), contents: .static("aContent")), + (path: RelativePath("b"), contents: .static("bContent")), + ] + templateLoader.loadTemplateStub = { _ in + Template(description: "", + files: files) + } + let destinationPath = try temporaryPath() + let expectedFiles: [(AbsolutePath, String)] = files.compactMap { + let content: String + switch $0.contents { + case let .static(staticContent): + content = staticContent + case .generated: + XCTFail("Unexpected type") + return nil + } + return (destinationPath.appending($0.path), content) + } + + // When + try subject.generate(at: try temporaryPath(), + to: destinationPath, + attributes: []) + + // Then + try expectedFiles.forEach { + XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1) + } + } + + func test_files_are_generated_with_attributes() throws { + // Given + templateLoader.loadTemplateStub = { _ in + Template(description: "", + attributes: [ + .required("name"), + .required("contentName"), + .required("directoryName"), + .required("fileName"), + ], + files: [ + (path: RelativePath("{{ name }}"), contents: .static("{{ contentName }}")), + (path: RelativePath("{{ directoryName }}/{{ fileName }}"), contents: .static("bContent")), + ], + directories: [RelativePath("{{ directoryName }}")]) + } + let name = "test name" + let contentName = "test content" + let directoryName = "test directory" + let fileName = "test file" + let destinationPath = try temporaryPath() + let expectedFiles: [(AbsolutePath, String)] = [ + (destinationPath.appending(component: name), contentName), + (destinationPath.appending(components: directoryName, fileName), "bContent") + ] + + // When + try subject.generate(at: try temporaryPath(), + to: destinationPath, + attributes: [ + "--name", name, + "--contentName", contentName, + "--directoryName", directoryName, + "--fileName", fileName + ]) + + // Then + try expectedFiles.forEach { + XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1) + } + } + + func test_generated_files() throws { + // Given + let sourcePath = try temporaryPath() + let destinationPath = try temporaryPath() + let name = "test name" + let aContent = "test a content" + let bContent = "test b content" + templateLoader.loadTemplateStub = { _ in + Template(description: "", + attributes: [.required("name")], + files: [ + (path: RelativePath("a"), contents: .generated(sourcePath.appending(component: "a"))), + (path: RelativePath("b/{{ name }}"), contents: .generated(sourcePath.appending(components: "b"))) + ], + directories: [RelativePath("b")]) + } + templateLoader.loadGenerateFileStub = { filePath, _ in + if filePath == destinationPath.appending(components: "a") { + return aContent + } else if filePath == destinationPath.appending(components: "b") { + return bContent + } else { + XCTFail("unexpected path \(filePath)") + return "" + } + } + let expectedFiles: [(AbsolutePath, String)] = [ + (destinationPath.appending(component: "a"), aContent), + (destinationPath.appending(components: "b", name), bContent) + ] + + // When + try subject.generate(at: sourcePath, + to: destinationPath, + attributes: ["--name", name]) + + // Then + try expectedFiles.forEach { + XCTAssertEqual(try FileHandler.shared.readTextFile($0.0), $0.1) + } + } + + func test_attributes_passed_to_generated_file() throws { + // Given + let sourcePath = try temporaryPath() + let destinationPath = try temporaryPath() + var parsedAttributes: [ParsedAttribute] = [] + templateLoader.loadGenerateFileStub = { filePath, attributes in + if filePath == sourcePath.appending(component: "a") { + parsedAttributes = attributes + } else { + XCTFail("unexpected path \(filePath)") + } + return "" + } + templateLoader.loadTemplateStub = { _ in + Template(description: "", + attributes: [.required("name")], + files: [(path: RelativePath("a"), + contents: .generated(sourcePath.appending(component: "a")))]) + } + let expectedParsedAttributes: [ParsedAttribute] = [ParsedAttribute(name: "name", value: "attribute name")] + + // When + try subject.generate(at: sourcePath, + to: destinationPath, + attributes: ["--name", "attribute name"]) + + // Then + XCTAssertEqual(parsedAttributes, expectedParsedAttributes) + } +} diff --git a/fixtures/ios_app_with_templates/Tuist/Templates/custom/Template.swift b/fixtures/ios_app_with_templates/Tuist/Templates/custom/Template.swift index 4566572ba..675e69433 100644 --- a/fixtures/ios_app_with_templates/Tuist/Templates/custom/Template.swift +++ b/fixtures/ios_app_with_templates/Tuist/Templates/custom/Template.swift @@ -1,19 +1,18 @@ -import ProjectDescription import TemplateDescription -let nameArgument: Template.Attribute = .required("name") -let platformArgument: Template.Attribute = .optional("platform", default: "ios") +let nameAttribute: Template.Attribute = .required("name") +let platformAttribute: Template.Attribute = .optional("platform", default: "ios") let testContents = """ -// this is test \(nameArgument) content +// this is test \(nameAttribute) content """ let template = Template( - description: "Custom \(nameArgument)", - arguments: [ + description: "Custom \(nameAttribute)", + attributes: [ nameArgument, - platformArgument + platformAttribute ], files: [ .static(path: "custom_dir/custom.swift",