Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playground Templates #5

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 103 additions & 10 deletions Sources/Playground.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ public class Playground: Generatable {
case tvOS = "tvos"
}

/// 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)
}

/// 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

Expand All @@ -30,19 +38,23 @@ 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`
* 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, code: String? = nil) {
public init(path: String, platform: Platform, source: Source?) {
self.path = path.removingSuffixIfNeeded("/")
.addingSuffixIfNeeded(".playground")
.appending("/")
.addingSuffixIfNeeded(".playground")
.appending("/")

self.platform = platform
self.code = code
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
Expand All @@ -51,8 +63,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())
Expand All @@ -74,6 +85,77 @@ public class Playground: Generatable {
xml.append("</playground>")
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))
}
}
}

internal extension Playground {
final class Template {
let info: Info
let folder: Folder

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
}
}

var mainFolder: Folder {
return try! folder.subfolder(named: info.mainFilename)
}

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)
}
}
}
}

internal extension Playground.Template {
struct Info: Decodable {
let mainFilename: String
let platforms: [String]
let allowedTypes: [String]

private enum CodingKeys: String, CodingKey {
case mainFilename = "MainTemplateFile"
case platforms = "Platforms"
case allowedTypes = "AllowedTypes"
}
}

enum Error: Swift.Error {
case notAvailableForPlatform(String)
case invalidTemplateType
}
}

// MARK: - Private extensions
Expand All @@ -87,6 +169,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 {
Expand Down
94 changes: 90 additions & 4 deletions Tests/XgenTests/XgenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import Foundation
import XCTest
import Xgen
@testable import Xgen
import Files
import ShellOut

Expand Down Expand Up @@ -57,19 +57,19 @@ 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()

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: [])
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}