Skip to content

Commit

Permalink
Add development support for reloading templates when you render them (#…
Browse files Browse the repository at this point in the history
…30)

* Add support for reloading templates when you render them

* comment

* Ensure reload is only available in DEBUG

* move preprocessor block

* swift format

* MustacheTemplate.init?(filename:) internal

* Only pass reload flag down in DEBUG builds

* Rebase with main
  • Loading branch information
adam-fowler authored Jul 16, 2024
1 parent 7689de0 commit 5bb66ac
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 22 deletions.
25 changes: 17 additions & 8 deletions Sources/Mustache/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -20,15 +20,17 @@ struct MustacheContext {
let inherited: [String: MustacheTemplate]?
let contentType: MustacheContentType
let library: MustacheLibrary?
let reloadPartials: Bool

/// initialize context with a single objectt
init(_ object: Any, library: MustacheLibrary? = nil) {
init(_ object: Any, library: MustacheLibrary? = nil, reloadPartials: Bool = false) {
self.stack = [object]
self.sequenceContext = nil
self.indentation = nil
self.inherited = nil
self.contentType = HTMLContentType()
self.library = library
self.reloadPartials = reloadPartials
}

private init(
Expand All @@ -37,14 +39,16 @@ struct MustacheContext {
indentation: String?,
inherited: [String: MustacheTemplate]?,
contentType: MustacheContentType,
library: MustacheLibrary? = nil
library: MustacheLibrary? = nil,
reloadPartials: Bool
) {
self.stack = stack
self.sequenceContext = sequenceContext
self.indentation = indentation
self.inherited = inherited
self.contentType = contentType
self.library = library
self.reloadPartials = reloadPartials
}

/// return context with object add to stack
Expand All @@ -57,7 +61,8 @@ struct MustacheContext {
indentation: self.indentation,
inherited: self.inherited,
contentType: self.contentType,
library: self.library
library: self.library,
reloadPartials: self.reloadPartials
)
}

Expand All @@ -83,7 +88,8 @@ struct MustacheContext {
indentation: indentation,
inherited: inherits,
contentType: HTMLContentType(),
library: self.library
library: self.library,
reloadPartials: self.reloadPartials
)
}

Expand All @@ -100,7 +106,8 @@ struct MustacheContext {
indentation: indentation,
inherited: self.inherited,
contentType: self.contentType,
library: self.library
library: self.library,
reloadPartials: self.reloadPartials
)
}

Expand All @@ -114,7 +121,8 @@ struct MustacheContext {
indentation: self.indentation,
inherited: self.inherited,
contentType: self.contentType,
library: self.library
library: self.library,
reloadPartials: self.reloadPartials
)
}

Expand All @@ -126,7 +134,8 @@ struct MustacheContext {
indentation: self.indentation,
inherited: self.inherited,
contentType: contentType,
library: self.library
library: self.library,
reloadPartials: self.reloadPartials
)
}
}
13 changes: 5 additions & 8 deletions Sources/Mustache/Library+FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -27,17 +27,14 @@ extension MustacheLibrary {
var templates: [String: MustacheTemplate] = [:]
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)
var template: MustacheTemplate
do {
template = try MustacheTemplate(string: string)
guard let template = try MustacheTemplate(filename: directory + path) else { continue }
// drop ".mustache" from path to get name
let name = String(path.dropLast(extWithDot.count))
templates[name] = template
} catch let error as MustacheTemplate.ParserError {
throw ParserError(filename: path, context: error.context, error: error.error)
}
// drop ".mustache" from path to get name
let name = String(path.dropLast(extWithDot.count))
templates[name] = template
}
return templates
}
Expand Down
17 changes: 16 additions & 1 deletion Sources/Mustache/Library.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -78,6 +78,21 @@ public struct MustacheLibrary: Sendable {
return template.render(object, library: self)
}

/// Render object using templated with name
/// - Parameters:
/// - object: Object to render
/// - name: Name of template
/// - reload: Reload templates when rendering. This is only available in debug builds
/// - Returns: Rendered text
public func render(_ object: Any, withTemplate name: String, reload: Bool) -> String? {
guard let template = templates[name] else { return nil }
#if DEBUG
return template.render(object, library: self, reload: reload)
#else
return template.render(object, library: self)
#endif
}

