Remove custom Result type and throw errors

This commit is contained in:
Kyle Fuller
2015-09-25 12:53:45 -07:00
parent 25f5583542
commit 9c335caeb6
17 changed files with 211 additions and 460 deletions

View File

@@ -106,7 +106,7 @@ Heres an example. Registering a template tag called `custom` which just rende
```swift ```swift
parser.registerSimpleTag("custom") { context in parser.registerSimpleTag("custom") { context in
return .Success("Hello World") return "Hello World"
} }
``` ```
@@ -144,7 +144,7 @@ class DebugNode : Node {
self.nodes = nodes self.nodes = nodes
} }
func render(context: Context) -> Result { func render(context: Context) throws -> String {
// Is there a debug variable inside the context? // Is there a debug variable inside the context?
if let debug = context["debug"] as? Bool { if let debug = context["debug"] as? Bool {
// Is debug set to true? // Is debug set to true?
@@ -155,7 +155,7 @@ class DebugNode : Node {
} }
// Debug is turned off, so let's not render anything // Debug is turned off, so let's not render anything
return .Success("") return ""
} }
} }
``` ```
@@ -163,15 +163,10 @@ class DebugNode : Node {
We will need to write a parser to parse up until the `enddebug` template block and create a `DebugNode` with the nodes in-between. If there was another error form another Node inside, then we will return that error. We will need to write a parser to parse up until the `enddebug` template block and create a `DebugNode` with the nodes in-between. If there was another error form another Node inside, then we will return that error.
```swift ```swift
parser.registerTag("debug") { (parser, token) -> TokenParser.Result in parser.registerTag("debug") { parser, token in
// Use the parser to parse every token up until the `enddebug` block. // Use the parser to parse every token up until the `enddebug` block.
switch parser.parse(until(["enddebug"])) let nodes = try until(["enddebug"]))
case .Success(let nodes): return DebugNode(nodes)
nodes
case .Error(let error):
// There was an error, this is most-likely due to another template block returning an error.
return .Error(error)
}
} }
``` ```

View File

@@ -25,14 +25,12 @@ let context = Context(dictionary: [
] ]
]) ])
let template = try? Template(named: "template.stencil") do {
let result = template!.render(context) let template = Template(named: "template.stencil")
let rendered = template.render(context)
switch result { print(rendered)
case .Error(let error): } catch {
println("There was an error rendering your template (\(error)).") print("Failed to render template \(error)")
case .Success(let string):
println("\(string)")
} }
``` ```
@@ -157,13 +155,13 @@ you to write your own custom tags. The following is the simplest form:
```swift ```swift
template.parser.registerSimpleTag("custom") { context in template.parser.registerSimpleTag("custom") { context in
return .Success("Hello World") return "Hello World"
} }
``` ```
When your tag is used via `{% custom %}` it will execute the registered block When your tag is used via `{% custom %}` it will execute the registered block
of code allowing you to modify or retrieve a value from the context. Then of code allowing you to modify or retrieve a value from the context. Then
return either a string rendered in your template, or an error. return either a string rendered in your template, or throw an error.
If you want to accept arguments or to capture different tokens between two sets If you want to accept arguments or to capture different tokens between two sets
of template tags. You will need to call the `registerTag` API which accepts a of template tags. You will need to call the `registerTag` API which accepts a

View File

@@ -16,7 +16,6 @@
27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; }; 27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; };
27CE0B011A50CBD1004A105B /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B001A50CBD1004A105B /* Include.swift */; }; 27CE0B011A50CBD1004A105B /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B001A50CBD1004A105B /* Include.swift */; };
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B031A50CBEA004A105B /* IncludeTests.swift */; }; 27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B031A50CBEA004A105B /* IncludeTests.swift */; };
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE4C0919FD29D000B9E0C5 /* Result.swift */; };
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CA19F92B4F002CF74B /* VariableTests.swift */; }; 7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CA19F92B4F002CF74B /* VariableTests.swift */; };
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CC19F92B61002CF74B /* Variable.swift */; }; 7725B3CD19F92B61002CF74B /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CC19F92B61002CF74B /* Variable.swift */; };
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CE19F94214002CF74B /* Tokenizer.swift */; }; 7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CE19F94214002CF74B /* Tokenizer.swift */; };
@@ -59,7 +58,6 @@
27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; }; 27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; };
27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = "<group>"; }; 27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = "<group>"; };
27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.swift; sourceTree = "<group>"; }; 27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.swift; sourceTree = "<group>"; };
71CE4C0919FD29D000B9E0C5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
7725B3CA19F92B4F002CF74B /* VariableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = "<group>"; }; 7725B3CA19F92B4F002CF74B /* VariableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = "<group>"; };
7725B3CC19F92B61002CF74B /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = "<group>"; }; 7725B3CC19F92B61002CF74B /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = "<group>"; };
7725B3CE19F94214002CF74B /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = "<group>"; }; 7725B3CE19F94214002CF74B /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = "<group>"; };
@@ -154,7 +152,6 @@
77EB082A19FA8600001870F1 /* Lexer.swift */, 77EB082A19FA8600001870F1 /* Lexer.swift */,
7725B3D419F9438F002CF74B /* Node.swift */, 7725B3D419F9438F002CF74B /* Node.swift */,
7725B3D619F94A43002CF74B /* Parser.swift */, 7725B3D619F94A43002CF74B /* Parser.swift */,
71CE4C0919FD29D000B9E0C5 /* Result.swift */,
77EB082419F96E88001870F1 /* Template.swift */, 77EB082419F96E88001870F1 /* Template.swift */,
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */, 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */,
7725B3CE19F94214002CF74B /* Tokenizer.swift */, 7725B3CE19F94214002CF74B /* Tokenizer.swift */,
@@ -422,7 +419,6 @@
7725B3D719F94A43002CF74B /* Parser.swift in Sources */, 7725B3D719F94A43002CF74B /* Parser.swift in Sources */,
77EB082519F96E88001870F1 /* Template.swift in Sources */, 77EB082519F96E88001870F1 /* Template.swift in Sources */,
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */, 7725B3CD19F92B61002CF74B /* Variable.swift in Sources */,
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */,
7725B3D519F9438F002CF74B /* Node.swift in Sources */, 7725B3D519F9438F002CF74B /* Node.swift in Sources */,
27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */, 27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */,
27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */, 27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */,

