From 9c335caeb6fc8e9c309fa585a10753b698a41e49 Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Fri, 25 Sep 2015 12:53:45 -0700 Subject: [PATCH] Remove custom Result type and throw errors --- ARCHITECTURE.md | 17 +- README.md | 26 +-- Stencil.xcodeproj/project.pbxproj | 4 - Stencil/Node.swift | 213 ++++++------------ Stencil/Parser.swift | 44 +--- Stencil/Result.swift | 25 -- Stencil/Template.swift | 27 +-- Stencil/TemplateLoader.swift | 8 - Stencil/TemplateLoader/Include.swift | 25 +- Stencil/TemplateLoader/Inheritence.swift | 80 +++---- Stencil/Tokenizer.swift | 3 - StencilTests/NodeTests.swift | 99 ++------ StencilTests/ParserTests.swift | 8 +- StencilTests/StencilTests.swift | 44 ++-- .../TemplateLoader/IncludeTests.swift | 33 +-- .../TemplateLoader/InheritenceTests.swift | 9 +- StencilTests/TemplateTests.swift | 6 +- 17 files changed, 211 insertions(+), 460 deletions(-) delete mode 100644 Stencil/Result.swift diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 23eec31..60f22e3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -106,7 +106,7 @@ Here’s an example. Registering a template tag called `custom` which just rende ```swift parser.registerSimpleTag("custom") { context in - return .Success("Hello World") + return "Hello World" } ``` @@ -144,7 +144,7 @@ class DebugNode : Node { self.nodes = nodes } - func render(context: Context) -> Result { + func render(context: Context) throws -> String { // Is there a debug variable inside the context? if let debug = context["debug"] as? Bool { // Is debug set to true? @@ -155,7 +155,7 @@ class DebugNode : Node { } // 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. ```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. - switch parser.parse(until(["enddebug"])) - case .Success(let 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) - } + let nodes = try until(["enddebug"])) + return DebugNode(nodes) } ``` diff --git a/README.md b/README.md index 5689512..efca766 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,18 @@ There are {{ articles.count }} articles. ```swift let context = Context(dictionary: [ - "articles": [ - [ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ], - [ "title": "Memory Management with ARC", "author": "Kyle Fuller" ], - ] + "articles": [ + [ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ], + [ "title": "Memory Management with ARC", "author": "Kyle Fuller" ], + ] ]) -let template = try? Template(named: "template.stencil") -let result = template!.render(context) - -switch result { - case .Error(let error): - println("There was an error rendering your template (\(error)).") - case .Success(let string): - println("\(string)") +do { + let template = Template(named: "template.stencil") + let rendered = template.render(context) + print(rendered) +} catch { + print("Failed to render template \(error)") } ``` @@ -157,13 +155,13 @@ you to write your own custom tags. The following is the simplest form: ```swift 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 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 of template tags. You will need to call the `registerTag` API which accepts a diff --git a/Stencil.xcodeproj/project.pbxproj b/Stencil.xcodeproj/project.pbxproj index df8e288..218a903 100644 --- a/Stencil.xcodeproj/project.pbxproj +++ b/Stencil.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; }; 27CE0B011A50CBD1004A105B /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B001A50CBD1004A105B /* Include.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 */; }; 7725B3CD19F92B61002CF74B /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CC19F92B61002CF74B /* Variable.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 = ""; }; 27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = ""; }; 27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.swift; sourceTree = ""; }; - 71CE4C0919FD29D000B9E0C5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 7725B3CA19F92B4F002CF74B /* VariableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = ""; }; 7725B3CC19F92B61002CF74B /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = ""; }; 7725B3CE19F94214002CF74B /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = ""; }; @@ -154,7 +152,6 @@ 77EB082A19FA8600001870F1 /* Lexer.swift */, 7725B3D419F9438F002CF74B /* Node.swift */, 7725B3D619F94A43002CF74B /* Parser.swift */, - 71CE4C0919FD29D000B9E0C5 /* Result.swift */, 77EB082419F96E88001870F1 /* Template.swift */, 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */, 7725B3CE19F94214002CF74B /* Tokenizer.swift */, @@ -422,7 +419,6 @@ 7725B3D719F94A43002CF74B /* Parser.swift in Sources */, 77EB082519F96E88001870F1 /* Template.swift in Sources */, 7725B3CD19F92B61002CF74B /* Variable.swift in Sources */, - 71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */, 7725B3D519F9438F002CF74B /* Node.swift in Sources */, 27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */, 27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */, diff --git a/Stencil/Node.swift b/Stencil/Node.swift index 7ccbb4d..68cd694 100644 --- a/Stencil/Node.swift +++ b/Stencil/Node.swift @@ -1,83 +1,52 @@ import Foundation -struct NodeError : Error { - let token:Token - let message:String +public struct TemplateSyntaxError : ErrorType, Equatable, CustomStringConvertible { + public let description:String - init(token:Token, message:String) { - self.token = token - self.message = message - } - - var description:String { - return "\(token.components().first!): \(message)" + public init(_ description:String) { + self.description = description } } -public protocol Node { - /// Return the node rendered as a string, or returns a failure - func render(context:Context) -> Result +public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool { + return lhs.description == rhs.description } -extension Array { - func map(block:((Element) -> (U?, Error?))) -> ([U]?, Error?) { - var results = [U]() - - 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 protocol NodeType { + /// Render the node in the given context + func render(context:Context) throws -> String } -public func renderNodes(nodes:[Node], context:Context) -> Result { - var result = "" - - for item in nodes { - switch item.render(context) { - case .Success(let string): - result += string - case .Error(let error): - return .Error(error) - } - } - - return .Success(result) +/// Render the collection of nodes in the given context +public func renderNodes(nodes:[NodeType], _ context:Context) throws -> String { + return try nodes.map { try $0.render(context) }.joinWithSeparator("") } -public class SimpleNode : Node { - let handler:(Context) -> (Result) +public class SimpleNode : NodeType { + let handler:Context throws -> String - public init(handler:((Context) -> (Result))) { + public init(handler:Context throws -> String) { self.handler = handler } - public func render(context:Context) -> Result { - return handler(context) + public func render(context: Context) throws -> String { + return try handler(context) } } -public class TextNode : Node { +public class TextNode : NodeType { public let text:String public init(text:String) { self.text = text } - public func render(context:Context) -> Result { - return .Success(self.text) + public func render(context:Context) throws -> String { + return self.text } } -public class VariableNode : Node { +public class VariableNode : NodeType { public let variable:Variable public init(variable:Variable) { @@ -88,23 +57,23 @@ public class VariableNode : Node { self.variable = Variable(variable) } - public func render(context:Context) -> Result { + public func render(context:Context) throws -> String { let result:AnyObject? = variable.resolve(context) if let result = result as? String { - return .Success(result) + return result } 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 class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { + public class func parse(parser:TokenParser, token:Token) -> NodeType { var format:Variable? let components = token.components() @@ -112,7 +81,7 @@ public class NowNode : Node { format = Variable(components[1]) } - return .Success(node:NowNode(format:format)) + return NowNode(format:format) } 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 format: AnyObject? = self.format.resolve(context) var formatter:NSDateFormatter? @@ -134,156 +103,115 @@ public class NowNode : Node { formatter = NSDateFormatter() formatter!.dateFormat = format } 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 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() if components.count == 4 && components[2] == "in" { let loopVariable = components[1] let variable = components[3] - var forNodes:[Node]! - var emptyNodes = [Node]() + var emptyNodes = [NodeType]() - switch parser.parse(until(["endfor", "empty"])) { - case .Success(let nodes): - forNodes = nodes - case .Error(let error): - return .Error(error: error) - } + let forNodes = try parser.parse(until(["endfor", "empty"])) if let token = parser.nextToken() { if token.contents == "empty" { - switch parser.parse(until(["endfor"])) { - case .Success(let nodes): - emptyNodes = nodes - case .Error(let error): - return .Error(error: error) - } - + emptyNodes = try parser.parse(until(["endfor"])) parser.nextToken() } } 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.loopVariable = loopVariable self.nodes = nodes } - public func render(context: Context) -> Result { - let values = variable.resolve(context) as? [AnyObject] - var output = "" - - if let values = values { - for item in values { + public func render(context: Context) throws -> String { + if let values = variable.resolve(context) as? [AnyObject] { + return try values.map { item in context.push() context[loopVariable] = item - let result = renderNodes(nodes, context: context) + let result = try renderNodes(nodes, context) context.pop() - - switch result { - case .Success(let string): - output += string - case .Error(let error): - return .Error(error) - } - } + return result + }.joinWithSeparator("") } - return .Success(output) + return "" } } -public class IfNode : Node { +public class IfNode : NodeType { public let variable:Variable - public let trueNodes:[Node] - public let falseNodes:[Node] + public let trueNodes:[NodeType] + 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] - var trueNodes = [Node]() - var falseNodes = [Node]() + var trueNodes = [NodeType]() + var falseNodes = [NodeType]() - switch parser.parse(until(["endif", "else"])) { - case .Success(let nodes): - trueNodes = nodes - case .Error(let error): - return .Error(error: error) - } + trueNodes = try parser.parse(until(["endif", "else"])) if let token = parser.nextToken() { if token.contents == "else" { - switch parser.parse(until(["endif"])) { - case .Success(let nodes): - falseNodes = nodes - case .Error(let error): - return .Error(error: error) - } + falseNodes = try parser.parse(until(["endif"])) parser.nextToken() } } 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] - var trueNodes = [Node]() - var falseNodes = [Node]() + var trueNodes = [NodeType]() + var falseNodes = [NodeType]() - switch parser.parse(until(["endif", "else"])) { - case .Success(let nodes): - falseNodes = nodes - case .Error(let error): - return .Error(error: error) - } + falseNodes = try parser.parse(until(["endif", "else"])) if let token = parser.nextToken() { if token.contents == "else" { - switch parser.parse(until(["endif"])) { - case .Success(let nodes): - trueNodes = nodes - case .Error(let error): - return .Error(error: error) - } + trueNodes = try parser.parse(until(["endif"])) parser.nextToken() } } 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.trueNodes = trueNodes self.falseNodes = falseNodes } - public func render(context: Context) -> Result { + public func render(context: Context) throws -> String { let result: AnyObject? = variable.resolve(context) var truthy = false @@ -296,7 +224,12 @@ public class IfNode : Node { } 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() return output diff --git a/Stencil/Parser.swift b/Stencil/Parser.swift index b8a0717..b5e1e11 100644 --- a/Stencil/Parser.swift +++ b/Stencil/Parser.swift @@ -1,5 +1,3 @@ -import Foundation - public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool { if let name = token.components().first { 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 public class TokenParser { - public typealias TagParser = (TokenParser, Token) -> Result - 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) - } + public typealias TagParser = (TokenParser, Token) throws -> NodeType private var tokens:[Token] private var tags = [String:TagParser]() @@ -47,19 +34,19 @@ public class TokenParser { } /// Registers a simple template tag with a name and a handler - public func registerSimpleTag(name:String, handler:((Context) -> (Stencil.Result))) { - registerTag(name, parser: { (parser, token) -> TokenParser.Result in - return .Success(node:SimpleNode(handler: handler)) + public func registerSimpleTag(name:String, handler:(Context throws -> String)) { + registerTag(name, parser: { parser, token in + return SimpleNode(handler: handler) }) } /// Parse the given tokens into nodes - public func parse() -> Results { - return parse(nil) + public func parse() throws -> [NodeType] { + return try parse(nil) } - public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) -> TokenParser.Results { - var nodes = NodeList() + public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) throws -> [NodeType] { + var nodes = [NodeType]() while tokens.count > 0 { let token = nextToken()! @@ -75,26 +62,19 @@ public class TokenParser { if let parse_until = parse_until { if parse_until(parser: self, token: token) { prependToken(token) - return .Success(nodes:nodes) + return nodes } } - if let tag = tag { - if let parser = self.tags[tag] { - switch parser(self, token) { - case .Success(let node): - nodes.append(node) - case .Error(let error): - return .Error(error:error) - } - } + if let tag = tag, let parser = self.tags[tag] { + nodes.append(try parser(self, token)) } case .Comment: continue } } - return .Success(nodes:nodes) + return nodes } public func nextToken() -> Token? { diff --git a/Stencil/Result.swift b/Stencil/Result.swift deleted file mode 100644 index cbde22c..0000000 --- a/Stencil/Result.swift +++ /dev/null @@ -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 - } -} diff --git a/Stencil/Template.swift b/Stencil/Template.swift index ed78845..0e968f3 100644 --- a/Stencil/Template.swift +++ b/Stencil/Template.swift @@ -12,15 +12,15 @@ public class Template { /// Create a template with the given name inside the given bundle public convenience init(named:String, inBundle bundle:NSBundle?) throws { - var url:NSURL? + let url:NSURL if let bundle = bundle { - url = bundle.URLForResource(named, withExtension: nil) + url = bundle.URLForResource(named, withExtension: nil)! } 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 @@ -40,20 +40,9 @@ public class Template { parser = TokenParser(tokens: tokens) } - /// Render the given template in a context - public func render(context:Context) -> Result { - switch parser.parse() { - case .Success(let nodes): - 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) + /// Render the given template + public func render(context:Context? = nil) throws -> String { + let nodes = try parser.parse() + return try renderNodes(nodes, context ?? Context()) } } diff --git a/Stencil/TemplateLoader.swift b/Stencil/TemplateLoader.swift index a05ed52..33c84a7 100644 --- a/Stencil/TemplateLoader.swift +++ b/Stencil/TemplateLoader.swift @@ -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 PathKit diff --git a/Stencil/TemplateLoader/Include.swift b/Stencil/TemplateLoader/Include.swift index fb87038..76c1b7e 100644 --- a/Stencil/TemplateLoader/Include.swift +++ b/Stencil/TemplateLoader/Include.swift @@ -1,44 +1,37 @@ import Foundation import PathKit -extension String : Error { - public var description:String { - return self - } -} -public class IncludeNode : Node { +public class IncludeNode : NodeType { 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("\"") 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) { 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 template = loader.loadTemplate(templateName) { - return template.render(context) + return try template.render(context) } let paths:String = loader.paths.map { path in return path.description - }.joinWithSeparator(", ") - let error = "Template '\(templateName)' not found in \(paths)" - return .Error(error) + }.joinWithSeparator(", ") + throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)") } - let error = "Template loader not in context" - return .Error(error) + throw TemplateSyntaxError("Template loader not in context") } } diff --git a/Stencil/TemplateLoader/Inheritence.swift b/Stencil/TemplateLoader/Inheritence.swift index a702fbb..1edc4c6 100644 --- a/Stencil/TemplateLoader/Inheritence.swift +++ b/Stencil/TemplateLoader/Inheritence.swift @@ -24,36 +24,32 @@ func any(elements:[Element], closure:(Element -> Bool)) -> Element? { return nil } -class ExtendsNode : Node { +class ExtendsNode : NodeType { let templateName:String 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("\"") 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() { - case .Success(let nodes): - if (any(nodes) { ($0 as? ExtendsNode) != nil }) != nil { - return .Error(error:"'extends' cannot appear more than once in the same template") - } - - let blockNodes = nodes.filter { node in node is BlockNode } - - let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:Node) -> [String:BlockNode] in - let node = (node as! BlockNode) - var dict = accumulator - dict[node.name] = node - return dict - }) - - return .Success(node:ExtendsNode(templateName: bits[1], blocks: nodes)) - case .Error(let error): - return .Error(error:error) + let parsedNodes = try parser.parse() + if (any(parsedNodes) { ($0 as? ExtendsNode) != nil }) != nil { + throw TemplateSyntaxError("'extends' cannot appear more than once in the same template") } + + let blockNodes = parsedNodes.filter { node in node is BlockNode } + + let nodes = blockNodes.reduce([String:BlockNode](), combine: { (accumulator, node:NodeType) -> [String:BlockNode] in + let node = (node as! BlockNode) + var dict = accumulator + dict[node.name] = node + return dict + }) + + return ExtendsNode(templateName: bits[1], blocks: nodes) } init(templateName:String, blocks:[String:BlockNode]) { @@ -61,12 +57,12 @@ class ExtendsNode : Node { self.blocks = blocks } - func render(context: Context) -> Result { + func render(context: Context) throws -> String { if let loader = context["loader"] as? TemplateLoader { if let template = loader.loadTemplate(templateName) { let blockContext = BlockContext(blocks: blocks) context.push([BlockContext.contextKey: blockContext]) - let result = template.render(context) + let result = try template.render(context) context.pop() return result } @@ -74,51 +70,39 @@ class ExtendsNode : Node { let paths:String = loader.paths.map { path in return path.description }.joinWithSeparator(", ") - let error = "Template '\(templateName)' not found in \(paths)" - return .Error(error) + throw TemplateSyntaxError("'\(templateName)' template not found in \(paths)") } - let error = "Template loader not in context" - return .Error(error) + throw TemplateSyntaxError("Template loader not in context") } } -class BlockNode : Node { +class BlockNode : NodeType { 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() 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] - var nodes = [Node]() - - 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)) + let nodes = try parser.parse(until(["endblock"])) + return BlockNode(name:blockName, nodes:nodes) } - init(name:String, nodes:[Node]) { + init(name:String, nodes:[NodeType]) { self.name = name self.nodes = nodes } - func render(context: Context) -> Result { - if let blockContext = context[BlockContext.contextKey] as? BlockContext { - if let node = blockContext.pop(name) { - return node.render(context) - } + func render(context: Context) throws -> String { + if let blockContext = context[BlockContext.contextKey] as? BlockContext, node = blockContext.pop(name) { + return try node.render(context) } - return renderNodes(nodes, context: context) + return try renderNodes(nodes, context) } } diff --git a/Stencil/Tokenizer.swift b/Stencil/Tokenizer.swift index 9545bc1..49ebda0 100644 --- a/Stencil/Tokenizer.swift +++ b/Stencil/Tokenizer.swift @@ -1,6 +1,3 @@ -import Foundation - - public enum Token : Equatable { /// A token representing a piece of text. case Text(value:String) diff --git a/StencilTests/NodeTests.swift b/StencilTests/NodeTests.swift index a36ba2a..224ccec 100644 --- a/StencilTests/NodeTests.swift +++ b/StencilTests/NodeTests.swift @@ -2,16 +2,10 @@ import Foundation import XCTest import Stencil -class ErrorNodeError : Error { - var description: String { - return "Node Error" - } -} -class ErrorNode : Node { - func render(context: Context) -> Result { - - return .Error(ErrorNodeError()) +class ErrorNode : NodeType { + func render(context: Context) throws -> String { + throw TemplateSyntaxError("Custom Error") } } @@ -30,73 +24,39 @@ class NodeTests: XCTestCase { class TextNodeTests: NodeTests { func testTextNodeResolvesText() { let node = TextNode(text:"Hello World") - let _ = node.render(context) - - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, "Hello World") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), "Hello World") } } class VariableNodeTests: NodeTests { func testVariableNodeResolvesVariable() { let node = VariableNode(variable:Variable("name")) - - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, "Kyle") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), "Kyle") } func testVariableNodeResolvesNonStringVariable() { let node = VariableNode(variable:Variable("age")) - - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, "27") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), "27") } } class RenderNodeTests: NodeTests { func testRenderingNodes() { - let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [Node] - switch renderNodes(nodes, context: context) { - case .Success(let result): - XCTAssertEqual(result, "Hello Kyle") - case .Error: - XCTAssert(false, "Unexpected error") - } + let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [NodeType] + XCTAssertEqual(try? renderNodes(nodes, context), "Hello Kyle") } 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) { - case .Success: - XCTAssert(false, "Unexpected success") - case .Error(let error): - XCTAssertEqual("\(error)", "Node Error") - } + assertFailure(try renderNodes(nodes, context), TemplateSyntaxError("Custom Error")) } } class ForNodeTests: NodeTests { func testForNodeRender() { let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[]) - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, "123") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), "123") } } @@ -114,7 +74,7 @@ class IfNodeTests: NodeTests { ] let parser = TokenParser(tokens: tokens) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! IfNode let trueNode = node.trueNodes.first as! TextNode let falseNode = node.falseNodes.first as! TextNode @@ -138,7 +98,7 @@ class IfNodeTests: NodeTests { ] let parser = TokenParser(tokens: tokens) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! IfNode let trueNode = node.trueNodes.first as! TextNode let falseNode = node.falseNodes.first as! TextNode @@ -158,7 +118,7 @@ class IfNodeTests: NodeTests { ] 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() { @@ -167,31 +127,19 @@ class IfNodeTests: NodeTests { ] 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 func testIfNodeRenderTruth() { let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")]) - - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, "true") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), "true") } func testIfNodeRenderFalse() { let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")]) - - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, "false") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), "false") } } @@ -204,7 +152,7 @@ class NowNodeTests: NodeTests { let tokens = [ Token.Block(value: "now") ] let parser = TokenParser(tokens: tokens) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! NowNode XCTAssertEqual(nodes.count, 1) 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 parser = TokenParser(tokens: tokens) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! NowNode XCTAssertEqual(nodes.count, 1) XCTAssertEqual(node.format.variable, "\"HH:mm\"") @@ -231,13 +179,6 @@ class NowNodeTests: NodeTests { formatter.dateFormat = "yyyy-MM-dd" let date = formatter.stringFromDate(NSDate()) - switch node.render(context) { - case .Success(let string): - XCTAssertEqual(string, date) - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? node.render(context), date) } - } - diff --git a/StencilTests/ParserTests.swift b/StencilTests/ParserTests.swift index 28b07fa..c8f15a9 100644 --- a/StencilTests/ParserTests.swift +++ b/StencilTests/ParserTests.swift @@ -8,7 +8,7 @@ class TokenParserTests: XCTestCase { Token.Text(value: "Hello World") ]) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! TextNode XCTAssertEqual(nodes.count, 1) XCTAssertEqual(node.text, "Hello World") @@ -20,7 +20,7 @@ class TokenParserTests: XCTestCase { Token.Variable(value: "name") ]) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! VariableNode XCTAssertEqual(nodes.count, 1) XCTAssertEqual(node.variable, Variable("name")) @@ -32,7 +32,7 @@ class TokenParserTests: XCTestCase { Token.Comment(value: "Secret stuff!") ]) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in XCTAssertEqual(nodes.count, 0) } } @@ -42,7 +42,7 @@ class TokenParserTests: XCTestCase { Token.Block(value: "now"), ]) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in XCTAssertEqual(nodes.count, 1) } } diff --git a/StencilTests/StencilTests.swift b/StencilTests/StencilTests.swift index ceb681b..3d77826 100644 --- a/StencilTests/StencilTests.swift +++ b/StencilTests/StencilTests.swift @@ -2,27 +2,27 @@ import Foundation import XCTest import Stencil -func assertSuccess(result:TokenParser.Results, block:(([Node]) -> ())) { - switch result { - case .Success(let nodes): - block(nodes) - case .Error: - XCTAssert(false, "Unexpected error") +func assertSuccess(@autoclosure closure:() throws -> (T), block:(T -> ())) { + do { + block(try closure()) + } catch { + XCTFail("Unexpected error \(error)") } } -func assertFailure(result:TokenParser.Results, description:String) { - switch result { - case .Success: - XCTAssert(false, "Unexpected error") - case .Error(let error): - XCTAssertEqual("\(error)", description) +func assertFailure(@autoclosure closure:() throws -> (T), _ error:U) { + do { + try closure() + } catch let e as U { + XCTAssertEqual(e, error) + } catch { + XCTFail() } } -class CustomNode : Node { - func render(context:Context) -> Result { - return .Success("Hello World") +class CustomNode : NodeType { + func render(context:Context) throws -> String { + return "Hello World" } } @@ -42,7 +42,7 @@ class StencilTests: XCTestCase { ]) let template = Template(templateString:templateString) - let result = template.render(context) + let result = try? template.render(context) let fixture = "There are 2 articles.\n" + "\n" + @@ -50,7 +50,7 @@ class StencilTests: XCTestCase { " - Memory Management with ARC by Kyle Fuller.\n" + "\n" - XCTAssertEqual(result, Result.Success(fixture)) + XCTAssertEqual(result, fixture) } func testCustomTag() { @@ -58,11 +58,10 @@ class StencilTests: XCTestCase { let template = Template(templateString:templateString) template.parser.registerTag("custom") { parser, token in - return .Success(node:CustomNode()) + return CustomNode() } - let result = template.render() - XCTAssertEqual(result, Result.Success("Hello World")) + XCTAssertEqual(try? template.render(), "Hello World") } func testSimpleCustomTag() { @@ -70,10 +69,9 @@ class StencilTests: XCTestCase { let template = Template(templateString:templateString) template.parser.registerSimpleTag("custom") { context in - return .Success("Hello World") + return "Hello World" } - let result = template.render() - XCTAssertEqual(result, Result.Success("Hello World")) + XCTAssertEqual(try? template.render(), "Hello World") } } diff --git a/StencilTests/TemplateLoader/IncludeTests.swift b/StencilTests/TemplateLoader/IncludeTests.swift index 18fe766..3e84388 100644 --- a/StencilTests/TemplateLoader/IncludeTests.swift +++ b/StencilTests/TemplateLoader/IncludeTests.swift @@ -20,14 +20,14 @@ class IncludeTests: NodeTests { let tokens = [ Token.Block(value: "include") ] 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() { let tokens = [ Token.Block(value: "include \"test.html\"") ] let parser = TokenParser(tokens: tokens) - assertSuccess(parser.parse()) { nodes in + assertSuccess(try parser.parse()) { nodes in let node = nodes.first as! IncludeNode XCTAssertEqual(nodes.count, 1) XCTAssertEqual(node.templateName, "test.html") @@ -38,38 +38,27 @@ class IncludeTests: NodeTests { func testRenderWithoutLoader() { let node = IncludeNode(templateName: "test.html") - let result = node.render(Context()) - switch result { - case .Success: - XCTAssert(false, "Unexpected error") - case .Error(let error): + do { + try node.render(Context()) + } catch { XCTAssertEqual("\(error)", "Template loader not in context") } } func testRenderWithoutTemplateNamed() { let node = IncludeNode(templateName: "unknown.html") - let result = node.render(Context(dictionary:["loader":loader])) - switch result { - case .Success: - XCTAssert(false, "Unexpected error") - case .Error(let error): - XCTAssertTrue("\(error)".hasPrefix("Template 'unknown.html' not found")) + do { + try node.render(Context(dictionary:["loader":loader])) + } catch { + XCTAssertTrue("\(error)".hasPrefix("'unknown.html' template not found")) } } func testRender() { let node = IncludeNode(templateName: "test.html") - let result = node.render(Context(dictionary:["loader":loader, "target": "World"])) - - switch result { - case .Success(let string): - XCTAssertEqual(string, "Hello World!") - case .Error(let error): - XCTAssert(false, "Unexpected error: \(error)") - } + let value = try? node.render(Context(dictionary:["loader":loader, "target": "World"])) + XCTAssertEqual(value, "Hello World!") } - } diff --git a/StencilTests/TemplateLoader/InheritenceTests.swift b/StencilTests/TemplateLoader/InheritenceTests.swift index 5aaba4c..306b632 100644 --- a/StencilTests/TemplateLoader/InheritenceTests.swift +++ b/StencilTests/TemplateLoader/InheritenceTests.swift @@ -16,14 +16,7 @@ class InheritenceTests: NodeTests { func testInheritence() { context = Context(dictionary: ["loader": loader]) let template = loader.loadTemplate("child.html")! - let result = template.render(context) - - switch result { - case .Success(let rendered): - XCTAssertEqual(rendered, "Header\nChild") - case .Error: - XCTAssert(false, "Unexpected error") - } + XCTAssertEqual(try? template.render(context), "Header\nChild") } } diff --git a/StencilTests/TemplateTests.swift b/StencilTests/TemplateTests.swift index 8a833fe..93aeaba 100644 --- a/StencilTests/TemplateTests.swift +++ b/StencilTests/TemplateTests.swift @@ -3,12 +3,10 @@ import XCTest import Stencil class TemplateTests: XCTestCase { - func testTemplate() { let context = Context(dictionary: [ "name": "Kyle" ]) let template = Template(templateString: "Hello World") - let result = template.render(context) - XCTAssertEqual(result, Result.Success("Hello World")) + let result = try? template.render(context) + XCTAssertEqual(result, "Hello World") } - }