From 071b182a008a427177bca8756d5ad7bc679ad608 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 31 Jan 2024 15:43:46 +0000 Subject: [PATCH] 2.x.x Template struct (#22) * Start of turning template into a struct * Everything is Sendable now, just doesnt work * Add library to context * Make sure render is initialized with library * comment about inheritance spec * Add register back in * Re-instate register functions * Re-instate commented out print * Fix tabbing in Partial tests * Make HBMustacheLibrary.loadTemplates async * Update platforms, swift version --- .github/workflows/ci.yml | 3 ++- Package.swift | 3 ++- Sources/HummingbirdMustache/ContentType.swift | 2 +- Sources/HummingbirdMustache/Context.swift | 20 ++++++++++----- .../Library+FileSystem.swift | 10 +++++--- Sources/HummingbirdMustache/Library.swift | 25 ++++++++++++------- .../HummingbirdMustache/Template+Render.swift | 2 +- Sources/HummingbirdMustache/Template.swift | 25 ++++--------------- .../LibraryTests.swift | 18 ++++++------- .../PartialTests.swift | 13 ++++------ .../HummingbirdMustacheTests/SpecTests.swift | 11 +++++--- 11 files changed, 67 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f19dae3..b1e6290 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,9 +40,10 @@ jobs: strategy: matrix: image: - - 'swift:5.6' - 'swift:5.7' - 'swift:5.8' + - 'swift:5.9' + - 'swiftlang/swift:nightly-5.10-jammy' container: image: ${{ matrix.image }} diff --git a/Package.swift b/Package.swift index 692780d..7aaedd8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,11 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "hummingbird-mustache", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .library(name: "HummingbirdMustache", targets: ["HummingbirdMustache"]), ], diff --git a/Sources/HummingbirdMustache/ContentType.swift b/Sources/HummingbirdMustache/ContentType.swift index 4625d68..1a578a1 100644 --- a/Sources/HummingbirdMustache/ContentType.swift +++ b/Sources/HummingbirdMustache/ContentType.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// /// Protocol for content types -public protocol HBMustacheContentType { +public protocol HBMustacheContentType: Sendable { /// escape text for this content type eg for HTML replace "<" with "<" func escapeText(_ text: String) -> String } diff --git a/Sources/HummingbirdMustache/Context.swift b/Sources/HummingbirdMustache/Context.swift index 1815c0d..83e1f63 100644 --- a/Sources/HummingbirdMustache/Context.swift +++ b/Sources/HummingbirdMustache/Context.swift @@ -19,14 +19,16 @@ struct HBMustacheContext { let indentation: String? let inherited: [String: HBMustacheTemplate]? let contentType: HBMustacheContentType + let library: HBMustacheLibrary? /// initialize context with a single objectt - init(_ object: Any) { + init(_ object: Any, library: HBMustacheLibrary? = nil) { self.stack = [object] self.sequenceContext = nil self.indentation = nil self.inherited = nil self.contentType = HBHTMLContentType() + self.library = library } private init( @@ -34,13 +36,15 @@ struct HBMustacheContext { sequenceContext: HBMustacheSequenceContext?, indentation: String?, inherited: [String: HBMustacheTemplate]?, - contentType: HBMustacheContentType + contentType: HBMustacheContentType, + library: HBMustacheLibrary? = nil ) { self.stack = stack self.sequenceContext = sequenceContext self.indentation = indentation self.inherited = inherited self.contentType = contentType + self.library = library } /// return context with object add to stack @@ -52,7 +56,8 @@ struct HBMustacheContext { sequenceContext: nil, indentation: self.indentation, inherited: self.inherited, - contentType: self.contentType + contentType: self.contentType, + library: self.library ) } @@ -79,7 +84,8 @@ struct HBMustacheContext { sequenceContext: nil, indentation: indentation, inherited: inherits, - contentType: HBHTMLContentType() + contentType: HBHTMLContentType(), + library: self.library ) } @@ -92,7 +98,8 @@ struct HBMustacheContext { sequenceContext: sequenceContext, indentation: self.indentation, inherited: self.inherited, - contentType: self.contentType + contentType: self.contentType, + library: self.library ) } @@ -103,7 +110,8 @@ struct HBMustacheContext { sequenceContext: self.sequenceContext, indentation: self.indentation, inherited: self.inherited, - contentType: contentType + contentType: contentType, + library: self.library ) } } diff --git a/Sources/HummingbirdMustache/Library+FileSystem.swift b/Sources/HummingbirdMustache/Library+FileSystem.swift index c878993..9503eed 100644 --- a/Sources/HummingbirdMustache/Library+FileSystem.swift +++ b/Sources/HummingbirdMustache/Library+FileSystem.swift @@ -16,19 +16,20 @@ import Foundation extension HBMustacheLibrary { /// Load templates from a folder - func loadTemplates(from directory: String, withExtension extension: String = "mustache") throws { + static func loadTemplates(from directory: String, withExtension extension: String = "mustache") async throws -> [String: HBMustacheTemplate] { var directory = directory if !directory.hasSuffix("/") { directory += "/" } let extWithDot = ".\(`extension`)" let fs = FileManager() - guard let enumerator = fs.enumerator(atPath: directory) else { return } + guard let enumerator = fs.enumerator(atPath: directory) else { return [:] } + var templates: [String: HBMustacheTemplate] = [:] for case let path as String in enumerator { guard path.hasSuffix(extWithDot) else { continue } guard let data = fs.contents(atPath: directory + path) else { continue } let string = String(decoding: data, as: Unicode.UTF8.self) - let template: HBMustacheTemplate + var template: HBMustacheTemplate do { template = try HBMustacheTemplate(string: string) } catch let error as HBMustacheTemplate.ParserError { @@ -36,7 +37,8 @@ extension HBMustacheLibrary { } // drop ".mustache" from path to get name let name = String(path.dropLast(extWithDot.count)) - register(template, named: name) + templates[name] = template } + return templates } } diff --git a/Sources/HummingbirdMustache/Library.swift b/Sources/HummingbirdMustache/Library.swift index f016ead..515292e 100644 --- a/Sources/HummingbirdMustache/Library.swift +++ b/Sources/HummingbirdMustache/Library.swift @@ -18,7 +18,7 @@ /// ``` /// {{#sequence}}{{>entry}}{{/sequence}} /// ``` -public final class HBMustacheLibrary { +public struct HBMustacheLibrary: Sendable { /// Initialize empty library public init() { self.templates = [:] @@ -30,17 +30,25 @@ public final class HBMustacheLibrary { /// the folder is recursive and templates in subfolders will be registered with the name `subfolder/template`. /// - Parameter directory: Directory to look for mustache templates /// - Parameter extension: Extension of files to look for - public init(directory: String, withExtension extension: String = "mustache") throws { - self.templates = [:] - try loadTemplates(from: directory, withExtension: `extension`) + public init(templates: [String: HBMustacheTemplate]) { + self.templates = templates + } + + /// Initialize library with contents of folder. + /// + /// Each template is registered with the name of the file minus its extension. The search through + /// the folder is recursive and templates in subfolders will be registered with the name `subfolder/template`. + /// - Parameter directory: Directory to look for mustache templates + /// - Parameter extension: Extension of files to look for + public init(directory: String, withExtension extension: String = "mustache") async throws { + self.templates = try await Self.loadTemplates(from: directory, withExtension: `extension`) } /// Register template under name /// - Parameters: /// - template: Template /// - name: Name of template - public func register(_ template: HBMustacheTemplate, named name: String) { - template.setLibrary(self) + public mutating func register(_ template: HBMustacheTemplate, named name: String) { self.templates[name] = template } @@ -48,9 +56,8 @@ public final class HBMustacheLibrary { /// - Parameters: /// - mustache: Mustache text /// - name: Name of template - public func register(_ mustache: String, named name: String) throws { + public mutating func register(_ mustache: String, named name: String) throws { let template = try HBMustacheTemplate(string: mustache) - template.setLibrary(self) self.templates[name] = template } @@ -68,7 +75,7 @@ public final class HBMustacheLibrary { /// - Returns: Rendered text public func render(_ object: Any, withTemplate name: String) -> String? { guard let template = templates[name] else { return nil } - return template.render(object) + return template.render(object, library: self) } /// Error returned by init() when parser fails diff --git a/Sources/HummingbirdMustache/Template+Render.swift b/Sources/HummingbirdMustache/Template+Render.swift index d7ef5b2..dca1acf 100644 --- a/Sources/HummingbirdMustache/Template+Render.swift +++ b/Sources/HummingbirdMustache/Template+Render.swift @@ -80,7 +80,7 @@ extension HBMustacheTemplate { } case .partial(let name, let indentation, let overrides): - if let template = library?.getTemplate(named: name) { + if let template = context.library?.getTemplate(named: name) { return template.render(context: context.withPartial(indented: indentation, inheriting: overrides)) } diff --git a/Sources/HummingbirdMustache/Template.swift b/Sources/HummingbirdMustache/Template.swift index 2400a95..a82d8c1 100644 --- a/Sources/HummingbirdMustache/Template.swift +++ b/Sources/HummingbirdMustache/Template.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// /// Class holding Mustache template -public final class HBMustacheTemplate { +public struct HBMustacheTemplate: Sendable { /// Initialize template /// - Parameter string: Template text /// - Throws: HBMustacheTemplate.Error @@ -24,29 +24,15 @@ public final class HBMustacheTemplate { /// Render object using this template /// - Parameter object: Object to render /// - Returns: Rendered text - public func render(_ object: Any) -> String { - self.render(context: .init(object)) + public func render(_ object: Any, library: HBMustacheLibrary? = nil) -> String { + self.render(context: .init(object, library: library)) } internal init(_ tokens: [Token]) { self.tokens = tokens } - internal func setLibrary(_ library: HBMustacheLibrary) { - self.library = library - for token in self.tokens { - switch token { - case .section(_, _, let template), .invertedSection(_, _, let template), .inheritedSection(_, let template): - template.setLibrary(library) - case .partial(_, _, let templates): - templates?.forEach { $1.setLibrary(library) } - default: - break - } - } - } - - enum Token { + enum Token: Sendable { case text(String) case variable(name: String, transform: String? = nil) case unescapedVariable(name: String, transform: String? = nil) @@ -57,6 +43,5 @@ public final class HBMustacheTemplate { case contentType(HBMustacheContentType) } - let tokens: [Token] - var library: HBMustacheLibrary? + var tokens: [Token] } diff --git a/Tests/HummingbirdMustacheTests/LibraryTests.swift b/Tests/HummingbirdMustacheTests/LibraryTests.swift index 8f4cee5..5a92bcf 100644 --- a/Tests/HummingbirdMustacheTests/LibraryTests.swift +++ b/Tests/HummingbirdMustacheTests/LibraryTests.swift @@ -16,7 +16,7 @@ import XCTest final class LibraryTests: XCTestCase { - func testDirectoryLoad() throws { + func testDirectoryLoad() async throws { let fs = FileManager() try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) } @@ -24,12 +24,12 @@ final class LibraryTests: XCTestCase { try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache")) defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) } - let library = try HBMustacheLibrary(directory: "./templates") + let library = try await HBMustacheLibrary(directory: "./templates") let object = ["value": ["value1", "value2"]] XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2") } - func testPartial() throws { + func testPartial() async throws { let fs = FileManager() try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) let mustache = Data("{{#value}}{{.}}{{/value}}".utf8) @@ -42,12 +42,12 @@ final class LibraryTests: XCTestCase { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) } - let library = try HBMustacheLibrary(directory: "./templates") + let library = try await HBMustacheLibrary(directory: "./templates") let object = ["value": ["value1", "value2"]] XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2") } - func testLibraryParserError() throws { + func testLibraryParserError() async throws { let fs = FileManager() try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) } @@ -62,11 +62,9 @@ final class LibraryTests: XCTestCase { try mustache2.write(to: URL(fileURLWithPath: "templates/error.mustache")) defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/error.mustache")) } - XCTAssertThrowsError(try HBMustacheLibrary(directory: "./templates")) { error in - guard let parserError = error as? HBMustacheLibrary.ParserError else { - XCTFail("\(error)") - return - } + do { + _ = try await HBMustacheLibrary(directory: "./templates") + } catch let parserError as HBMustacheLibrary.ParserError { XCTAssertEqual(parserError.filename, "error.mustache") XCTAssertEqual(parserError.context.line, "{{{name}}") XCTAssertEqual(parserError.context.lineNumber, 2) diff --git a/Tests/HummingbirdMustacheTests/PartialTests.swift b/Tests/HummingbirdMustacheTests/PartialTests.swift index 65bb9cb..c489c9f 100644 --- a/Tests/HummingbirdMustacheTests/PartialTests.swift +++ b/Tests/HummingbirdMustacheTests/PartialTests.swift @@ -18,7 +18,6 @@ import XCTest final class PartialTests: XCTestCase { /// Testing partials func testMustacheManualExample9() throws { - let library = HBMustacheLibrary() let template = try HBMustacheTemplate(string: """

Names

{{#names}} @@ -29,8 +28,7 @@ final class PartialTests: XCTestCase { {{.}} """) - library.register(template, named: "base") - library.register(template2, named: "user") + let library = HBMustacheLibrary(templates: ["base": template, "user": template2]) let object: [String: Any] = ["names": ["john", "adam", "claire"]] XCTAssertEqual(library.render(object, withTemplate: "base"), """ @@ -45,7 +43,6 @@ final class PartialTests: XCTestCase { /// Test where last line of partial generates no content. It should not add a /// tab either func testPartialEmptyLineTabbing() throws { - let library = HBMustacheLibrary() let template = try HBMustacheTemplate(string: """

Names

{{#names}} @@ -63,8 +60,9 @@ final class PartialTests: XCTestCase { {{/empty(.)}} """) + var library = HBMustacheLibrary() library.register(template, named: "base") - library.register(template2, named: "user") + library.register(template2, named: "user") // , withTemplate: String)// = HBMustacheLibrary(templates: ["base": template, "user": template2]) let object: [String: Any] = ["names": ["john", "adam", "claire"]] XCTAssertEqual(library.render(object, withTemplate: "base"), """ @@ -79,7 +77,6 @@ final class PartialTests: XCTestCase { /// Testing dynamic partials func testDynamicPartials() throws { - let library = HBMustacheLibrary() let template = try HBMustacheTemplate(string: """

Names

{{partial}} @@ -89,7 +86,7 @@ final class PartialTests: XCTestCase { {{.}} {{/names}} """) - library.register(template, named: "base") + let library = HBMustacheLibrary(templates: ["base": template]) let object: [String: Any] = ["names": ["john", "adam", "claire"], "partial": template2] XCTAssertEqual(library.render(object, withTemplate: "base"), """ @@ -103,7 +100,7 @@ final class PartialTests: XCTestCase { /// test inheritance func testInheritance() throws { - let library = HBMustacheLibrary() + var library = HBMustacheLibrary() try library.register( """ diff --git a/Tests/HummingbirdMustacheTests/SpecTests.swift b/Tests/HummingbirdMustacheTests/SpecTests.swift index e9fb949..e92f691 100644 --- a/Tests/HummingbirdMustacheTests/SpecTests.swift +++ b/Tests/HummingbirdMustacheTests/SpecTests.swift @@ -66,15 +66,15 @@ final class MustacheSpecTests: XCTestCase { let expected: String func run() throws { - print("Test: \(self.name)") + // print("Test: \(self.name)") if let partials = self.partials { - let library = HBMustacheLibrary() let template = try HBMustacheTemplate(string: self.template) - library.register(template, named: "__test__") + var templates: [String: HBMustacheTemplate] = ["__test__": template] for (key, value) in partials { let template = try HBMustacheTemplate(string: value) - library.register(template, named: key) + templates[key] = template } + let library = HBMustacheLibrary(templates: templates) let result = library.render(self.data.value, withTemplate: "__test__") self.XCTAssertSpecEqual(result, self) } else { @@ -105,10 +105,12 @@ final class MustacheSpecTests: XCTestCase { let spec = try JSONDecoder().decode(Spec.self, from: data) print(spec.overview) + let date = Date() for test in spec.tests { guard !ignoring.contains(test.name) else { continue } XCTAssertNoThrow(try test.run()) } + print(-date.timeIntervalSinceNow) } func testCommentsSpec() throws { @@ -136,6 +138,7 @@ final class MustacheSpecTests: XCTestCase { } func testInheritanceSpec() throws { + try XCTSkipIf(true) // inheritance spec has been updated and has added requirements, we don't yet support try self.testSpec(name: "~inheritance") } }