View File

@@ -1,83 +1,52 @@
import Foundation import Foundation
struct NodeError : Error { public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible {
let token:Token public let description:String
let message:String
init(token:Token, message:String) { public init(_ description:String) {
self.token = token self.description = description
self.message = message
}
var description:String {
return "\(token.components().first!): \(message)"
} }
} }
public protocol Node { public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
/// Return the node rendered as a string, or returns a failure return lhs.description == rhs.description
func render(context:Context) -> Result
} }
extension Array { public protocol NodeType {
func map<U>(block:((Element) -> (U?, Error?))) -> ([U]?, Error?) { /// Render the node in the given context
var results = [U]() func render(context:Context) throws -> String
for item in self {
let (result, error) = block(item)
if let error = error {
return (nil, error)
} else if (result != nil) {
// let result = result exposing a bug in the Swift compier :(
results.append(result!)
}
}
return (results, nil)
}
} }
public func renderNodes(nodes:[Node], context:Context) -> Result { /// Render the collection of nodes in the given context
var result = "" public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String {
return try nodes.map { try $0.render(context) }.joinWithSeparator("")
for item in nodes {
switch item.render(context) {
case .Success(let string):
result += string
case .Error(let error):
return .Error(error)
}
}
return .Success(result)
} }
public class SimpleNode : Node { public class SimpleNode : NodeType {
let handler:(Context) -> (Result) let handler:Context throws -> String
public init(handler:((Context) -> (Result))) { public init(handler:Context throws -> String) {
self.handler = handler self.handler = handler
} }
public func render(context:Context) -> Result { public func render(context: Context) throws -> String {
return handler(context) return try handler(context)
} }
} }
public class TextNode : Node { public class TextNode : NodeType {
public let text:String public let text:String
public init(text:String) { public init(text:String) {
self.text = text self.text = text
} }
public func render(context:Context) -> Result { public func render(context:Context) throws -> String {
return .Success(self.text) return self.text
} }
} }
public class VariableNode : Node { public class VariableNode : NodeType {
public let variable:Variable public let variable:Variable
public init(variable:Variable) { public init(variable:Variable) {
@@ -88,23 +57,23 @@ public class VariableNode : Node {
self.variable = Variable(variable) self.variable = Variable(variable)
} }
public func render(context:Context) -> Result { public func render(context:Context) throws -> String {
let result:AnyObject? = variable.resolve(context) let result:AnyObject? = variable.resolve(context)
if let result = result as? String { if let result = result as? String {
return .Success(result) return result
} else if let result = result as? NSObject { } else if let result = result as? NSObject {
return .Success(result.description) return result.description
} }
return .Success("") return ""
} }
} }
public class NowNode : Node { public class NowNode : NodeType {
public let format:Variable public let format:Variable
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { public class func parse(parser:TokenParser, token:Token) -> NodeType {
var format:Variable? var format:Variable?
let components = token.components() let components = token.components()
@@ -112,7 +81,7 @@ public class NowNode : Node {
format = Variable(components[1]) format = Variable(components[1])
} }
return .Success(node:NowNode(format:format)) return NowNode(format:format)
} }
public init(format:Variable?) { public init(format:Variable?) {
@@ -123,7 +92,7 @@ public class NowNode : Node {
} }
} }
public func render(context: Context) -> Result { public func render(context: Context) throws -> String {
let date = NSDate() let date = NSDate()
let format: AnyObject? = self.format.resolve(context) let format: AnyObject? = self.format.resolve(context)
var formatter:NSDateFormatter? var formatter:NSDateFormatter?
@@ -134,156 +103,115 @@ public class NowNode : Node {
formatter = NSDateFormatter() formatter = NSDateFormatter()
formatter!.dateFormat = format formatter!.dateFormat = format
} else { } else {
return .Success("") return ""
} }
return .Success(formatter!.stringFromDate(date)) return formatter!.stringFromDate(date)
} }
} }
public class ForNode : Node { public class ForNode : NodeType {
let variable:Variable let variable:Variable
let loopVariable:String let loopVariable:String
let nodes:[Node] let nodes:[NodeType]
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let components = token.components() let components = token.components()
if components.count == 4 && components[2] == "in" { if components.count == 4 && components[2] == "in" {
let loopVariable = components[1] let loopVariable = components[1]
let variable = components[3] let variable = components[3]
var forNodes:[Node]! var emptyNodes = [NodeType]()
var emptyNodes = [Node]()
switch parser.parse(until(["endfor", "empty"])) { let forNodes = try parser.parse(until(["endfor", "empty"]))
case .Success(let nodes):
forNodes = nodes
case .Error(let error):
return .Error(error: error)
}
if let token = parser.nextToken() { if let token = parser.nextToken() {
if token.contents == "empty" { if token.contents == "empty" {
switch parser.parse(until(["endfor"])) { emptyNodes = try parser.parse(until(["endfor"]))
case .Success(let nodes):
emptyNodes = nodes
case .Error(let error):
return .Error(error: error)
}
parser.nextToken() parser.nextToken()
} }
} else { } else {
return .Error(error: NodeError(token: token, message: "`endfor` was not found.")) throw TemplateSyntaxError("`endfor` was not found.")
} }
return .Success(node:ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)) return ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes)
} }
return .Error(error: NodeError(token: token, message: "Invalid syntax. Expected `for x in y`.")) throw TemplateSyntaxError("'for' statements should use the following 'for x in y' `\(token.contents)`.")
} }
public init(variable:String, loopVariable:String, nodes:[Node], emptyNodes:[Node]) { public init(variable:String, loopVariable:String, nodes:[NodeType], emptyNodes:[NodeType]) {
self.variable = Variable(variable) self.variable = Variable(variable)
self.loopVariable = loopVariable self.loopVariable = loopVariable
self.nodes = nodes self.nodes = nodes
} }
public func render(context: Context) -> Result { public func render(context: Context) throws -> String {
let values = variable.resolve(context) as? [AnyObject] if let values = variable.resolve(context) as? [AnyObject] {
var output = "" return try values.map { item in
if let values = values {
for item in values {
context.push() context.push()
context[loopVariable] = item context[loopVariable] = item
let result = renderNodes(nodes, context: context) let result = try renderNodes(nodes, context)
context.pop() context.pop()
return result
switch result { }.joinWithSeparator("")
case .Success(let string):
output += string
case .Error(let error):
return .Error(error)
}
}
} }
return .Success(output) return ""
} }
} }
public class IfNode : Node { public class IfNode : NodeType {
public let variable:Variable public let variable:Variable
public let trueNodes:[Node] public let trueNodes:[NodeType]
public let falseNodes:[Node] public let falseNodes:[NodeType]
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let variable = token.components()[1] let variable = token.components()[1]
var trueNodes = [Node]() var trueNodes = [NodeType]()
var falseNodes = [Node]() var falseNodes = [NodeType]()
switch parser.parse(until(["endif", "else"])) { trueNodes = try parser.parse(until(["endif", "else"]))
case .Success(let nodes):
trueNodes = nodes
case .Error(let error):
return .Error(error: error)
}
if let token = parser.nextToken() { if let token = parser.nextToken() {
if token.contents == "else" { if token.contents == "else" {
switch parser.parse(until(["endif"])) { falseNodes = try parser.parse(until(["endif"]))
case .Success(let nodes):
falseNodes = nodes
case .Error(let error):
return .Error(error: error)
}
parser.nextToken() parser.nextToken()
} }
} else { } else {
return .Error(error:NodeError(token: token, message: "`endif` was not found.")) throw TemplateSyntaxError("`endif` was not found.")
} }
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)) return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
} }
public class func parse_ifnot(parser:TokenParser, token:Token) -> TokenParser.Result { public class func parse_ifnot(parser:TokenParser, token:Token) throws -> NodeType {
let variable = token.components()[1] let variable = token.components()[1]
var trueNodes = [Node]() var trueNodes = [NodeType]()
var falseNodes = [Node]() var falseNodes = [NodeType]()
switch parser.parse(until(["endif", "else"])) { falseNodes = try parser.parse(until(["endif", "else"]))
case .Success(let nodes):
falseNodes = nodes
case .Error(let error):
return .Error(error: error)
}
if let token = parser.nextToken() { if let token = parser.nextToken() {
if token.contents == "else" { if token.contents == "else" {
switch parser.parse(until(["endif"])) { trueNodes = try parser.parse(until(["endif"]))
case .Success(let nodes):
trueNodes = nodes
case .Error(let error):
return .Error(error: error)
}
parser.nextToken() parser.nextToken()
} }
} else { } else {
return .Error(error:NodeError(token: token, message: "`endif` was not found.")) throw TemplateSyntaxError("`endif` was not found.")
} }
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)) return IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes)
} }
public init(variable:String, trueNodes:[Node], falseNodes:[Node]) { public init(variable:String, trueNodes:[NodeType], falseNodes:[NodeType]) {
self.variable = Variable(variable) self.variable = Variable(variable)
self.trueNodes = trueNodes self.trueNodes = trueNodes
self.falseNodes = falseNodes self.falseNodes = falseNodes
} }
public func render(context: Context) -> Result { public func render(context: Context) throws -> String {
let result: AnyObject? = variable.resolve(context) let result: AnyObject? = variable.resolve(context)
var truthy = false var truthy = false
@@ -296,7 +224,12 @@ public class IfNode : Node {
} }
context.push() context.push()
let output = renderNodes(truthy ? trueNodes : falseNodes, context: context) let output:String
if truthy {
output = try renderNodes(trueNodes, context)
} else {
output = try renderNodes(falseNodes, context)
}
context.pop() context.pop()
return output return output

View File

@@ -1,5 +1,3 @@
import Foundation
public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool { public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
if let name = token.components().first { if let name = token.components().first {
for tag in tags { for tag in tags {
@@ -14,18 +12,7 @@ public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
/// A class for parsing an array of tokens and converts them into a collection of Node's /// A class for parsing an array of tokens and converts them into a collection of Node's
public class TokenParser { public class TokenParser {
public typealias TagParser = (TokenParser, Token) -> Result public typealias TagParser = (TokenParser, Token) throws -> NodeType
public typealias NodeList = [Node]
public enum Result {
case Success(node: Node)
case Error(error: Stencil.Error)
}
public enum Results {
case Success(nodes: NodeList)
case Error(error: Stencil.Error)
}
private var tokens:[Token] private var tokens:[Token]
private var tags = [String:TagParser]() private var tags = [String:TagParser]()
@@ -47,19 +34,19 @@ public class TokenParser {
} }
/// Registers a simple template tag with a name and a handler /// Registers a simple template tag with a name and a handler
public func registerSimpleTag(name:String, handler:((Context) -> (Stencil.Result))) { public func registerSimpleTag(name:String, handler:(Context throws -> String)) {
registerTag(name, parser: { (parser, token) -> TokenParser.Result in registerTag(name, parser: { parser, token in
return .Success(node:SimpleNode(handler: handler)) return SimpleNode(handler: handler)
}) })
} }
/// Parse the given tokens into nodes /// Parse the given tokens into nodes
public func parse() -> Results { public func parse() throws -> [NodeType] {
return parse(nil) return try parse(nil)
} }
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) -> TokenParser.Results { public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] {
var nodes = NodeList() var nodes = [NodeType]()
while tokens.count > 0 { while tokens.count > 0 {
let token = nextToken()! let token = nextToken()!
@@ -75,26 +62,19 @@ public class TokenParser {
if let parse_until = parse_until { if let parse_until = parse_until {
if parse_until(parser: self, token: token) { if parse_until(parser: self, token: token) {
prependToken(token) prependToken(token)
return .Success(nodes:nodes) return nodes
} }
} }
if let tag = tag { if let tag = tag, let parser = self.tags[tag] {
if let parser = self.tags[tag] { nodes.append(try parser(self, token))
switch parser(self, token) {
case .Success(let node):
nodes.append(node)
case .Error(let error):
return .Error(error:error)
}
}
} }
case .Comment: case .Comment:
continue continue
} }
} }
return .Success(nodes:nodes) return nodes
} }
public func nextToken() -> Token? { public func nextToken() -> Token? {

View File

@@ -1,25 +0,0 @@
import Foundation
public protocol Error : CustomStringConvertible {
}
public func ==(lhs:Error, rhs:Error) -> Bool {
return lhs.description == rhs.description
}
public enum Result : Equatable {
case Success(String)
case Error(Stencil.Error)
}
public func ==(lhs:Result, rhs:Result) -> Bool {
switch (lhs, rhs) {
case (.Success(let lhsValue), .Success(let rhsValue)):
return lhsValue == rhsValue
case (.Error(let lhsValue), .Error(let rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}

View File

@@ -12,15 +12,15 @@ public class Template {
/// Create a template with the given name inside the given bundle /// Create a template with the given name inside the given bundle
public convenience init(named:String, inBundle bundle:NSBundle?) throws { public convenience init(named:String, inBundle bundle:NSBundle?) throws {
var url:NSURL? let url:NSURL
if let bundle = bundle { if let bundle = bundle {
url = bundle.URLForResource(named, withExtension: nil) url = bundle.URLForResource(named, withExtension: nil)!
} else { } else {
url = NSBundle.mainBundle().URLForResource(named, withExtension: nil) url = NSBundle.mainBundle().URLForResource(named, withExtension: nil)!
} }
try self.init(URL:url!) try self.init(URL:url)
} }
/// Create a template with a file found at the given URL /// Create a template with a file found at the given URL
@@ -40,20 +40,9 @@ public class Template {
parser = TokenParser(tokens: tokens) parser = TokenParser(tokens: tokens)
} }
/// Render the given template in a context /// Render the given template
public func render(context:Context) -> Result { public func render(context:Context? = nil) throws -> String {
switch parser.parse() { let nodes = try parser.parse()
case .Success(let nodes): return try renderNodes(nodes, context ?? Context())
return renderNodes(nodes, context: context)
case .Error(let error):
return .Error(error)
}
}
/// Render the given template without a context
public func render() -> Result {
let context = Context()
return render(context)
} }
} }

View File

@@ -1,11 +1,3 @@
//
// TemplateLoader.swift
// Stencil
//
// Created by Kyle Fuller on 28/12/2014.
// Copyright (c) 2014 Cocode. All rights reserved.
//
import Foundation import Foundation
import PathKit import PathKit

View File

@@ -1,44 +1,37 @@
import Foundation import Foundation
import PathKit import PathKit
extension String : Error {
public var description:String {
return self
}
}
public class IncludeNode : Node { public class IncludeNode : NodeType {
public let templateName:String public let templateName:String
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { public class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let bits = token.contents.componentsSeparatedByString("\"") let bits = token.contents.componentsSeparatedByString("\"")
if bits.count != 3 { if bits.count != 3 {
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be included")) throw TemplateSyntaxError("'include' tag takes one argument, the template file to be included")
} }
return .Success(node:IncludeNode(templateName: bits[1])) return IncludeNode(templateName: bits[1])
} }
public init(templateName:String) { public init(templateName:String) {
self.templateName = templateName self.templateName = templateName
} }
public func render(context: Context) -> Result { public func render(context: Context) throws -> String {
if let loader = context["loader"] as? TemplateLoader { if let loader = context["loader"] as? TemplateLoader {
if let template = loader.loadTemplate(templateName) { if let template = loader.loadTemplate(templateName) {
return template.render(context) return try template.render(context)
} }
let paths:String = loader.paths.map { path in let paths:String = loader.paths.map { path in
return path.description return path.description
}.joinWithSeparator(", ") }.joinWithSeparator(", ")
let error = "Template '\(templateName)' not found in \(paths)" throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
return .Error(error)
} }
let error = "Template loader not in context" throw TemplateSyntaxError("Template loader not in context")
return .Error(error)
} }
} }

View File

@@ -24,36 +24,32 @@ func any<Element>(elements:[Element], closure:(Element -> Bool)) -> Element? {
return nil return nil
} }
class ExtendsNode : Node { class ExtendsNode : NodeType {
let templateName:String let templateName:String
let blocks:[String:BlockNode] let blocks:[String:BlockNode]
class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let bits = token.contents.componentsSeparatedByString("\"") let bits = token.contents.componentsSeparatedByString("\"")
if bits.count != 3 { if bits.count != 3 {
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be extended")) throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
} }
switch parser.parse() { let parsedNodes = try parser.parse()
case .Success(let nodes): if (any(parsedNodes) { ($0 as? ExtendsNode) != nil }) != nil {
if (any(nodes) { ($0 as? ExtendsNode) != nil }) != nil { throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
return .Error(error:"'extends' cannot appear more than once in the same template")
} }
let blockNodes = nodes.filter { node in node is BlockNode } let blockNodes = parsedNodes.filter { node in node is BlockNode }
let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:Node) -> [String:BlockNode] in let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:NodeType) -> [String:BlockNode] in
let node = (node as! BlockNode) let node = (node as! BlockNode)
var dict = accumulator var dict = accumulator
dict[node.name] = node dict[node.name] = node
return dict return dict
}) })
return .Success(node:ExtendsNode(templateName: bits[1], blocks: nodes)) return ExtendsNode(templateName: bits[1], blocks: nodes)
case .Error(let error):
return .Error(error:error)
}
} }
init(templateName:String, blocks:[String:BlockNode]) { init(templateName:String, blocks:[String:BlockNode]) {
@@ -61,12 +57,12 @@ class ExtendsNode : Node {
self.blocks = blocks self.blocks = blocks
} }
func render(context: Context) -> Result { func render(context: Context) throws -> String {
if let loader = context["loader"] as? TemplateLoader { if let loader = context["loader"] as? TemplateLoader {
if let template = loader.loadTemplate(templateName) { if let template = loader.loadTemplate(templateName) {
let blockContext = BlockContext(blocks: blocks) let blockContext = BlockContext(blocks: blocks)
context.push([BlockContext.contextKey: blockContext]) context.push([BlockContext.contextKey: blockContext])
let result = template.render(context) let result = try template.render(context)
context.pop() context.pop()
return result return result
} }
@@ -74,51 +70,39 @@ class ExtendsNode : Node {
let paths:String = loader.paths.map { path in let paths:String = loader.paths.map { path in
return path.description return path.description
}.joinWithSeparator(", ") }.joinWithSeparator(", ")
let error = "Template '\(templateName)' not found in \(paths)" throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)")
return .Error(error)
} }
let error = "Template loader not in context" throw TemplateSyntaxError("Template loader not in context")
return .Error(error)
} }
} }
class BlockNode : Node { class BlockNode : NodeType {
let name:String let name:String
let nodes:[Node] let nodes:[NodeType]
class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { class func parse(parser:TokenParser, token:Token) throws -> NodeType {
let bits = token.components() let bits = token.components()
if bits.count != 2 { if bits.count != 2 {
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be included")) throw TemplateSyntaxError("'block' tag takes one argument, the template file to be included")
} }
let blockName = bits[1] let blockName = bits[1]
var nodes = [Node]() let nodes = try parser.parse(until(["endblock"]))
return BlockNode(name:blockName, nodes:nodes)
switch parser.parse(until(["endblock"])) {
case .Success(let blockNodes):
nodes = blockNodes
case .Error(let error):
return .Error(error: error)
} }
return .Success(node:BlockNode(name:blockName, nodes:nodes)) init(name:String, nodes:[NodeType]) {
}
init(name:String, nodes:[Node]) {
self.name = name self.name = name
self.nodes = nodes self.nodes = nodes
} }
func render(context: Context) -> Result { func render(context: Context) throws -> String {
if let blockContext = context[BlockContext.contextKey] as? BlockContext { if let blockContext = context[BlockContext.contextKey] as? BlockContext, node = blockContext.pop(name) {
if let node = blockContext.pop(name) { return try node.render(context)
return node.render(context)
}
} }
return renderNodes(nodes, context: context) return try renderNodes(nodes, context)
} }
} }

