Skip to content
This repository has been archived by the owner on Nov 16, 2020. It is now read-only.

improve render caching, fixes #24 #31

Merged
merged 8 commits into from
Aug 8, 2018
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/Packages
/*.xcodeproj
Package.resolved
DerivedData

10 changes: 7 additions & 3 deletions Sources/TemplateKit/Data/TemplateDataEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,13 @@ fileprivate final class _TemplateDataKeyedEncoder<K>: KeyedEncodingContainerProt
}

func encode<T>(_ value: T, forKey key: K) throws where T: Encodable {
let encoder = _TemplateDataEncoder(context: context, codingPath: codingPath + [key])
try value.encode(to: encoder)
if let data = value as? TemplateDataRepresentable {
try context.data.set(to: .data(data.convertToTemplateData()), at: codingPath + [key])
} else {

let encoder = _TemplateDataEncoder(context: context, codingPath: codingPath + [key])
try value.encode(to: encoder)
}
}
}

Expand Down Expand Up @@ -223,4 +228,3 @@ fileprivate final class _TemplateDataUnkeyedEncoder: UnkeyedEncodingContainer {
try value.encode(to: encoder)
}
}

2 changes: 1 addition & 1 deletion Sources/TemplateKit/Pipeline/ASTCache.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// Caches a `TemplateRenderer`'s parsed ASTs.
public struct ASTCache {
/// Internal AST storage.
internal var storage: [Int: [TemplateSyntax]]
internal var storage: [String: [TemplateSyntax]]

/// Creates a new `ASTCache`.
public init() {
Expand Down
152 changes: 92 additions & 60 deletions Sources/TemplateKit/Pipeline/TemplateRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/// The `templateFileEnding` should also be unique to that templating language.
///
/// See each protocol requirement for more information.
public protocol TemplateRenderer: class {
public protocol TemplateRenderer: ViewRenderer {
/// The available tags. `TemplateTag`s found in the AST will be looked up using this dictionary.
var tags: [String: TagRenderer] { get }

Expand All @@ -31,95 +31,127 @@ public protocol TemplateRenderer: class {
}

extension TemplateRenderer {
// MARK: Render

/// Renders template bytes into a view using the supplied context.
///
/// - parameters:
/// - template: Raw template bytes.
/// - context: `TemplateData` to expose as context to the template.
/// - file: Template description, will be used for generating errors.
/// - returns: `Future` containing the rendered `View`.
public func render(template: Data, _ context: TemplateData, file: String = "template") -> Future<View> {
return Future.flatMap(on: container) {
let hash = template.hashValue
let ast: [TemplateSyntax]
if let cached = self.astCache?.storage[hash] {
ast = cached
/// See `ViewRenderer`.
public var shouldCache: Bool {
get { return astCache != nil }
set {
if newValue {
astCache = .init()
} else {
let scanner = TemplateByteScanner(data: template, file: file)
ast = try self.parser.parse(scanner: scanner)
self.astCache?.storage[hash] = ast
astCache = nil
}

let serializer = TemplateSerializer(
renderer: self,
context: .init(data: context),
using: self.container
)
return serializer.serialize(ast: ast)
}
}
}

// MARK: Convenience.

/// Loads and renders a raw template at the supplied path.
extension TemplateRenderer {
// MARK: Render Path

/// Loads and renders a raw template at the supplied path using an empty context.
///
/// - parameters:
/// - path: Path to file contianing raw template bytes.
/// - context: `TemplateData` to expose as context to the template.
/// - returns: `Future` containing the rendered `View`.
public func render(_ path: String, _ context: TemplateData) -> Future<View> {
let path = path.hasSuffix(templateFileEnding) ? path : path + templateFileEnding
let absolutePath = path.hasPrefix("/") ? path : relativeDirectory + path

guard let data = FileManager.default.contents(atPath: absolutePath) else {
let error = TemplateKitError(
identifier: "fileNotFound",
reason: "No file was found at path: \(absolutePath)"
)
return Future.map(on: container) { throw error }
public func render(_ path: String) -> Future<View> {
return render(path, .null)
}

/// Renders the template bytes into a view using the supplied `Encodable` object as context.
///
/// - parameters:
/// - path: Path to file contianing raw template bytes.
/// - context: `Encodable` item that will be encoded to `TemplateData` and used as template context.
/// - returns: `Future` containing the rendered `View`.
public func render<E>(_ path: String, _ context: E) -> Future<View> where E: Encodable {
do {
return try TemplateDataEncoder().encode(context, on: self.container).flatMap { context in
return self.render(path, context)
}
} catch {
return container.future(error: error)
}

return render(template: data, context, file: absolutePath)
}

/// Loads and renders a raw template at the supplied path using an empty context.
/// Loads and renders a raw template at the supplied path.
///
/// - parameters:
/// - path: Path to file contianing raw template bytes.
/// - context: `TemplateData` to expose as context to the template.
/// - returns: `Future` containing the rendered `View`.
public func render(_ path: String) -> Future<View> {
return render(path, .null)
public func render(_ path: String, _ context: TemplateData) -> Future<View> {
do {
let path = path.hasSuffix(templateFileEnding) ? path : path + templateFileEnding
let absolutePath = path.hasPrefix("/") ? path : relativeDirectory + path

let ast: [TemplateSyntax]
if let cached = astCache?.storage[absolutePath] {
ast = cached
} else {
guard let data = FileManager.default.contents(atPath: absolutePath) else {
throw TemplateKitError(
identifier: "fileNotFound",
reason: "No file was found at path: \(absolutePath)"
)
}
ast = try _parse(data, file: absolutePath)
astCache?.storage[absolutePath] = ast
}
return _serialize(context, ast, file: absolutePath)
} catch {
return container.future(error: error)
}
}

// MARK: Codable

// MARK: Render Data
/// Renders the template bytes into a view using the supplied `Encodable` object as context.
///
/// - parameters:
/// - template: Raw template bytes.
/// - context: `Encodable` item that will be encoded to `TemplateData` and used as template context.
/// - returns: `Future` containing the rendered `View`.
public func render<E>(template: Data, _ context: E) -> Future<View> where E: Encodable {
return Future.flatMap(on: container) {
return try TemplateDataEncoder().encode(context, on: self.container).flatMap(to: View.self) { context in
do {
return try TemplateDataEncoder().encode(context, on: self.container).flatMap { context in
return self.render(template: template, context)
}
} catch {
return container.future(error: error)
}
}

/// Renders the template bytes into a view using the supplied `Encodable` object as context.
/// Renders template bytes into a view using the supplied context.
///
/// - parameters:
/// - path: Path to file contianing raw template bytes.
/// - context: `Encodable` item that will be encoded to `TemplateData` and used as template context.
/// - template: Raw template bytes.
/// - context: `TemplateData` to expose as context to the template.
/// - file: Template description, will be used for generating errors.
/// - returns: `Future` containing the rendered `View`.
public func render<E>(_ path: String, _ context: E) -> Future<View> where E: Encodable {
return Future.flatMap(on: container) {
return try TemplateDataEncoder().encode(context, on: self.container).flatMap(to: View.self) { context in
return self.render(path, context)
}
public func render(template: Data, _ context: TemplateData, file: String? = nil) -> Future<View> {
let path = file ?? "template"
do {
return try _serialize(context, _parse(template, file: path), file: path)
} catch {
return container.future(error: error)
}
}

// MARK: Private

/// Serializes an AST + Context
private func _serialize(_ context: TemplateData, _ ast: [TemplateSyntax], file: String) -> Future<View> {
let serializer = TemplateSerializer(
renderer: self,
context: .init(data: context),
using: self.container
)
return serializer.serialize(ast: ast)
}

/// Parses data to AST.
private func _parse(_ template: Data, file: String) throws -> [TemplateSyntax] {
print("PARSE: \(file)")
let scanner = TemplateByteScanner(data: template, file: file)
return try parser.parse(scanner: scanner)
}
}
2 changes: 1 addition & 1 deletion Sources/TemplateKit/Tag/DateFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public final class DateFormat: TagRenderer {

let formatter = DateFormatter()
/// Assume the date is a floating point number
let date = Date(timeIntervalSinceReferenceDate: tag.parameters[0].double ?? 0)
let date = Date(timeIntervalSince1970: tag.parameters[0].double ?? 0)
/// Set format as the second param or default to ISO-8601 format.
if tag.parameters.count == 2, let param = tag.parameters[1].string {
formatter.dateFormat = param
Expand Down
15 changes: 0 additions & 15 deletions Sources/TemplateKit/ViewRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/// Renders an Encodable object into a `View`.
public protocol ViewRenderer: class {

/// For view renderers that use a cache to optimize view loads, use this variable to toggle whether or not cache should be implemented
///
/// Normally, cache is disabled in development so views can be tested w/o recompilation.
Expand All @@ -15,17 +14,3 @@ public protocol ViewRenderer: class {
/// - returns: `Future` containing the rendered `View`.
func render<E>(_ path: String, _ context: E) -> Future<View> where E: Encodable
}

extension ViewRenderer where Self: TemplateRenderer {
/// See `ViewRenderer`.
public var shouldCache: Bool {
get { return astCache != nil }
set {
if newValue {
astCache = .init()
} else {
astCache = nil
}
}
}
}
36 changes: 36 additions & 0 deletions Tests/TemplateKitTests/TemplateDataEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,41 @@ class TemplateDataEncoderTests: XCTestCase {
).serialize(ast: ast).wait()
XCTAssertEqual(String(data: view.data, encoding: .utf8), "headVaportail")
}

// https://github.com/vapor/template-kit/issues/20
func testGH20() throws {
func wrap(_ syntax: TemplateSyntaxType) -> TemplateSyntax {
return TemplateSyntax(type: syntax, source: TemplateSource(file: "test", line: 0, column: 0, range: 0..<1))
}

let path: [CodingKey] = [
BasicKey.init("date"),
]

let worker = EmbeddedEventLoop()
let container = BasicContainer(config: .init(), environment: .testing, services: .init(), on: worker)
let renderer = PlaintextRenderer(viewsDir: "/", on: container)
renderer.tags["date"] = DateFormat()
let ast: [TemplateSyntax] = [
wrap(.tag(TemplateTag(
name: "date",
parameters: [wrap(.identifier(TemplateIdentifier(path: path)))],
body: nil
)))
]
let date = Date()
let data = try TemplateDataEncoder().testEncode(["date": date])
print(data)
let view = try TemplateSerializer(
renderer: renderer,
context: TemplateDataContext(data: data),
using: container
).serialize(ast: ast).wait()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
print(formatter.string(from: date))
XCTAssertEqual(String(data: view.data, encoding: .utf8), formatter.string(from: date))
}

static var allTests = [
("testString", testString),
Expand All @@ -176,6 +211,7 @@ class TemplateDataEncoderTests: XCTestCase {
("testComplexEncodable", testComplexEncodable),
("testNestedEncodable", testNestedEncodable),
("testGH10", testGH10),
("testGH20", testGH20),
]
}

Expand Down