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
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)
}
```

View File

@@ -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

View File

@@ -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 = "<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>"; };
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>"; };
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>"; };
@@ -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 */,

View File

@@ -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<U>(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

View File

@@ -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? {

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
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())
}
}

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 PathKit

View File

@@ -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")
}
}

View File

@@ -24,36 +24,32 @@ func any<Element>(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)
}
}

View File

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

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<T>(@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<T, U : Equatable>(@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")
}
}

View File

@@ -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!")
}
}

View File

@@ -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")
}
}

View File

@@ -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")
}
}