View File

@@ -1,6 +1,3 @@
import Foundation
public enum Token : Equatable { public enum Token : Equatable {
/// A token representing a piece of text. /// A token representing a piece of text.
case Text(value:String) case Text(value:String)

View File

@@ -2,16 +2,10 @@ import Foundation
import XCTest import XCTest
import Stencil import Stencil
class ErrorNodeError : Error {
var description: String {
return "Node Error"
}
}
class ErrorNode : Node { class ErrorNode : NodeType {
func render(context: Context) -> Result { func render(context: Context) throws -> String {
throw TemplateSyntaxError("Custom Error")
return .Error(ErrorNodeError())
} }
} }
@@ -30,73 +24,39 @@ class NodeTests: XCTestCase {
class TextNodeTests: NodeTests { class TextNodeTests: NodeTests {
func testTextNodeResolvesText() { func testTextNodeResolvesText() {
let node = TextNode(text:"Hello World") let node = TextNode(text:"Hello World")
let _ = node.render(context) XCTAssertEqual(try? node.render(context), "Hello World")
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "Hello World")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
} }
class VariableNodeTests: NodeTests { class VariableNodeTests: NodeTests {
func testVariableNodeResolvesVariable() { func testVariableNodeResolvesVariable() {
let node = VariableNode(variable:Variable("name")) let node = VariableNode(variable:Variable("name"))
XCTAssertEqual(try? node.render(context), "Kyle")
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "Kyle")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
func testVariableNodeResolvesNonStringVariable() { func testVariableNodeResolvesNonStringVariable() {
let node = VariableNode(variable:Variable("age")) let node = VariableNode(variable:Variable("age"))
XCTAssertEqual(try? node.render(context), "27")
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "27")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
} }
class RenderNodeTests: NodeTests { class RenderNodeTests: NodeTests {
func testRenderingNodes() { func testRenderingNodes() {
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [Node] let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [NodeType]
switch renderNodes(nodes, context: context) { XCTAssertEqual(try? renderNodes(nodes, context), "Hello Kyle")
case .Success(let result):
XCTAssertEqual(result, "Hello Kyle")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
func testRenderingNodesWithFailure() { func testRenderingNodesWithFailure() {
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name"), ErrorNode()] as [Node] let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name"), ErrorNode()] as [NodeType]
switch renderNodes(nodes, context: context) { assertFailure(try renderNodes(nodes, context), TemplateSyntaxError("Custom Error"))
case .Success:
XCTAssert(false, "Unexpected success")
case .Error(let error):
XCTAssertEqual("\(error)", "Node Error")
}
} }
} }
class ForNodeTests: NodeTests { class ForNodeTests: NodeTests {
func testForNodeRender() { func testForNodeRender() {
let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[]) let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[])
switch node.render(context) { XCTAssertEqual(try? node.render(context), "123")
case .Success(let string):
XCTAssertEqual(string, "123")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
} }
@@ -114,7 +74,7 @@ class IfNodeTests: NodeTests {
] ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! IfNode let node = nodes.first as! IfNode
let trueNode = node.trueNodes.first as! TextNode let trueNode = node.trueNodes.first as! TextNode
let falseNode = node.falseNodes.first as! TextNode let falseNode = node.falseNodes.first as! TextNode
@@ -138,7 +98,7 @@ class IfNodeTests: NodeTests {
] ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! IfNode let node = nodes.first as! IfNode
let trueNode = node.trueNodes.first as! TextNode let trueNode = node.trueNodes.first as! TextNode
let falseNode = node.falseNodes.first as! TextNode let falseNode = node.falseNodes.first as! TextNode
@@ -158,7 +118,7 @@ class IfNodeTests: NodeTests {
] ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertFailure(parser.parse(), description: "if: `endif` was not found.") assertFailure(try parser.parse(), TemplateSyntaxError("`endif` was not found."))
} }
func testParseIfNotWithoutEndIfError() { func testParseIfNotWithoutEndIfError() {
@@ -167,31 +127,19 @@ class IfNodeTests: NodeTests {
] ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertFailure(parser.parse(), description: "ifnot: `endif` was not found.") assertFailure(try parser.parse(), TemplateSyntaxError("`endif` was not found."))
} }
// MARK: Rendering // MARK: Rendering
func testIfNodeRenderTruth() { func testIfNodeRenderTruth() {
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")]) let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
XCTAssertEqual(try? node.render(context), "true")
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "true")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
func testIfNodeRenderFalse() { func testIfNodeRenderFalse() {
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")]) let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
XCTAssertEqual(try? node.render(context), "false")
switch node.render(context) {
case .Success(let string):
XCTAssertEqual(string, "false")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
} }
@@ -204,7 +152,7 @@ class NowNodeTests: NodeTests {
let tokens = [ Token.Block(value: "now") ] let tokens = [ Token.Block(value: "now") ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! NowNode let node = nodes.first as! NowNode
XCTAssertEqual(nodes.count, 1) XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.format.variable, "\"yyyy-MM-dd 'at' HH:mm\"") XCTAssertEqual(node.format.variable, "\"yyyy-MM-dd 'at' HH:mm\"")
@@ -215,7 +163,7 @@ class NowNodeTests: NodeTests {
let tokens = [ Token.Block(value: "now \"HH:mm\"") ] let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! NowNode let node = nodes.first as! NowNode
XCTAssertEqual(nodes.count, 1) XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.format.variable, "\"HH:mm\"") XCTAssertEqual(node.format.variable, "\"HH:mm\"")
@@ -231,13 +179,6 @@ class NowNodeTests: NodeTests {
formatter.dateFormat = "yyyy-MM-dd" formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.stringFromDate(NSDate()) let date = formatter.stringFromDate(NSDate())
switch node.render(context) { XCTAssertEqual(try? node.render(context), date)
case .Success(let string):
XCTAssertEqual(string, date)
case .Error:
XCTAssert(false, "Unexpected error")
} }
}
} }

View File

@@ -8,7 +8,7 @@ class TokenParserTests: XCTestCase {
Token.Text(value: "Hello World") Token.Text(value: "Hello World")
]) ])
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! TextNode let node = nodes.first as! TextNode
XCTAssertEqual(nodes.count, 1) XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.text, "Hello World") XCTAssertEqual(node.text, "Hello World")
@@ -20,7 +20,7 @@ class TokenParserTests: XCTestCase {
Token.Variable(value: "name") Token.Variable(value: "name")
]) ])
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! VariableNode let node = nodes.first as! VariableNode
XCTAssertEqual(nodes.count, 1) XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.variable, Variable("name")) XCTAssertEqual(node.variable, Variable("name"))
@@ -32,7 +32,7 @@ class TokenParserTests: XCTestCase {
Token.Comment(value: "Secret stuff!") Token.Comment(value: "Secret stuff!")
]) ])
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
XCTAssertEqual(nodes.count, 0) XCTAssertEqual(nodes.count, 0)
} }
} }
@@ -42,7 +42,7 @@ class TokenParserTests: XCTestCase {
Token.Block(value: "now"), Token.Block(value: "now"),
]) ])
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
XCTAssertEqual(nodes.count, 1) XCTAssertEqual(nodes.count, 1)
} }
} }

