From 6ad609e562197493c78e3ad65cb418f12f68ea8f Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Mon, 28 Nov 2016 17:20:47 +0000 Subject: [PATCH] refactor(if): Move expressions to separate file --- Sources/Expression.swift | 136 +++++++++++++++++++ Sources/IfTag.swift | 138 ------------------- Tests/StencilTests/ExpressionSpec.swift | 171 ++++++++++++++++++++++++ Tests/StencilTests/XCTest.swift | 2 +- 4 files changed, 308 insertions(+), 139 deletions(-) create mode 100644 Sources/Expression.swift create mode 100644 Tests/StencilTests/ExpressionSpec.swift diff --git a/Sources/Expression.swift b/Sources/Expression.swift new file mode 100644 index 0000000..45ea171 --- /dev/null +++ b/Sources/Expression.swift @@ -0,0 +1,136 @@ +protocol Expression: CustomStringConvertible { + func evaluate(context: Context) throws -> Bool +} + + +protocol InfixOperator: Expression { + init(lhs: Expression, rhs: Expression) +} + + +protocol PrefixOperator: Expression { + init(expression: Expression) +} + + +final class StaticExpression: Expression, CustomStringConvertible { + let value: Bool + + init(value: Bool) { + self.value = value + } + + func evaluate(context: Context) throws -> Bool { + return value + } + + var description: String { + return "\(value)" + } +} + + +final class VariableExpression: Expression, CustomStringConvertible { + let variable: Variable + + init(variable: Variable) { + self.variable = variable + } + + var description: String { + return "(variable: \(variable.variable))" + } + + /// Resolves a variable in the given context as boolean + func resolve(context: Context, variable: Variable) throws -> Bool { + let result = try variable.resolve(context) + var truthy = false + + if let result = result as? [Any] { + truthy = !result.isEmpty + } else if let result = result as? [String:Any] { + truthy = !result.isEmpty + } else if let result = result as? Bool { + truthy = result + } else if let result = result as? Int { + truthy = result > 0 + } else if let result = result as? Float { + truthy = result > 0 + } else if let result = result as? Double { + truthy = result > 0 + } else if result != nil { + truthy = true + } + + return truthy + } + + func evaluate(context: Context) throws -> Bool { + return try resolve(context: context, variable: variable) + } +} + + +final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { + let expression: Expression + + init(expression: Expression) { + self.expression = expression + } + + var description: String { + return "not \(expression)" + } + + func evaluate(context: Context) throws -> Bool { + return try !expression.evaluate(context: context) + } +} + + +final class OrExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) or \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + let lhs = try self.lhs.evaluate(context: context) + if lhs { + return lhs + } + + return try rhs.evaluate(context: context) + } +} + + +final class AndExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) and \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + let lhs = try self.lhs.evaluate(context: context) + if !lhs { + return lhs + } + + return try rhs.evaluate(context: context) + } +} diff --git a/Sources/IfTag.swift b/Sources/IfTag.swift index 91b8753..b8ef3f7 100644 --- a/Sources/IfTag.swift +++ b/Sources/IfTag.swift @@ -1,141 +1,3 @@ -protocol Expression: CustomStringConvertible { - func evaluate(context: Context) throws -> Bool -} - - -protocol InfixOperator: Expression { - init(lhs: Expression, rhs: Expression) -} - - -protocol PrefixOperator: Expression { - init(expression: Expression) -} - - -final class StaticExpression: Expression, CustomStringConvertible { - let value: Bool - - init(value: Bool) { - self.value = value - } - - func evaluate(context: Context) throws -> Bool { - return value - } - - var description: String { - return "\(value)" - } -} - - -final class VariableExpression: Expression, CustomStringConvertible { - let variable: Variable - - init(variable: Variable) { - self.variable = variable - } - - var description: String { - return "(variable: \(variable.variable))" - } - - /// Resolves a variable in the given context as boolean - func resolve(context: Context, variable: Variable) throws -> Bool { - let result = try variable.resolve(context) - var truthy = false - - if let result = result as? [Any] { - truthy = !result.isEmpty - } else if let result = result as? [String:Any] { - truthy = !result.isEmpty - } else if let result = result as? Bool { - truthy = result - } else if let result = result as? Int { - truthy = result > 0 - } else if let result = result as? Float { - truthy = result > 0 - } else if let result = result as? Double { - truthy = result > 0 - } else if result != nil { - truthy = true - } - - return truthy - } - - func evaluate(context: Context) throws -> Bool { - return try resolve(context: context, variable: variable) - } -} - - -final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { - let expression: Expression - - init(expression: Expression) { - self.expression = expression - } - - var description: String { - return "not \(expression)" - } - - func evaluate(context: Context) throws -> Bool { - return try !expression.evaluate(context: context) - } -} - - -final class OrExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression - - init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } - - var description: String { - return "(\(lhs) or \(rhs))" - } - - func evaluate(context: Context) throws -> Bool { - let lhs = try self.lhs.evaluate(context: context) - if lhs { - return lhs - } - - return try rhs.evaluate(context: context) - } -} - - -final class AndExpression: Expression, InfixOperator, CustomStringConvertible { - let lhs: Expression - let rhs: Expression - - init(lhs: Expression, rhs: Expression) { - self.lhs = lhs - self.rhs = rhs - } - - var description: String { - return "(\(lhs) and \(rhs))" - } - - func evaluate(context: Context) throws -> Bool { - let lhs = try self.lhs.evaluate(context: context) - if !lhs { - return lhs - } - - return try rhs.evaluate(context: context) - } -} - - enum Operator { case infix(String, Int, InfixOperator.Type) case prefix(String, Int, PrefixOperator.Type) diff --git a/Tests/StencilTests/ExpressionSpec.swift b/Tests/StencilTests/ExpressionSpec.swift new file mode 100644 index 0000000..466f924 --- /dev/null +++ b/Tests/StencilTests/ExpressionSpec.swift @@ -0,0 +1,171 @@ +import Spectre +@testable import Stencil + + +func testExpressions() { + describe("Expression") { + $0.describe("VariableExpression") { + let expression = VariableExpression(variable: Variable("value")) + + $0.it("evaluates to true when value is not nil") { + let context = Context(dictionary: ["value": "known"]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when value is unset") { + let context = Context() + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to true when array variable is not empty") { + let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]] + let context = Context(dictionary: ["value": [items]]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when array value is empty") { + let emptyItems = [[String: Any]]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to false when dictionary value is empty") { + let emptyItems = [String:Any]() + let context = Context(dictionary: ["value": emptyItems]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to false when Array value is empty") { + let context = Context(dictionary: ["value": ([] as [Any])]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to true when integer value is above 0") { + let context = Context(dictionary: ["value": 1]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when integer value is below 0 or below") { + let context = Context(dictionary: ["value": 0]) + try expect(try expression.evaluate(context: context)).to.beFalse() + + let negativeContext = Context(dictionary: ["value": 0]) + try expect(try expression.evaluate(context: negativeContext)).to.beFalse() + } + + $0.it("evaluates to true when float value is above 0") { + let context = Context(dictionary: ["value": Float(0.5)]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when float is 0 or below") { + let context = Context(dictionary: ["value": Float(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + + $0.it("evaluates to true when double value is above 0") { + let context = Context(dictionary: ["value": Double(0.5)]) + try expect(try expression.evaluate(context: context)).to.beTrue() + } + + $0.it("evaluates to false when double is 0 or below") { + let context = Context(dictionary: ["value": Double(0)]) + try expect(try expression.evaluate(context: context)).to.beFalse() + } + } + + $0.describe("NotExpression") { + $0.it("returns truthy for positive expressions") { + let expression = NotExpression(expression: StaticExpression(value: true)) + try expect(expression.evaluate(context: Context())).to.beFalse() + } + + $0.it("returns falsy for negative expressions") { + let expression = NotExpression(expression: StaticExpression(value: false)) + try expect(expression.evaluate(context: Context())).to.beTrue() + } + } + + $0.describe("expression parsing") { + $0.it("can parse a variable expression") { + let expression = try parseExpression(components: ["value"]) + try expect(expression.evaluate(context: Context())).to.beFalse() + try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue() + } + + $0.it("can parse a not expression") { + let expression = try parseExpression(components: ["not", "value"]) + try expect(expression.evaluate(context: Context())).to.beTrue() + try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse() + } + + $0.describe("and expression") { + let expression = try! parseExpression(components: ["lhs", "and", "rhs"]) + + $0.it("evaluates to false with lhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse() + } + + $0.it("evaluates to false with rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() + } + + $0.it("evaluates to false with lhs and rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + } + + $0.it("evaluates to true with lhs and rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + } + + $0.describe("or expression") { + let expression = try! parseExpression(components: ["lhs", "or", "rhs"]) + + $0.it("evaluates to true with lhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() + } + + $0.it("evaluates to true with rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() + } + + $0.it("evaluates to true with lhs and rhs true") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() + } + + $0.it("evaluates to false with lhs and rhs false") { + try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() + } + } + + $0.describe("multiple expression") { + let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"]) + + $0.it("evaluates to true with one") { + try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue() + } + + $0.it("evaluates to true with one and three") { + try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() + } + + $0.it("evaluates to true with two") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() + } + + $0.it("evaluates to false with two and three") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + } + + $0.it("evaluates to false with two and three") { + try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() + } + + $0.it("evaluates to false with nothing") { + try expect(expression.evaluate(context: Context())).to.beFalse() + } + } + } + } +} diff --git a/Tests/StencilTests/XCTest.swift b/Tests/StencilTests/XCTest.swift index 789a317..cea0c34 100644 --- a/Tests/StencilTests/XCTest.swift +++ b/Tests/StencilTests/XCTest.swift @@ -12,12 +12,12 @@ public func stencilTests() { testVariable() testNode() testForNode() + testExpressions() testIfNode() testNowNode() testInclude() testInheritence() testStencil() - }