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

Commit

Permalink
Merge pull request #31 from vapor/render-cache-24
Browse files Browse the repository at this point in the history
improve render caching, fixes #24
  • Loading branch information
tanner0101 authored Aug 8, 2018
2 parents 89cd49c + fd2c0ef commit 9217849
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 80 deletions.
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

0 comments on commit 9217849

Please sign in to comment.