From 3526653260a3f015151955364dc43234acad5752 Mon Sep 17 00:00:00 2001 From: Bernhard Loibl Date: Sun, 24 Dec 2017 12:19:39 +0100 Subject: [PATCH 1/3] Implements template source for playground generation. --- Sources/Playground.swift | 105 +++++++++++++++++++++++++++++--- Tests/XgenTests/XgenTests.swift | 94 ++++++++++++++++++++++++++-- 2 files changed, 186 insertions(+), 13 deletions(-) diff --git a/Sources/Playground.swift b/Sources/Playground.swift index 3ca6d69..c7cacbc 100644 --- a/Sources/Playground.swift +++ b/Sources/Playground.swift @@ -16,12 +16,18 @@ public class Playground: Generatable { case tvOS = "tvos" } + /// Enum representing the source of the playground content + public enum Source { + case code(String) + case template(path: String) + } + /// The path to generate a playground at public let path: String /// The platform to generate a playground for public var platform: Platform - /// The code that the playground should contain (if nil, a default will be used) - public var code: String? + /// The source that the playground should contain (if nil, a default will be used) + public var source: Source? // MARK: - Initializer @@ -30,19 +36,19 @@ public class Playground: Generatable { * * - parameter path: The path to generate a playground at * - parameter platform: The platform to generate a playground for - * - parameter code: The code that the playground should contain. If `nil` + * - parameter code: The source that the playground should contain. If `nil` * the playground will contain a system framework import * * Note that you have to call `generate()` on the playground to actually * generate it on the file system. */ - public init(path: String, platform: Platform = .iOS, code: String? = nil) { + public init(path: String, platform: Platform = .iOS, source: Source? = nil) { self.path = path.removingSuffixIfNeeded("/") - .addingSuffixIfNeeded(".playground") - .appending("/") + .addingSuffixIfNeeded(".playground") + .appending("/") self.platform = platform - self.code = code + self.source = source } // MARK: - Generatable @@ -51,8 +57,7 @@ public class Playground: Generatable { do { let folder = try FileSystem().createFolderIfNeeded(at: path) - let codeFile = try folder.createFile(named: "Contents.swift") - try codeFile.write(string: code ?? .makeDefaultCode(for: platform)) + try generateSourceCode(in: folder) let xmlFile = try folder.createFile(named: "contents.xcplayground") try xmlFile.write(string: generateXML()) @@ -74,6 +79,77 @@ public class Playground: Generatable { xml.append("") return xml } + + private func generateSourceCode(in folder: Folder) throws { + switch source { + case let .code(string)?: + let codeFile = try folder.createFile(named: "Contents.swift") + try codeFile.write(string: string) + case let .template(path)?: + let template = try Template(path: path, platform: platform) + try template.copyContents(to: folder) + default: + let codeFile = try folder.createFile(named: "Contents.swift") + try codeFile.write(string: .makeDefaultCode(for: platform)) + } + } +} + +public extension Playground { + public final class Template { + public let info: Info + public let folder: Folder + + public init(path: String, platform: Playground.Platform) throws { + let folderPath = path.removingSuffixIfNeeded("/") + .addingSuffixIfNeeded(".xctemplate") + .appending("/") + self.folder = try Folder(path: folderPath) + + let data = try self.folder.file(named: "TemplateInfo.plist").read() + let decoder = PropertyListDecoder() + self.info = try decoder.decode(Info.self, from: data) + + guard self.info.platforms.contains(platform.templateIdentifier) else { + throw Error.notAvailableForPlatform(platform.rawValue) + } + + guard self.info.allowedTypes.contains("com.apple.dt.playground") else { + throw Error.invalidTemplateType + } + } + + public var mainFolder: Folder { + return try! folder.subfolder(named: info.mainFilename) + } + + public func copyContents(to targetFolder: Folder) throws { + try mainFolder.file(named: "Contents.swift").copy(to: targetFolder) + + if let sourcesFolder = try? mainFolder.subfolder(named: "Sources") { + try sourcesFolder.copy(to: targetFolder) + } + } + } +} + +public extension Playground.Template { + public struct Info: Decodable { + public let mainFilename: String + public let platforms: [String] + public let allowedTypes: [String] + + private enum CodingKeys: String, CodingKey { + case mainFilename = "MainTemplateFile" + case platforms = "Platforms" + case allowedTypes = "AllowedTypes" + } + } + + public enum Error: Swift.Error { + case notAvailableForPlatform(String) + case invalidTemplateType + } } // MARK: - Private extensions @@ -87,6 +163,17 @@ private extension Playground.Platform { return "Cocoa" } } + + var templateIdentifier: String { + switch self { + case .iOS: + return "com.apple.platform.iphoneos" + case .macOS: + return "com.apple.platform.macosx" + case .tvOS: + return "com.apple.platform.appletvos" + } + } } private extension String { diff --git a/Tests/XgenTests/XgenTests.swift b/Tests/XgenTests/XgenTests.swift index 624c1b3..52a6a17 100644 --- a/Tests/XgenTests/XgenTests.swift +++ b/Tests/XgenTests/XgenTests.swift @@ -6,7 +6,7 @@ import Foundation import XCTest -import Xgen +@testable import Xgen import Files import ShellOut @@ -57,11 +57,11 @@ class XgenTests: XCTestCase { } func testGeneratingPlayground() throws { - let code = "import Foundation\n\nprint(\"Hello world\")" + let string = "import Foundation\n\nprint(\"Hello world\")" let playground = Playground( path: folder.path + "Playground", platform: .macOS, - code: code + source: .code(string) ) try playground.generate() @@ -69,7 +69,7 @@ class XgenTests: XCTestCase { let playgroundFolder = try folder.subfolder(named: "Playground.playground") let codeFile = try playgroundFolder.file(named: "Contents.swift") - try XCTAssertEqual(codeFile.readAsString(), code) + try XCTAssertEqual(codeFile.readAsString(), string) let contentsFile = try playgroundFolder.file(named: "contents.xcplayground") let xml = try XMLDocument(data: contentsFile.read(), options: []) @@ -98,6 +98,69 @@ class XgenTests: XCTestCase { let contentsFile = try workspaceFolder.file(named: "Contents.xcworkspacedata") XCTAssertTrue(try contentsFile.readAsString().contains(playgroundFolder.path)) } + + func testGeneratingPlaygroundFromTemplate() throws { + let code = "import Foundation\n\nprint(\"Hello world\")" + let templateFolder = try folder.createTemplateFolder(withCode: code) + let platform = Playground.Platform.iOS + + let playground = Playground( + path: folder.path + "Playground", + platform: platform, + source: .template(path: templateFolder.path) + ) + + try playground.generate() + + let playgroundFolder = try folder.subfolder(named: "Playground.playground") + + let codeFile = try playgroundFolder.file(named: "Contents.swift") + try XCTAssertEqual(codeFile.readAsString(), code) + + let sources = try playgroundFolder.subfolder(named: "Sources").files + let template = try Playground.Template(path: templateFolder.path, platform: platform) + let templateSources = try template.mainFolder.subfolder(named: "Sources").files + XCTAssertEqual(sources.count, templateSources.count) + } + + func testGeneratingPlaygroundFromTemplateWithNotAvailablePlatform() throws { + let code = "import Foundation\n\nprint(\"Hello world\")" + let templateFolder = try folder.createTemplateFolder(withCode: code) + let platform = Playground.Platform.tvOS + + let playground = Playground( + path: folder.path + "Playground", + platform: platform, + source: .template(path: templateFolder.path) + ) + + XCTAssertThrowsError(try playground.generate()) { error in + guard let underlyingError = (error as? XgenError)?.underlyingError, + case let Playground.Template.Error.notAvailableForPlatform(platformString) = underlyingError else { + return XCTFail() + } + XCTAssertEqual(platformString, platform.rawValue) + } + } + + func testGeneratingPlaygroundFromTemplateWithInvalidType() throws { + let code = "import Foundation\n\nprint(\"Hello world\")" + let info = ["AllowedTypes": ["com.apple.dt.project"]] + let templateFolder = try folder.createTemplateFolder(withCode: code, info: info) + + let playground = Playground( + path: folder.path + "Playground", + platform: .iOS, + source: .template(path: templateFolder.path) + ) + + XCTAssertThrowsError(try playground.generate()) { error in + guard let underlyingError = (error as? XgenError)?.underlyingError, + case Playground.Template.Error.invalidTemplateType = underlyingError else { + return XCTFail() + } + } + } } // MARK: - Extensions @@ -106,4 +169,27 @@ private extension Folder { @discardableResult func moveToAndPerform(command: String) throws -> String { return try shellOut(to: "cd \(path) && \(command)") } + + func createTemplateFolder(withCode code: String, info: [String: Any] = [:]) throws -> Folder { + let folder = try createSubfolder(named: "Template.xctemplate") + let templateFileName = "Playground.playground" + let baseInfo: [String: Any] = [ + "AllowedTypes": ["com.apple.dt.playground"], + "Platforms": ["com.apple.platform.iphoneos"], + "MainTemplateFile": templateFileName, + "Summary": "A Playground" + ] + let mergedInfo = info.merging(baseInfo) { (old, _) in old } + let data = try PropertyListSerialization.data(fromPropertyList: mergedInfo, format: .xml, options: 0) + try folder.createFile(named: "TemplateInfo.plist", contents: data) + + let playgroundFolder = try folder.createSubfolder(named: templateFileName) + try playgroundFolder.createFile(named: "Contents.swift", contents: code) + + let templateSourcesFolder = try playgroundFolder.createSubfolder(named: "Sources") + try templateSourcesFolder.createFile(named: "One.swift") + try templateSourcesFolder.createFile(named: "Two.swift") + + return folder + } } From ff816676aa644be69be78e966aadc88e8019650c Mon Sep 17 00:00:00 2001 From: Bernhard Loibl Date: Tue, 9 Jan 2018 18:05:43 +0100 Subject: [PATCH 2/3] Adjusts docs and visibility for components in playground. --- Sources/Playground.swift | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Sources/Playground.swift b/Sources/Playground.swift index c7cacbc..a20e26f 100644 --- a/Sources/Playground.swift +++ b/Sources/Playground.swift @@ -36,13 +36,13 @@ public class Playground: Generatable { * * - parameter path: The path to generate a playground at * - parameter platform: The platform to generate a playground for - * - parameter code: The source that the playground should contain. If `nil` - * the playground will contain a system framework import + * - parameter source: The source that the playground should contain. If `nil` + * the playground will contain a system framework import * * Note that you have to call `generate()` on the playground to actually * generate it on the file system. */ - public init(path: String, platform: Platform = .iOS, source: Source? = nil) { + public init(path: String, platform: Platform, source: Source?) { self.path = path.removingSuffixIfNeeded("/") .addingSuffixIfNeeded(".playground") .appending("/") @@ -50,6 +50,10 @@ public class Playground: Generatable { self.platform = platform self.source = source } + + public convenience init(path: String, platform: Platform = .iOS, code: String? = nil) { + self.init(path: path, platform: platform, source: code.map(Source.code)) + } // MARK: - Generatable @@ -95,12 +99,12 @@ public class Playground: Generatable { } } -public extension Playground { - public final class Template { - public let info: Info - public let folder: Folder +internal extension Playground { + final class Template { + let info: Info + let folder: Folder - public init(path: String, platform: Playground.Platform) throws { + init(path: String, platform: Playground.Platform) throws { let folderPath = path.removingSuffixIfNeeded("/") .addingSuffixIfNeeded(".xctemplate") .appending("/") @@ -119,11 +123,11 @@ public extension Playground { } } - public var mainFolder: Folder { + var mainFolder: Folder { return try! folder.subfolder(named: info.mainFilename) } - public func copyContents(to targetFolder: Folder) throws { + func copyContents(to targetFolder: Folder) throws { try mainFolder.file(named: "Contents.swift").copy(to: targetFolder) if let sourcesFolder = try? mainFolder.subfolder(named: "Sources") { @@ -133,11 +137,11 @@ public extension Playground { } } -public extension Playground.Template { - public struct Info: Decodable { - public let mainFilename: String - public let platforms: [String] - public let allowedTypes: [String] +internal extension Playground.Template { + struct Info: Decodable { + let mainFilename: String + let platforms: [String] + let allowedTypes: [String] private enum CodingKeys: String, CodingKey { case mainFilename = "MainTemplateFile" @@ -146,7 +150,7 @@ public extension Playground.Template { } } - public enum Error: Swift.Error { + enum Error: Swift.Error { case notAvailableForPlatform(String) case invalidTemplateType } From 3ee65e415e4fce551b0caec88af787dca51051aa Mon Sep 17 00:00:00 2001 From: Bernhard Loibl Date: Tue, 9 Jan 2018 18:13:12 +0100 Subject: [PATCH 3/3] Adds doc for playground source cases. --- Sources/Playground.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Playground.swift b/Sources/Playground.swift index a20e26f..34ea2e7 100644 --- a/Sources/Playground.swift +++ b/Sources/Playground.swift @@ -18,7 +18,9 @@ public class Playground: Generatable { /// Enum representing the source of the playground content public enum Source { + /// The code that the playground should contain case code(String) + /// The path to a Xcode playground template (.xctemplate) that should be used for the playground case template(path: String) }