View File

@@ -2,27 +2,27 @@ import Foundation
import XCTest import XCTest
import Stencil import Stencil
func assertSuccess(result:TokenParser.Results, block:(([Node]) -> ())) { func assertSuccess<T>(@autoclosure closure:() throws -> (T), block:(T -> ())) {
switch result { do {
case .Success(let nodes): block(try closure())
block(nodes) } catch {
case .Error: XCTFail("Unexpected error \(error)")
XCTAssert(false, "Unexpected error")
} }
} }
func assertFailure(result:TokenParser.Results, description:String) { func assertFailure<T, U : Equatable>(@autoclosure closure:() throws -> (T), _ error:U) {
switch result { do {
case .Success: try closure()
XCTAssert(false, "Unexpected error") } catch let e as U {
case .Error(let error): XCTAssertEqual(e, error)
XCTAssertEqual("\(error)", description) } catch {
XCTFail()
} }
} }
class CustomNode : Node { class CustomNode : NodeType {
func render(context:Context) -> Result { func render(context:Context) throws -> String {
return .Success("Hello World") return "Hello World"
} }
} }
@@ -42,7 +42,7 @@ class StencilTests: XCTestCase {
]) ])
let template = Template(templateString:templateString) let template = Template(templateString:templateString)
let result = template.render(context) let result = try? template.render(context)
let fixture = "There are 2 articles.\n" + let fixture = "There are 2 articles.\n" +
"\n" + "\n" +
@@ -50,7 +50,7 @@ class StencilTests: XCTestCase {
" - Memory Management with ARC by Kyle Fuller.\n" + " - Memory Management with ARC by Kyle Fuller.\n" +
"\n" "\n"
XCTAssertEqual(result, Result.Success(fixture)) XCTAssertEqual(result, fixture)
} }
func testCustomTag() { func testCustomTag() {
@@ -58,11 +58,10 @@ class StencilTests: XCTestCase {
let template = Template(templateString:templateString) let template = Template(templateString:templateString)
template.parser.registerTag("custom") { parser, token in template.parser.registerTag("custom") { parser, token in
return .Success(node:CustomNode()) return CustomNode()
} }
let result = template.render() XCTAssertEqual(try? template.render(), "Hello World")
XCTAssertEqual(result, Result.Success("Hello World"))
} }
func testSimpleCustomTag() { func testSimpleCustomTag() {
@@ -70,10 +69,9 @@ class StencilTests: XCTestCase {
let template = Template(templateString:templateString) let template = Template(templateString:templateString)
template.parser.registerSimpleTag("custom") { context in template.parser.registerSimpleTag("custom") { context in
return .Success("Hello World") return "Hello World"
} }
let result = template.render() XCTAssertEqual(try? template.render(), "Hello World")
XCTAssertEqual(result, Result.Success("Hello World"))
} }
} }

