Skip to content

Commit 2f4ebfc

Browse files
evinyangnicklockwood
authored andcommitted
Support dictionary literals using infix operator (#40)
1 parent 81e8f21 commit 2f4ebfc

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-1
lines changed

Sources/AnyExpression.swift

+20-1
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,24 @@ public struct AnyExpression: CustomStringConvertible {
401401
}
402402
}
403403
case .function("[]", _):
404-
return { box.store($0.map(box.load)) }
404+
return { args in
405+
let args = args.map(box.load)
406+
let keyVals = args.compactMap { $0 as? Dictionary<AnyHashable, Any>.Element }
407+
return box.store(
408+
args.isEmpty || args.count != keyVals.count
409+
? args
410+
: keyVals.reduce(into: [AnyHashable: Any]()) { $0[$1.key] = $1.value }
411+
)
412+
}
413+
case .infix(":"):
414+
return { args in
415+
switch (box.load(args[0]), box.load(args[1])) {
416+
case let (lhs as AnyHashable, rhs):
417+
return box.store(Dictionary<AnyHashable, Any>.Element(key: lhs, value: rhs))
418+
case let (lhs, rhs):
419+
throw Error.typeMismatch(symbol, [lhs, rhs])
420+
}
421+
}
405422
case let .variable(name):
406423
guard let string = unwrapString(name) else {
407424
return { _ in throw Error.undefinedSymbol(symbol) }
@@ -608,6 +625,8 @@ extension AnyExpression.Error {
608625
}
609626
case .infix("==") where types.count == 2 && types[0] == types[1]:
610627
return .message("Arguments for \(symbol) must conform to the Hashable protocol")
628+
case .infix(":") where types.count == 2 && !(args[0] is AnyHashable):
629+
return .message("First argument for \(symbol) must conform to the Hashable protocol")
611630
case _ where types.count == 1:
612631
return .message("Argument of type \(types[0]) is not compatible with \(symbol)")
613632
default:

Tests/AnyExpressionTests.swift

+42
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,48 @@ class AnyExpressionTests: XCTestCase {
398398
}
399399
}
400400

401+
func testStringDictionaryLiteral() {
402+
let expression = AnyExpression("['a': 1, 'b': 2.5, 'c': 3]")
403+
XCTAssertEqual(try expression.evaluate(), ["a": 1, "b": 2.5, "c": 3])
404+
}
405+
406+
func testDoubleDictionaryLiteral() {
407+
let expression = AnyExpression("[1.5: false, 2.0: nil, 3.5: true]")
408+
XCTAssertEqual(try expression.evaluate(), [1.5: false, 2.0: nil, 3.5: true])
409+
}
410+
411+
func testIntDictionaryLiteral() {
412+
let expression = AnyExpression("[1: 'f', 2: 'e', 3: 'd']")
413+
XCTAssertEqual(try expression.evaluate(), [1: "f", 2: "e", 3: "d"])
414+
}
415+
416+
func testDictionaryLiteralWithNonHashableKey() {
417+
let expression = AnyExpression("[nil: false]")
418+
XCTAssertThrowsError(try expression.evaluate() as Any) { error in
419+
XCTAssertEqual(error as? Expression.Error, .typeMismatch(.infix(":"), [nil as Any? as Any, false]))
420+
}
421+
}
422+
423+
func testSubscriptStringDictionaryLiteralWithString() {
424+
let expression = AnyExpression("['a': 1, 'b': 2.5, 'c': 3]['c']")
425+
XCTAssertEqual(try expression.evaluate(), 3)
426+
}
427+
428+
func testSubscriptDoubleDictionaryLiteralWithInt() {
429+
let expression = AnyExpression("[1.5: false, 2.0: nil, 3.5: true][2]")
430+
XCTAssertEqual(try expression.evaluate(), Optional<Bool>.none)
431+
}
432+
433+
func testSubscriptIntDictionaryLiteralWithDouble() {
434+
let expression = AnyExpression("[1: 'f', 2: 'e', 3: 'd'][1.0]")
435+
XCTAssertEqual(try expression.evaluate(), "f")
436+
}
437+
438+
func testSubscriptDictionaryLiteralWithNonexistentKey() {
439+
let expression = AnyExpression("[1: 'f', 2: 'e', 3: 'd']['d']")
440+
XCTAssertEqual(try expression.evaluate(), Optional<String>.none)
441+
}
442+
401443
// MARK: Ranges
402444

403445
func testClosedIntRange() {

0 commit comments

Comments
 (0)