diff --git a/Sources/MapNode.swift b/Sources/MapNode.swift index a909713f..3002bd47 100644 --- a/Sources/MapNode.swift +++ b/Sources/MapNode.swift @@ -7,74 +7,81 @@ import Stencil -public class MapNode: NodeType { - let variable: Variable - let resultName: String - let mapVariable: String? - let nodes: [NodeType] - - public class func parse(parser: TokenParser, token: Token) throws -> NodeType { - let components = token.components() - - guard components.count == 4 && components[2] == "into" || - components.count == 6 && components[2] == "into" && components[4] == "using" else { - let error = "'map' statements should use the following " + - "'map {array} into {varname} [using {element}]' `\(token.contents)`." - throw TemplateSyntaxError(error) - } - - let variable = components[1] - let resultName = components[3] - var mapVariable: String? = nil - if components.count > 4 { - mapVariable = components[5] - } - - let mapNodes = try parser.parse(until(["endmap", "empty"])) - - guard let token = parser.nextToken() else { - throw TemplateSyntaxError("`endmap` was not found.") - } - - if token.contents == "empty" { - _ = parser.nextToken() - } - - return MapNode(variable: variable, resultName: resultName, mapVariable: mapVariable, nodes: mapNodes) - } - - public init(variable: String, resultName: String, mapVariable: String?, nodes: [NodeType]) { - self.variable = Variable(variable) - self.resultName = resultName - self.mapVariable = mapVariable - self.nodes = nodes - } +class MapNode: NodeType { + let variable: Variable + let resultName: String + let mapVariable: String? + let nodes: [NodeType] + + class func parse(parser: TokenParser, token: Token) throws -> NodeType { + let components = token.components() + + guard components.count == 4 && components[2] == "into" || + components.count == 6 && components[2] == "into" && components[4] == "using" else { + let error = "'map' statements should use the following " + + "'map {array} into {varname} [using {element}]' `\(token.contents)`." + throw TemplateSyntaxError(error) + } + + let variable = components[1] + let resultName = components[3] + var mapVariable: String? = nil + if components.count > 4 { + mapVariable = components[5] + } + + let mapNodes = try parser.parse(until(["endmap", "empty"])) + + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endmap` was not found.") + } + + if token.contents == "empty" { + _ = parser.nextToken() + } + + return MapNode(variable: variable, resultName: resultName, mapVariable: mapVariable, nodes: mapNodes) + } + + init(variable: String, resultName: String, mapVariable: String?, nodes: [NodeType]) { + self.variable = Variable(variable) + self.resultName = resultName + self.mapVariable = mapVariable + self.nodes = nodes + } + + func render(_ context: Context) throws -> String { + let values = try variable.resolve(context) + + if let values = values as? [Any], values.count > 0 { + let mappedValues: [String] = try values.enumerated().map { (index, item) in + let mapContext = self.context(values: values, index: index, item: item) + + return try context.push(dictionary: mapContext) { + try renderNodes(nodes, context) + } + } + context[resultName] = mappedValues + } + + // Map should never render anything + return "" + } - public func render(_ context: Context) throws -> String { - let values = try variable.resolve(context) + func context(values: [Any], index: Int, item: Any) -> [String: Any] { + var result: [String: Any] = [ + "maploop": [ + "counter": index, + "first": index == 0, + "last": index == (values.count - 1), + "item": item + ] + ] - if let values = values as? [Any], values.count > 0 { - let mappedValues: [String] = try values.enumerated().map { (index, item) in - var mapContext: [String: Any] = [ - "maploop": [ - "counter": index, - "first": index == 0, - "last": index == (values.count - 1), - "item": item - ] - ] - if let mapVariable = mapVariable { - mapContext[mapVariable] = item - } - - return try context.push(dictionary: mapContext) { - try renderNodes(nodes, context) - } - } - context[resultName] = mappedValues + if let mapVariable = mapVariable { + result[mapVariable] = item } - // Map should never render anything (no side effects) - return "" + return result } } diff --git a/Tests/StencilSwiftKitTests/MapNodeTests.swift b/Tests/StencilSwiftKitTests/MapNodeTests.swift index 3fb6e605..710def1e 100644 --- a/Tests/StencilSwiftKitTests/MapNodeTests.swift +++ b/Tests/StencilSwiftKitTests/MapNodeTests.swift @@ -5,27 +5,153 @@ // import XCTest +@testable import Stencil @testable import StencilSwiftKit class MapNodeTests: XCTestCase { static let context = [ - "items1": ["one", "two", "three"], - "items2": ["hello", "world", "everyone"] + "items": ["one", "two", "three"] ] - func testBasic() { - let template = StencilSwiftTemplate(templateString: Fixtures.string(for: "map-basic.stencil"), environment: stencilSwiftEnvironment()) - let result = try! template.render(MapNodeTests.context) + func testParser() { + let tokens: [Token] = [ + .block(value: "map items into result"), + .text(value: "hello"), + .block(value: "endmap") + ] - let expected = Fixtures.string(for: "map-basic.out") - XCTDiffStrings(result, expected) + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + guard let nodes = try? parser.parse(), + let node = nodes.first as? MapNode else { + XCTFail("Unable to parse tokens") + return + } + + XCTAssertEqual(node.variable, Variable("items")) + XCTAssertEqual(node.resultName, "result") + XCTAssertNil(node.mapVariable) + XCTAssertEqual(node.nodes.count, 1) + XCTAssert(node.nodes.first is TextNode) + } + + func testParserWithMapVariable() { + let tokens: [Token] = [ + .block(value: "map items into result using item"), + .text(value: "hello"), + .block(value: "endmap") + ] + + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + guard let nodes = try? parser.parse(), + let node = nodes.first as? MapNode else { + XCTFail("Unable to parse tokens") + return + } + + XCTAssertEqual(node.variable, Variable("items")) + XCTAssertEqual(node.resultName, "result") + XCTAssertEqual(node.mapVariable, "item") + XCTAssertEqual(node.nodes.count, 1) + XCTAssert(node.nodes.first is TextNode) } - func testWithIndex() { - let template = StencilSwiftTemplate(templateString: Fixtures.string(for: "map-with-index.stencil"), environment: stencilSwiftEnvironment()) - let result = try! template.render(MapNodeTests.context) + func testParserFail() { + // no closing tag + do { + let tokens: [Token] = [ + .block(value: "map items into result"), + .text(value: "hello") + ] + + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + XCTAssertThrowsError(try parser.parse()) + } + + // no parameters + do { + let tokens: [Token] = [ + .block(value: "map"), + .text(value: "hello"), + .block(value: "endmap") + ] + + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + XCTAssertThrowsError(try parser.parse()) + } + + // no result parameters + do { + let tokens: [Token] = [ + .block(value: "map items"), + .text(value: "hello"), + .block(value: "endmap") + ] + + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + XCTAssertThrowsError(try parser.parse()) + } + + // no result variable name + do { + let tokens: [Token] = [ + .block(value: "map items into"), + .text(value: "hello"), + .block(value: "endmap") + ] + + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + XCTAssertThrowsError(try parser.parse()) + } + + // no map variable name + do { + let tokens: [Token] = [ + .block(value: "map items into result using"), + .text(value: "hello"), + .block(value: "endmap") + ] + + let parser = TokenParser(tokens: tokens, environment: stencilSwiftEnvironment()) + XCTAssertThrowsError(try parser.parse()) + } + } - let expected = Fixtures.string(for: "map-with-index.out") - XCTDiffStrings(result, expected) + func testRender() { + let context = Context(dictionary: MapNodeTests.context) + let node = MapNode(variable: "items", resultName: "result", mapVariable: nil, nodes: [TextNode(text: "hello")]) + let output = try! node.render(context) + + XCTAssertEqual(output, "") + } + + func testContext() { + let context = Context(dictionary: MapNodeTests.context) + let node = MapNode(variable: "items", resultName: "result", mapVariable: "item", nodes: [TextNode(text: "hello")]) + _ = try! node.render(context) + + guard let items = context["items"] as? [String], let result = context["result"] as? [String] else { + XCTFail("Unable to render map") + return + } + XCTAssertEqual(items, MapNodeTests.context["items"] ?? []) + XCTAssertEqual(result, ["hello", "hello", "hello"]) + } + + func testMapLoopContext() { + let context = Context(dictionary: MapNodeTests.context) + let node = MapNode(variable: "items", resultName: "result", mapVariable: nil, nodes: [ + VariableNode(variable: "maploop.counter"), + VariableNode(variable: "maploop.first"), + VariableNode(variable: "maploop.last"), + VariableNode(variable: "maploop.item") + ]) + _ = try! node.render(context) + + guard let items = context["items"] as? [String], let result = context["result"] as? [String] else { + XCTFail("Unable to render map") + return + } + XCTAssertEqual(items, MapNodeTests.context["items"] ?? []) + XCTAssertEqual(result, ["0truefalseone", "1falsefalsetwo", "2falsetruethree"]) } }