-
Notifications
You must be signed in to change notification settings - Fork 225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolvable boolean expressions #164
Changes from 1 commit
83113ab
3bf77d9
a63d6d9
e2aedf9
a0af6f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
protocol Expression: CustomStringConvertible { | ||
protocol Expression: CustomStringConvertible, Resolvable { | ||
func evaluate(context: Context) throws -> Bool | ||
} | ||
|
||
extension Expression { | ||
|
||
func resolve(_ context: Context) throws -> Any? { | ||
return try "\(evaluate(context: context))" | ||
} | ||
|
||
} | ||
|
||
protocol InfixOperator: Expression { | ||
init(lhs: Expression, rhs: Expression) | ||
|
@@ -40,9 +47,13 @@ final class VariableExpression: Expression, CustomStringConvertible { | |
var description: String { | ||
return "(variable: \(variable))" | ||
} | ||
|
||
|
||
func resolve(_ context: Context) throws -> Any? { | ||
return try variable.resolve(context) | ||
} | ||
|
||
/// Resolves a variable in the given context as boolean | ||
func resolve(context: Context, variable: Resolvable) throws -> Bool { | ||
func evaluate(context: Context) throws -> Bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weird, I think I've seen this change a few times in other PRs? 😕😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe, some features required similar changes |
||
let result = try variable.resolve(context) | ||
var truthy = false | ||
|
||
|
@@ -63,9 +74,6 @@ final class VariableExpression: Expression, CustomStringConvertible { | |
return truthy | ||
} | ||
|
||
func evaluate(context: Context) throws -> Bool { | ||
return try resolve(context: context, variable: variable) | ||
} | ||
} | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,14 @@ func testExpressions() { | |
$0.describe("VariableExpression") { | ||
let expression = VariableExpression(variable: Variable("value")) | ||
|
||
$0.it("can resolve variable") { | ||
let context = Context(dictionary: ["value": "known"]) | ||
try expect(expression.resolve(context) as? String) == "known" | ||
|
||
try expect(Template(templateString: "{{ value == \"known\" }}").render(["value": "known"])) == "true" | ||
try expect(Template(templateString: "{{ value == \"known\" }}").render(["value": "unknown"])) == "false" | ||
} | ||
|
||
$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() | ||
|
@@ -93,13 +101,19 @@ func testExpressions() { | |
|
||
$0.describe("NotExpression") { | ||
$0.it("returns truthy for positive expressions") { | ||
let expression = NotExpression(expression: StaticExpression(value: true)) | ||
let expression = NotExpression(expression: VariableExpression(variable: Variable("true"))) | ||
try expect(expression.evaluate(context: Context())).to.beFalse() | ||
|
||
try expect(Template(templateString: "{% if true %}true{% else %}false{% endif %}").render(Context())) == "true" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is the unit tests for expression, I do not want to test other units in this file. This is further becoming an integration test when we are creating a template and testing other functionality such as the `if statements. |
||
try expect(Template(templateString: "{% if false %}true{% else %}false{% endif %}").render(Context())) == "false" | ||
} | ||
|
||
$0.it("returns falsy for negative expressions") { | ||
let expression = NotExpression(expression: StaticExpression(value: false)) | ||
let expression = NotExpression(expression: VariableExpression(variable: Variable("false"))) | ||
try expect(expression.evaluate(context: Context())).to.beTrue() | ||
|
||
try expect(Template(templateString: "{% if not true %}true{% else %}false{% endif %}").render(Context())) == "false" | ||
try expect(Template(templateString: "{% if not false %}true{% else %}false{% endif %}").render(Context())) == "true" | ||
} | ||
} | ||
|
||
|
@@ -136,167 +150,176 @@ func testExpressions() { | |
} | ||
} | ||
|
||
func expectExpression(with components: [String], context: [String: Any], toBe expected: Bool) throws { | ||
let expression = try parseExpression(components: components, tokenParser: parser) | ||
try expect(expression.evaluate(context: Context(dictionary: context))) == expected | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will cause the source maps for the thrown error when the tests fail to be raised from here making test failures harder to pinpoint. To solve this we can grab the file and line number from the func expectExpression(..., file: String = #file, line: Int = #line, function: String = #function) {
...
try expect(..., file: file, line: line, function: function) == expected
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! |
||
|
||
let template = Template(templateString: "{{ \(components.joined(separator: " ")) }}") | ||
let result = try template.render(context) | ||
try expect(result) == String(expected) | ||
} | ||
|
||
$0.describe("or expression") { | ||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser) | ||
|
||
let components = ["lhs", "or", "rhs"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trailing newlines |
||
$0.it("evaluates to true with lhs true") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": true, "rhs": false], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to true with rhs true") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": false, "rhs": true], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to true with lhs and rhs true") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": true, "rhs": true], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with lhs and rhs false") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": false, "rhs": false], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("equality expression") { | ||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser) | ||
let components = ["lhs", "==", "rhs"] | ||
|
||
$0.it("evaluates to true with equal lhs/rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": "a"], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with non equal lhs/rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": "b"], toBe: false) | ||
} | ||
|
||
$0.it("evaluates to true with nils") { | ||
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue() | ||
try expectExpression(with: components, context: [:], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to true with numbers") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": 1, "rhs": 1.0], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with non equal numbers") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": 1, "rhs": 1.1], toBe: false) | ||
} | ||
|
||
$0.it("evaluates to true with booleans") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": true, "rhs": true], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with falsy booleans") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": true, "rhs": false], toBe: false) | ||
} | ||
|
||
$0.it("evaluates to false with different types") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": true, "rhs": 1], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("inequality expression") { | ||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser) | ||
let components = ["lhs", "!=", "rhs"] | ||
|
||
$0.it("evaluates to true with inequal lhs/rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": "b"], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with equal lhs/rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": "b", "rhs": "b"], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("more than expression") { | ||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser) | ||
let components = ["lhs", ">", "rhs"] | ||
|
||
$0.it("evaluates to true with lhs > rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 4], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with lhs == rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5.0], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("more than equal expression") { | ||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser) | ||
let components = ["lhs", ">=", "rhs"] | ||
|
||
$0.it("evaluates to true with lhs == rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with lhs < rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5.1], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("less than expression") { | ||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser) | ||
let components = ["lhs", "<", "rhs"] | ||
|
||
$0.it("evaluates to true with lhs < rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": 4, "rhs": 4.5], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with lhs == rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5.0], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("less than equal expression") { | ||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser) | ||
let components = ["lhs", "<=", "rhs"] | ||
|
||
$0.it("evaluates to true with lhs == rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": 5.0, "rhs": 5], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with lhs > rhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": 5.1, "rhs": 5.0], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("multiple expression") { | ||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser) | ||
let 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() | ||
try expectExpression(with: components, context: ["one": true], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to true with one and three") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue() | ||
try expectExpression(with: components, context: ["one": true, "three": true], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to true with two") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue() | ||
try expectExpression(with: components, context: ["two": true], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false with two and three") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() | ||
try expectExpression(with: components, context: ["two": true, "three": true], toBe: false) | ||
} | ||
|
||
$0.it("evaluates to false with two and three") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse() | ||
try expectExpression(with: components, context: ["two": true, "three": true], toBe: false) | ||
} | ||
|
||
$0.it("evaluates to false with nothing") { | ||
try expect(expression.evaluate(context: Context())).to.beFalse() | ||
try expectExpression(with: components, context: [:], toBe: false) | ||
} | ||
} | ||
|
||
$0.describe("in expression") { | ||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser) | ||
let components = ["lhs", "in", "rhs"] | ||
|
||
$0.it("evaluates to true when rhs contains lhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue() | ||
try expectExpression(with: components, context: ["lhs": 1, "rhs": [1, 2, 3]], toBe: true) | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": ["a", "b", "c"]], toBe: true) | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": "abc"], toBe: true) | ||
try expectExpression(with: components, context: ["lhs": 1, "rhs": 1...3], toBe: true) | ||
try expectExpression(with: components, context: ["lhs": 1, "rhs": 1..<3], toBe: true) | ||
} | ||
|
||
$0.it("evaluates to false when rhs does not contain lhs") { | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse() | ||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse() | ||
try expectExpression(with: components, context: ["lhs": 1, "rhs": [2, 3, 4]], toBe: false) | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": ["b", "c", "d"]], toBe: false) | ||
try expectExpression(with: components, context: ["lhs": "a", "rhs": "bcd"], toBe: false) | ||
try expectExpression(with: components, context: ["lhs": 4, "rhs": 1...3], toBe: false) | ||
try expectExpression(with: components, context: ["lhs": 3, "rhs": 1..<3], toBe: false) | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot the
.
and 2 spaces. Also remove the empty line above.