├── .gitignore ├── README.md ├── Package.swift ├── LICENSE ├── Sources └── BoolBuilder │ └── BoolBuilder.swift └── Tests └── BoolBuilderTests └── BoolBuilderTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BoolBuilder 2 | 3 | `@resultBuilder` for building a `Bool`. 4 | 5 | ## Example 6 | 7 | ```swift 8 | import BoolBuilder 9 | 10 | let condition: Bool = all { 11 | any { 12 | conditionA 13 | conditionB 14 | .inverted 15 | 16 | either { 17 | conditionC 18 | } or: { 19 | conditionD 20 | } 21 | } 22 | conditionE 23 | } 24 | ``` 25 | 26 | ## Acknowledgements 27 | 28 | Thanks to [@Vince14Genius](https://github.com/vince14genius) for the idea and API feedback. 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.4 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "BoolBuilder", 8 | products: [ 9 | .library( 10 | name: "BoolBuilder", 11 | targets: ["BoolBuilder"]), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "BoolBuilder", 16 | dependencies: []), 17 | .testTarget( 18 | name: "BoolBuilderTests", 19 | dependencies: ["BoolBuilder"]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zhiyu Zhu/朱智语 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/BoolBuilder/BoolBuilder.swift: -------------------------------------------------------------------------------- 1 | extension Bool { 2 | /// `false` if `self` is `true`; `true` if `self` is `false`. 3 | public var inverted: Bool { 4 | !self 5 | } 6 | } 7 | 8 | /// Performs an eXclusive OR operation on two Boolean conditions. 9 | /// 10 | /// - Parameters: 11 | /// - condition: A function producing one of the two conditions to check. 12 | /// - theOtherCondition: A function producing the other condition to check. 13 | /// - Returns: `true` if `condition` and `theOtherCondition` return different 14 | /// values, otherwise `false`. 15 | public func either(_ condition: () throws -> Bool, 16 | or theOtherCondition: () throws -> Bool) rethrows -> Bool { 17 | try condition() != theOtherCondition() 18 | } 19 | 20 | // MARK: - && and || 21 | 22 | public protocol _BoolBuilder { 23 | typealias Component = () -> Bool 24 | typealias Operator = (Bool, Component) -> Bool 25 | static var combinePartialResult: Operator { get } 26 | } 27 | 28 | extension _BoolBuilder { 29 | public static func buildExpression( 30 | _ expression: @escaping @autoclosure Component 31 | ) -> Component { 32 | expression 33 | } 34 | 35 | #if swift(>=5.7) 36 | public static func buildPartialBlock(first: Component) -> Bool { 37 | first() 38 | } 39 | 40 | public static func buildPartialBlock( 41 | accumulated: Bool, next: Component 42 | ) -> Bool { 43 | combinePartialResult(accumulated, next) 44 | } 45 | #else 46 | public static func buildBlock( 47 | _ first: Component, _ remaining: Component... 48 | ) -> Bool { 49 | remaining.reduce(first(), combinePartialResult) 50 | } 51 | #endif 52 | } 53 | 54 | @resultBuilder 55 | public enum AndBuilder: _BoolBuilder { 56 | public static let combinePartialResult: Operator = { $0 && $1() } 57 | 58 | #if canImport(PlaygroundBluetooth) 59 | // Swift Playgrounds doesn't show compiler warnings, so 60 | // we need to promote this to a compiler error instead. 61 | // Since PlaygroundBluetooth is a Swift Playgrounds only framework, it's 62 | // a good indicator for if we are compiling for Swift Playgrounds or not. 63 | 64 | @available(*, unavailable, message: """ 65 | Replace empty BoolBuilder with true instead. 66 | """) 67 | public static func buildBlock() -> Bool { true } 68 | #else 69 | @available(*, deprecated, message: """ 70 | Empty BoolBuilder always evaluates to true. 71 | Consider replacing with true instead. 72 | """) 73 | public static func buildBlock() -> Bool { true } 74 | #endif 75 | } 76 | 77 | @resultBuilder 78 | public enum OrBuilder: _BoolBuilder { 79 | public static let combinePartialResult: Operator = { $0 || $1() } 80 | 81 | #if canImport(PlaygroundBluetooth) 82 | @available(*, unavailable, message: """ 83 | Replace empty BoolBuilder with false instead. 84 | """) 85 | public static func buildBlock() -> Bool { false } 86 | #else 87 | @available(*, deprecated, message: """ 88 | Empty BoolBuilder always evaluates to false. 89 | Consider replacing with false instead. 90 | """) 91 | public static func buildBlock() -> Bool { false } 92 | #endif 93 | } 94 | 95 | /// Performs logical AND operations on the provided Boolean conditions 96 | /// with short-circuit semantics. 97 | /// 98 | /// - Parameter conditions: Conditions to check. 99 | /// - Returns: `true` if all the `conditions` are `true`, otherwise `false`. 100 | public func all(@AndBuilder conditions makeResult: () -> Bool) -> Bool { 101 | makeResult() 102 | } 103 | 104 | /// Performs logical OR operations on the provided Boolean conditions 105 | /// with short-circuit semantics. 106 | /// 107 | /// - Parameter conditions: Conditions to check. 108 | /// - Returns: `true` if at least one of the `conditions` are `true`, 109 | /// otherwise `false`. 110 | public func any(@OrBuilder conditions makeResult: () -> Bool) -> Bool { 111 | makeResult() 112 | } 113 | 114 | // MARK: - && and || with try expressions 115 | 116 | public protocol _ThrowingBoolBuilder { 117 | typealias Component = () throws -> Bool 118 | typealias Operator = (Bool, Component) throws -> Bool 119 | static var combinePartialResult: Operator { get } 120 | } 121 | 122 | extension _ThrowingBoolBuilder { 123 | public typealias FinalResult = Result 124 | 125 | public static func buildExpression( 126 | _ expression: @escaping @autoclosure Component 127 | ) -> Component { 128 | expression 129 | } 130 | 131 | #if swift(>=5.7) 132 | public static func buildPartialBlock(first: Component) -> FinalResult { 133 | FinalResult { 134 | try first() 135 | } 136 | } 137 | 138 | public static func buildPartialBlock( 139 | accumulated: FinalResult, next: Component 140 | ) -> FinalResult { 141 | accumulated.flatMap { result in 142 | FinalResult { 143 | try combinePartialResult(result, next) 144 | } 145 | } 146 | } 147 | #else 148 | public static func buildBlock( 149 | _ first: Component, _ remaining: Component... 150 | ) -> FinalResult { 151 | remaining.reduce(FinalResult { 152 | try first() 153 | }) { accumulated, next in 154 | accumulated.flatMap { result in 155 | FinalResult { 156 | try combinePartialResult(result, next) 157 | } 158 | } 159 | } 160 | } 161 | #endif 162 | } 163 | 164 | @resultBuilder 165 | public enum ThrowingAndBuilder: _ThrowingBoolBuilder { 166 | public static let combinePartialResult: Operator = (&&) 167 | 168 | @available(*, unavailable, message: """ 169 | Empty throwing BoolBuilder can never throw. 170 | Replace with true instead and file a bug report to BoolBuilder authors \ 171 | with example code that reproduces this error message. 172 | """) 173 | public static func buildBlock() -> FinalResult { .success(true) } 174 | } 175 | 176 | @resultBuilder 177 | public enum ThrowingOrBuilder: _ThrowingBoolBuilder { 178 | public static let combinePartialResult: Operator = (||) 179 | 180 | @available(*, unavailable, message: """ 181 | Empty throwing BoolBuilder can never throw. 182 | Replace with false instead and file a bug report to BoolBuilder authors \ 183 | with example code that reproduces this error message. 184 | """) 185 | public static func buildBlock() -> FinalResult { .success(false) } 186 | } 187 | 188 | /// Performs logical AND operations on the provided Boolean conditions 189 | /// with short-circuit semantics. 190 | /// 191 | /// - Parameter conditions: Conditions to check. 192 | /// - Throws: Re-throws the first error thrown by the conditions to check. 193 | /// - Returns: `true` if all the `conditions` are `true`, otherwise `false`. 194 | @_disfavoredOverload 195 | public func all( 196 | @ThrowingAndBuilder conditions makeResult: () throws 197 | -> ThrowingAndBuilder.FinalResult 198 | ) throws -> Bool { 199 | try makeResult().get() 200 | } 201 | 202 | /// Performs logical OR operations on the provided Boolean conditions 203 | /// with short-circuit semantics. 204 | /// 205 | /// - Parameter conditions: Conditions to check. 206 | /// - Throws: Re-throws the first error thrown by the conditions to check. 207 | /// - Returns: `true` if at least one of the `conditions` are `true`, 208 | /// otherwise `false`. 209 | @_disfavoredOverload 210 | public func any( 211 | @ThrowingOrBuilder conditions makeResult: () throws 212 | -> ThrowingOrBuilder.FinalResult 213 | ) throws -> Bool { 214 | try makeResult().get() 215 | } 216 | -------------------------------------------------------------------------------- /Tests/BoolBuilderTests/BoolBuilderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import BoolBuilder 3 | 4 | final class BoolBuilderTests: XCTestCase { 5 | enum MyError: Error { 6 | case yes 7 | case no 8 | } 9 | 10 | func testWarnEmptyBoolBuilder() { 11 | XCTAssertTrue(all { }) 12 | XCTAssertFalse(any { }) 13 | XCTAssertTrue(try all { }) 14 | XCTAssertFalse(try any { }) 15 | } 16 | 17 | func testExample() { 18 | let conditions: [(Bool, Bool, Bool, Bool, Bool)] = (0..<(1 << 5)).map { 19 | (($0 >> 4).isMultiple(of: 2), 20 | ($0 >> 3).isMultiple(of: 2), 21 | ($0 >> 2).isMultiple(of: 2), 22 | ($0 >> 1).isMultiple(of: 2), 23 | $0.isMultiple(of: 2)) 24 | } 25 | for ( 26 | conditionA, conditionB, conditionC, conditionD, conditionE 27 | ) in conditions { 28 | XCTAssertEqual( 29 | all { 30 | any { 31 | conditionA 32 | conditionB 33 | .inverted 34 | 35 | either { 36 | conditionC 37 | } or: { 38 | conditionD 39 | } 40 | } 41 | conditionE 42 | }, 43 | ( 44 | ( 45 | conditionA 46 | || 47 | !conditionB 48 | || 49 | ( 50 | conditionC 51 | != 52 | conditionD 53 | ) 54 | ) 55 | && 56 | conditionE 57 | ) 58 | ) 59 | } 60 | } 61 | 62 | func testAll() { 63 | XCTAssertTrue(all { true }) 64 | XCTAssertFalse(all { false }) 65 | XCTAssertTrue(all { 66 | true 67 | true 68 | }) 69 | XCTAssertFalse(all { 70 | true 71 | false 72 | }) 73 | XCTAssertFalse(all { 74 | false 75 | true 76 | }) 77 | XCTAssertFalse(all { 78 | false 79 | false 80 | }) 81 | } 82 | 83 | func testOr() { 84 | XCTAssertTrue(any { true }) 85 | XCTAssertFalse(any { false }) 86 | XCTAssertTrue(any { 87 | true 88 | true 89 | }) 90 | XCTAssertTrue(any { 91 | true 92 | false 93 | }) 94 | XCTAssertTrue(any { 95 | false 96 | true 97 | }) 98 | XCTAssertFalse(any { 99 | false 100 | false 101 | }) 102 | } 103 | 104 | func testNot() { 105 | XCTAssertTrue(false.inverted) 106 | XCTAssertFalse(true.inverted) 107 | } 108 | 109 | func testExclusiveOr() { 110 | XCTAssertFalse(either { 111 | true 112 | } or: { 113 | true 114 | }) 115 | XCTAssertTrue(either { 116 | true 117 | } or: { 118 | false 119 | }) 120 | XCTAssertTrue(either { 121 | false 122 | } or: { 123 | true 124 | }) 125 | XCTAssertFalse(either { 126 | false 127 | } or: { 128 | false 129 | }) 130 | XCTAssertTrue(either { 131 | either { 132 | true 133 | } or: { 134 | false 135 | } 136 | } or: { 137 | all { 138 | true 139 | false 140 | } 141 | }) 142 | } 143 | 144 | func testShortCircuit() { 145 | var counter = 0 146 | var incrementCounter: Bool { 147 | counter += 1 148 | return true 149 | } 150 | XCTAssertTrue(any { 151 | true 152 | incrementCounter 153 | }) 154 | XCTAssertEqual(counter, 0) 155 | XCTAssertTrue(any { 156 | false 157 | true 158 | incrementCounter 159 | }) 160 | XCTAssertEqual(counter, 0) 161 | XCTAssertFalse(all { 162 | false 163 | incrementCounter 164 | }) 165 | XCTAssertEqual(counter, 0) 166 | XCTAssertFalse(all { 167 | true 168 | false 169 | incrementCounter 170 | }) 171 | XCTAssertEqual(counter, 0) 172 | XCTAssertTrue(all { incrementCounter }) 173 | XCTAssertEqual(counter, 1) 174 | XCTAssertTrue(any { incrementCounter }) 175 | XCTAssertEqual(counter, 2) 176 | } 177 | 178 | func testThrowing() { 179 | func alwaysThrows() throws -> Bool { 180 | throw MyError.yes 181 | } 182 | func shouldNotHappen() throws -> Bool { 183 | throw MyError.no 184 | } 185 | func alwaysTrue() throws -> Bool { 186 | true 187 | } 188 | func alwaysFalse() throws -> Bool { 189 | false 190 | } 191 | 192 | XCTAssertTrue(try all { 193 | try alwaysTrue() 194 | }) 195 | XCTAssertTrue(try any { 196 | try alwaysTrue() 197 | }) 198 | XCTAssertFalse(try all { 199 | try alwaysFalse() 200 | }) 201 | XCTAssertFalse(try any { 202 | try alwaysFalse() 203 | }) 204 | XCTAssertThrowsError(try all { 205 | try alwaysThrows() 206 | }) 207 | XCTAssertThrowsError(try any { 208 | try alwaysThrows() 209 | }) 210 | XCTAssertThrowsError(try all { 211 | true 212 | try alwaysThrows() 213 | }) 214 | XCTAssertNoThrow(try all { 215 | false 216 | try alwaysThrows() 217 | }) 218 | XCTAssertThrowsError(try any { 219 | false 220 | try alwaysThrows() 221 | }) 222 | XCTAssertNoThrow(try any { 223 | true 224 | try alwaysThrows() 225 | }) 226 | XCTAssertThrowsError(try either { 227 | true 228 | } or: { 229 | try alwaysThrows() 230 | }) 231 | XCTAssertThrowsError(try either { 232 | try alwaysThrows() 233 | } or: { 234 | false 235 | }) 236 | 237 | XCTAssertThrowsError(try all { 238 | all { 239 | true 240 | } 241 | try either { 242 | try any { 243 | true 244 | try alwaysThrows() 245 | } 246 | } or: { 247 | try alwaysThrows() 248 | } 249 | }) 250 | 251 | XCTAssertThrowsError(try all { 252 | true 253 | try alwaysThrows() 254 | try shouldNotHappen() 255 | }) { 256 | XCTAssertEqual($0 as? MyError, MyError.yes) 257 | } 258 | XCTAssertThrowsError(try any { 259 | false 260 | try alwaysThrows() 261 | try shouldNotHappen() 262 | }) { 263 | XCTAssertEqual($0 as? MyError, MyError.yes) 264 | } 265 | 266 | XCTAssertThrowsError(try all { 267 | let result = try alwaysThrows() 268 | result 269 | }) 270 | XCTAssertThrowsError(try any { 271 | let result = try alwaysThrows() 272 | result 273 | }) 274 | } 275 | 276 | func testMixAndMatch() { 277 | func alwaysThrows() throws -> Bool { 278 | throw MyError.yes 279 | } 280 | func shouldNotHappen() throws -> Bool { 281 | throw MyError.no 282 | } 283 | 284 | XCTAssertTrue(all { 285 | true || false 286 | true != false 287 | }) 288 | XCTAssertFalse(any { 289 | true && false 290 | true == false 291 | }) 292 | XCTAssertFalse(try all { 293 | try false && alwaysThrows() 294 | try shouldNotHappen() 295 | }) 296 | XCTAssertTrue(try any { 297 | try true || alwaysThrows() 298 | try shouldNotHappen() 299 | }) 300 | } 301 | 302 | func testManyConditions() { 303 | XCTAssertTrue(all { 304 | true 305 | true 306 | true 307 | true 308 | true 309 | 310 | true 311 | true 312 | true 313 | true 314 | true 315 | 316 | true 317 | true 318 | true 319 | true 320 | true 321 | 322 | true 323 | true 324 | true 325 | true 326 | true 327 | }) 328 | XCTAssertFalse(any { 329 | false 330 | false 331 | false 332 | false 333 | false 334 | 335 | false 336 | false 337 | false 338 | false 339 | false 340 | 341 | false 342 | false 343 | false 344 | false 345 | false 346 | 347 | false 348 | false 349 | false 350 | false 351 | false 352 | }) 353 | 354 | func alwaysThrows() throws -> Bool { 355 | throw MyError.yes 356 | } 357 | XCTAssertThrowsError(try all { 358 | true 359 | true 360 | true 361 | true 362 | true 363 | 364 | true 365 | true 366 | true 367 | true 368 | true 369 | 370 | true 371 | true 372 | true 373 | true 374 | true 375 | 376 | true 377 | true 378 | true 379 | true 380 | try alwaysThrows() 381 | }) 382 | XCTAssertThrowsError(try any { 383 | false 384 | false 385 | false 386 | false 387 | false 388 | 389 | false 390 | false 391 | false 392 | false 393 | false 394 | 395 | false 396 | false 397 | false 398 | false 399 | false 400 | 401 | false 402 | false 403 | false 404 | false 405 | try alwaysThrows() 406 | }) 407 | } 408 | } 409 | --------------------------------------------------------------------------------