View File

@@ -20,14 +20,14 @@ class IncludeTests: NodeTests {
let tokens = [ Token.Block(value: "include") ] let tokens = [ Token.Block(value: "include") ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertFailure(parser.parse(), description: "include: Tag takes one argument, the template file to be included") assertFailure(try parser.parse(), TemplateSyntaxError("'include' tag takes one argument, the template file to be included"))
} }
func testParse() { func testParse() {
let tokens = [ Token.Block(value: "include \"test.html\"") ] let tokens = [ Token.Block(value: "include \"test.html\"") ]
let parser = TokenParser(tokens: tokens) let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in assertSuccess(try parser.parse()) { nodes in
let node = nodes.first as! IncludeNode let node = nodes.first as! IncludeNode
XCTAssertEqual(nodes.count, 1) XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.templateName, "test.html") XCTAssertEqual(node.templateName, "test.html")
@@ -38,38 +38,27 @@ class IncludeTests: NodeTests {
func testRenderWithoutLoader() { func testRenderWithoutLoader() {
let node = IncludeNode(templateName: "test.html") let node = IncludeNode(templateName: "test.html")
let result = node.render(Context())
switch result { do {
case .Success: try node.render(Context())
XCTAssert(false, "Unexpected error") } catch {
case .Error(let error):
XCTAssertEqual("\(error)", "Template loader not in context") XCTAssertEqual("\(error)", "Template loader not in context")
} }
} }
func testRenderWithoutTemplateNamed() { func testRenderWithoutTemplateNamed() {
let node = IncludeNode(templateName: "unknown.html") let node = IncludeNode(templateName: "unknown.html")
let result = node.render(Context(dictionary:["loader":loader]))
switch result { do {
case .Success: try node.render(Context(dictionary:["loader":loader]))
XCTAssert(false, "Unexpected error") } catch {
case .Error(let error): XCTAssertTrue("\(error)".hasPrefix("'unknown.html' template not found"))
XCTAssertTrue("\(error)".hasPrefix("Template 'unknown.html' not found"))
} }
} }
func testRender() { func testRender() {
let node = IncludeNode(templateName: "test.html") let node = IncludeNode(templateName: "test.html")
let result = node.render(Context(dictionary:["loader":loader, "target": "World"])) let value = try? node.render(Context(dictionary:["loader":loader, "target": "World"]))
XCTAssertEqual(value, "Hello World!")
switch result {
case .Success(let string):
XCTAssertEqual(string, "Hello World!")
case .Error(let error):
XCTAssert(false, "Unexpected error: \(error)")
} }
}
} }

View File

@@ -16,14 +16,7 @@ class InheritenceTests: NodeTests {
func testInheritence() { func testInheritence() {
context = Context(dictionary: ["loader": loader]) context = Context(dictionary: ["loader": loader])
let template = loader.loadTemplate("child.html")! let template = loader.loadTemplate("child.html")!
let result = template.render(context) XCTAssertEqual(try? template.render(context), "Header\nChild")
switch result {
case .Success(let rendered):
XCTAssertEqual(rendered, "Header\nChild")
case .Error:
XCTAssert(false, "Unexpected error")
}
} }
} }

View File

@@ -3,12 +3,10 @@ import XCTest
import Stencil import Stencil
class TemplateTests: XCTestCase { class TemplateTests: XCTestCase {
func testTemplate() { func testTemplate() {
let context = Context(dictionary: [ "name": "Kyle" ]) let context = Context(dictionary: [ "name": "Kyle" ])
let template = Template(templateString: "Hello World") let template = Template(templateString: "Hello World")
let result = template.render(context) let result = try? template.render(context)
XCTAssertEqual(result, Result.Success("Hello World")) XCTAssertEqual(result, "Hello World")
} }
} }