/// Error returned by init() when parser fails
public struct ParserError: Swift.Error {
/// File error occurred in
Expand Down
30 changes: 30 additions & 0 deletions Sources/Mustache/Template+FileSystem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

extension MustacheTemplate {
/// Internal function to load a template from a file
/// - Parameters
/// - string: Template text
/// - filename: File template was loaded from
/// - Throws: MustacheTemplate.Error
init?(filename: String) throws {
let fs = FileManager()
guard let data = fs.contents(atPath: filename) else { return nil }
let string = String(decoding: data, as: Unicode.UTF8.self)
self.tokens = try Self.parse(string)
self.filename = filename
}
}
17 changes: 15 additions & 2 deletions Sources/Mustache/Template+Render.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -84,7 +84,20 @@ extension MustacheTemplate {
}

case .partial(let name, let indentation, let overrides):
if let template = context.library?.getTemplate(named: name) {
if var template = context.library?.getTemplate(named: name) {
#if DEBUG
if context.reloadPartials {
guard let filename = template.filename else {
preconditionFailure("Can only use reload if template was generated from a file")
}
do {
guard let partialTemplate = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" }
template = partialTemplate
} catch {
return "\(error)"
}
}
#endif
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
}

Expand Down
32 changes: 30 additions & 2 deletions Sources/Mustache/Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand All @@ -19,17 +19,44 @@ public struct MustacheTemplate: Sendable {
/// - Throws: MustacheTemplate.Error
public init(string: String) throws {
self.tokens = try Self.parse(string)
self.filename = nil
}

/// Render object using this template
/// - Parameter object: Object to render
/// - Parameters
/// - object: Object to render
/// - library: library template uses to access partials
/// - Returns: Rendered text
public func render(_ object: Any, library: MustacheLibrary? = nil) -> String {
self.render(context: .init(object, library: library))
}

/// Render object using this template
/// - Parameters
/// - object: Object to render
/// - library: library template uses to access partials
/// - reload: Should I reload this template when rendering. This is only available in debug builds
/// - Returns: Rendered text
public func render(_ object: Any, library: MustacheLibrary? = nil, reload: Bool) -> String {
#if DEBUG
if reload {
guard let filename else {
preconditionFailure("Can only use reload if template was generated from a file")
}
do {
guard let template = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" }
return template.render(context: .init(object, library: library, reloadPartials: reload))
} catch {
return "\(error)"
}
}
#endif
return self.render(context: .init(object, library: library))
}

internal init(_ tokens: [Token]) {
self.tokens = tokens
self.filename = nil
}

enum Token: Sendable {
Expand All @@ -45,4 +72,5 @@ public struct MustacheTemplate: Sendable {
}

var tokens: [Token]
let filename: String?
}
41 changes: 40 additions & 1 deletion Tests/MustacheTests/LibraryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -71,4 +71,43 @@ final class LibraryTests: XCTestCase {
XCTAssertEqual(parserError.context.columnNumber, 10)
}
}

#if DEBUG
func testReload() async throws {
let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".utf8)
try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache"))
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) }

let library = try await MustacheLibrary(directory: "./templates")
let object = ["value": ["value1", "value2"]]
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
let mustache2 = Data("<test2>{{#value}}<value>{{.}}</value>{{/value}}</test2>".utf8)
try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "<test2><value>value1</value><value>value2</value></test2>")
}

func testReloadPartial() async throws {
let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".utf8)
try mustache.write(to: URL(fileURLWithPath: "templates/test-partial.mustache"))
let mustache2 = Data("{{>test-partial}}".utf8)
try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
defer {
XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test-partial.mustache"))
XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache"))
XCTAssertNoThrow(try fs.removeItem(atPath: "templates"))
}

let library = try await MustacheLibrary(directory: "./templates")
let object = ["value": ["value1", "value2"]]
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
let mustache3 = Data("<test2>{{#value}}<value>{{.}}</value>{{/value}}</test2>".utf8)
try mustache3.write(to: URL(fileURLWithPath: "templates/test-partial.mustache"))
XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "<test2><value>value1</value><value>value2</value></test2>")
}
#endif
}

0 comments on commit 5bb66ac

Please sign in to comment.