diff --git a/Packages/ClientRuntime/Sources/Waiters/JMESUtils.swift b/Packages/ClientRuntime/Sources/Waiters/JMESUtils.swift index d52d577b9..083db84d7 100644 --- a/Packages/ClientRuntime/Sources/Waiters/JMESUtils.swift +++ b/Packages/ClientRuntime/Sources/Waiters/JMESUtils.swift @@ -35,15 +35,17 @@ public enum JMESUtils { return comparator(lhs, rhs) } -// Functions for comparing Int to Int. +// Functions for comparing Int to Int. Double comparators are used since Int has +// extra overloads on `==` that prevent it from resolving correctly, and Ints compared +// to Doubles are already compared as Doubles anyway. - public static func compare(_ lhs: Int?, _ comparator: (Int?, Int?) -> Bool, _ rhs: Int?) -> Bool { - comparator(lhs, rhs) + public static func compare(_ lhs: Int?, _ comparator: (Double?, Double?) -> Bool, _ rhs: Int?) -> Bool { + comparator(lhs.map { Double($0) }, rhs.map { Double($0) }) } - public static func compare(_ lhs: Int?, _ comparator: (Int, Int) -> Bool, _ rhs: Int?) -> Bool { + public static func compare(_ lhs: Int?, _ comparator: (Double, Double) -> Bool, _ rhs: Int?) -> Bool { guard let lhs = lhs, let rhs = rhs else { return false } - return comparator(lhs, rhs) + return comparator(Double(lhs), Double(rhs)) } // Function for comparing String to String. diff --git a/Packages/ClientRuntime/Tests/WaiterTests/JMESUtilsTests.swift b/Packages/ClientRuntime/Tests/WaiterTests/JMESUtilsTests.swift new file mode 100644 index 000000000..c3bfebf29 --- /dev/null +++ b/Packages/ClientRuntime/Tests/WaiterTests/JMESUtilsTests.swift @@ -0,0 +1,165 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import XCTest +@testable import ClientRuntime + +class JMESUtilsTests: XCTestCase { + + // MARK: - Equatable + + // Note that Swift has trouble resolving the `!=` operator + // for `Double?`, `Double?` operand types (not for any other type). + // Codegen will translate `!=` into `==` and negate the expression + // to avoid a compile failure. + // + // Hence `Int` (which uses `Double` comparators) and `Double` are + // not tested with `!=` below. + + func test_equateInt_handlesOptionalityCombos() async throws { + let lhs: Int? = 1 + let rhs: Int? = 2 + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs!)) + } + + func test_equateDouble_handlesOptionalityCombos() async throws { + let lhs: Double? = 1.0 + let rhs: Double? = 2.0 + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs!)) + } + + func test_equateString_handlesOptionalityCombos() async throws { + let lhs: String? = "a" + let rhs: String? = "b" + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs, !=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, !=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, !=, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, !=, rhs!)) + } + + func test_equateBool_handlesOptionalityCombos() async throws { + let lhs: Bool? = true + let rhs: Bool? = false + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs, !=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, !=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, !=, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, !=, rhs!)) + } + + func test_equateIntToDouble_handlesOptionalityCombos() async throws { + let lhs: Int? = 1 + let rhs: Double? = 2.0 + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs!)) + } + + func test_equateStringToRawRepresentable_handlesOptionalityCombos() async throws { + let lhs: String? = "a" + let rhs: Stringed? = Stringed(rawValue: "b") + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, ==, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, ==, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs, !=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, !=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, !=, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, !=, rhs!)) + } + + // MARK: - Comparable + + func test_compareInt_handlesOptionalityCombos() async throws { + let lhs: Int? = 1 + let rhs: Int? = 2 + XCTAssertTrue(JMESUtils.compare(lhs, <, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, <, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, <, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, <, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs, <=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, <=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, <=, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, <=, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs, >, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, >, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, >, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, >, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs, >=, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, >=, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, >=, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, >=, rhs!)) + } + + func test_compareDouble_handlesOptionalityCombos() async throws { + let lhs: Double? = 1.0 + let rhs: Double? = 2.0 + XCTAssertTrue(JMESUtils.compare(lhs, <, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, <, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, <, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, <, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs, <=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, <=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, <=, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, <=, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs, >, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, >, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, >, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, >, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs, >=, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, >=, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, >=, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, >=, rhs!)) + } + + func test_compareIntToDouble_handlesOptionalityCombos() async throws { + let lhs: Int? = 1 + let rhs: Double? = 2.0 + XCTAssertTrue(JMESUtils.compare(lhs, <, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, <, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, <, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, <, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs, <=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs!, <=, rhs)) + XCTAssertTrue(JMESUtils.compare(lhs, <=, rhs!)) + XCTAssertTrue(JMESUtils.compare(lhs!, <=, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs, >, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, >, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, >, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, >, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs, >=, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs!, >=, rhs)) + XCTAssertFalse(JMESUtils.compare(lhs, >=, rhs!)) + XCTAssertFalse(JMESUtils.compare(lhs!, >=, rhs!)) + } +} + +// Used for tests of Equatable between String and RawRepresentable. +fileprivate struct Stringed: RawRepresentable { + typealias RawValue = String + + let rawValue: String + + init?(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/JMESPathVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/JMESPathVisitor.kt index 2586bd608..ac69a5820 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/JMESPathVisitor.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/JMESPathVisitor.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.jmespath.JmespathExpression import software.amazon.smithy.jmespath.RuntimeType import software.amazon.smithy.jmespath.ast.AndExpression import software.amazon.smithy.jmespath.ast.ComparatorExpression +import software.amazon.smithy.jmespath.ast.ComparatorType import software.amazon.smithy.jmespath.ast.CurrentExpression import software.amazon.smithy.jmespath.ast.ExpressionTypeExpression import software.amazon.smithy.jmespath.ast.FieldExpression @@ -162,20 +163,29 @@ class JMESPathVisitor( } // Perform a comparison of two values. - // The JMESValue type is used to provide conversion and comparison as needed between types - // that aren't comparable in "pure Swift" (i.e. Int to Double or String to RawRepresentable - // by String.) + // + // The JMESUtils.compare() function is used to provide conversion and comparison as needed + // between types that aren't comparable in "pure Swift" (i.e. Int to Double or String to + // RawRepresentable by String.) + // // The Smithy comparator is a string that just happens to match up with all Swift comparators, - // so it is rendered into Swift as-is. + // so it is rendered into Swift as-is, with one exception: + // + // Due to overload resolution difficulties with `!=` in Swift, an inequality expression is + // rendered as equality then negated. override fun visitComparator(expression: ComparatorExpression): JMESVariable { val left = expression.left!!.accept(this) val right = expression.right!!.accept(this) + val isInequality = expression.comparator == ComparatorType.NOT_EQUAL + val comparator = ComparatorType.EQUAL.takeIf { isInequality } ?: expression.comparator + val negationMark = "!".takeIf { isInequality } ?: "" val comparisonResultVar = JMESVariable("comparison", false, boolShape) return addTempVar( comparisonResultVar, - "JMESUtils.compare(\$L, \$L, \$L)", + "\$LJMESUtils.compare(\$L, \$L, \$L)", + negationMark, left.name, - expression.comparator, + comparator, right.name ) }