Skip to content

Commit 2b544ec

Browse files
authored
Merge pull request #17 from ABridoux/develop
Develop
2 parents 2a48125 + 4308175 commit 2b544ec

File tree

10 files changed

+104
-35
lines changed

10 files changed

+104
-35
lines changed
File renamed without changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// SafeFetching
3+
// Copyright © 2021-present Alexis Bridoux.
4+
// MIT license, see LICENSE file for details
5+
6+
import CoreData
7+
8+
extension NSPredicate {
9+
10+
/// Safely build a predicate for a provided entity type
11+
public static func safe<Entity: NSManagedObject>(_ predicate: Builders.Predicate<Entity>) -> NSPredicate {
12+
predicate.nsValue
13+
}
14+
}

Sources/SafeFetching/Predicate/Declarations/PredicateRightValue+String.swift

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@
55

66
public extension Builders.KeyPathPredicateRightValue where Value == String? {
77

8-
static func hasPrefix(_ prefix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
9-
.init { .init(keyPath: $0, operatorString: "BEGINSWITH", value: prefix) }
8+
static func hasPrefix(_ prefix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
9+
.init { .init(keyPath: $0, operatorString: options.transformOperator("BEGINSWITH"), value: prefix) }
1010
}
1111

12-
static func hasNoPrefix(_ prefix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
13-
.init { .init(keyPath: $0, operatorString: "BEGINSWITH", value: prefix, isInverted: true) }
12+
static func hasNoPrefix(_ prefix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
13+
.init { .init(keyPath: $0, operatorString: options.transformOperator("BEGINSWITH"), value: prefix, isInverted: true) }
1414
}
1515

16-
static func hasSuffix(_ suffix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
17-
.init { .init(keyPath: $0, operatorString: "ENDSWITH", value: suffix) }
16+
static func hasSuffix(_ suffix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
17+
.init { .init(keyPath: $0, operatorString: options.transformOperator("ENDSWITH"), value: suffix) }
1818
}
1919

20-
static func hasNoSuffix(_ suffix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
21-
.init { .init(keyPath: $0, operatorString: "ENDSWITH", value: suffix, isInverted: true) }
20+
static func hasNoSuffix(_ suffix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
21+
.init { .init(keyPath: $0, operatorString: options.transformOperator("ENDSWITH"), value: suffix, isInverted: true) }
2222
}
2323

24-
static func contains(_ other: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
25-
.init { .init(keyPath: $0, operatorString: "CONTAINS", value: other) }
24+
static func contains(_ other: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
25+
.init { .init(keyPath: $0, operatorString: options.transformOperator("CONTAINS"), value: other) }
2626
}
2727

28-
static func doesNotContain(_ other: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
29-
.init { .init(keyPath: $0, operatorString: "CONTAINS", value: other, isInverted: true) }
28+
static func doesNotContain(_ other: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
29+
.init { .init(keyPath: $0, operatorString: options.transformOperator("CONTAINS"), value: other, isInverted: true) }
3030
}
3131

3232
static func matches(_ pattern: Builders.RegularExpressionPattern) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
@@ -40,28 +40,28 @@ public extension Builders.KeyPathPredicateRightValue where Value == String? {
4040

4141
public extension Builders.KeyPathPredicateRightValue where Value == String {
4242

43-
static func hasPrefix(_ prefix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
44-
.init { .init(keyPath: $0, operatorString: "BEGINSWITH", value: prefix) }
43+
static func hasPrefix(_ prefix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
44+
.init { .init(keyPath: $0, operatorString: options.transformOperator("BEGINSWITH"), value: prefix) }
4545
}
4646

47-
static func hasNoPrefix(_ prefix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
48-
.init { .init(keyPath: $0, operatorString: "BEGINSWITH", value: prefix, isInverted: true) }
47+
static func hasNoPrefix(_ prefix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
48+
.init { .init(keyPath: $0, operatorString: options.transformOperator("BEGINSWITH"), value: prefix, isInverted: true) }
4949
}
5050

51-
static func hasSuffix(_ suffix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
52-
.init { .init(keyPath: $0, operatorString: "ENDSWITH", value: suffix) }
51+
static func hasSuffix(_ suffix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
52+
.init { .init(keyPath: $0, operatorString: options.transformOperator("ENDSWITH"), value: suffix) }
5353
}
5454

55-
static func hasNoSuffix(_ suffix: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
56-
.init { .init(keyPath: $0, operatorString: "ENDSWITH", value: suffix, isInverted: true) }
55+
static func hasNoSuffix(_ suffix: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
56+
.init { .init(keyPath: $0, operatorString: options.transformOperator("ENDSWITH"), value: suffix, isInverted: true) }
5757
}
5858

59-
static func contains(_ other: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
60-
.init { .init(keyPath: $0, operatorString: "CONTAINS", value: other) }
59+
static func contains(_ other: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
60+
.init { .init(keyPath: $0, operatorString: options.transformOperator("CONTAINS"), value: other) }
6161
}
6262

63-
static func doesNotContain(_ other: String) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
64-
.init { .init(keyPath: $0, operatorString: "CONTAINS", value: other, isInverted: true) }
63+
static func doesNotContain(_ other: String, options: Builders.StringComparisonOptions? = nil) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {
64+
.init { .init(keyPath: $0, operatorString: options.transformOperator("CONTAINS"), value: other, isInverted: true) }
6565
}
6666

6767
static func matches(_ pattern: Builders.RegularExpressionPattern) -> Builders.KeyPathPredicateRightValue<Entity, Value, String> {

Sources/SafeFetching/Predicate/Declarations/StringKeyPredicateRightValue+OptionSet.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ public extension Builders.StringKeyPathPredicateRightValue where Value: OptionSe
88
/// - important: Should be used only with options set types that are converted from/to a primitive value
99
static func intersects(_ value: Value) -> Builders.StringKeyPathPredicateRightValue<Entity, Value, Value> {
1010
.init { keyPathString in
11-
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) == \($0)" }
11+
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) == \(value.testValue)" }
1212
}
1313
}
1414

1515
/// - important: Should be used only with options set types that are converted from/to a primitive value
1616
static func doesNotIntersect(_ value: Value) -> Builders.StringKeyPathPredicateRightValue<Entity, Value, Value> {
1717
.init { keyPathString in
18-
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) != \($0)" }
18+
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) != \(value.testValue)" }
1919
}
2020
}
2121
}
@@ -26,15 +26,15 @@ public extension Builders.StringKeyPathPredicateRightValue {
2626
static func intersects<W: OptionSet & DatabaseTestValue>(_ value: W) -> Builders.StringKeyPathPredicateRightValue<Entity, Value, Value>
2727
where Value == W?, W.RawValue: BinaryInteger {
2828
.init { keyPathString in
29-
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) == \($0)" }
29+
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) == \(value.testValue)" }
3030
}
3131
}
3232

3333
/// - important: Should be used only with options set types that are converted from/to a primitive value
3434
static func doesNotIntersect<W: OptionSet & DatabaseTestValue>(_ value: W) -> Builders.StringKeyPathPredicateRightValue<Entity, Value, Value>
3535
where Value == W?, W.RawValue: BinaryInteger {
3636
.init { keyPathString in
37-
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) != \($0)" }
37+
.init(keyPathString: keyPathString.key) { "\($0) & \(value.testValue) != \(value.testValue)" }
3838
}
3939
}
4040
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// SafeFetching
3+
// Copyright © 2021-present Alexis Bridoux.
4+
// MIT license, see LICENSE file for details
5+
6+
extension Builders {
7+
8+
/// Available options to compare strings
9+
///
10+
/// - [Reference](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/Articles/pSyntax.html#//apple_ref/doc/uid/TP40001795-215868)
11+
/// - [CoreData - objc.io](https://www.objc.io/books/core-data/) (for the normalized option - chapter "Efficient Searching")
12+
public enum StringComparisonOptions {
13+
14+
/// Diacritic insensitive
15+
case diacriticInsensitive
16+
17+
/// Case insensitive
18+
case caseInsensitive
19+
20+
/// Diacritic and case insensitive
21+
case diacriticAndCaseInsensitive
22+
23+
/// Specify that the field is already normalized
24+
case normalized
25+
}
26+
}
27+
28+
extension Builders.StringComparisonOptions {
29+
30+
var stringValue: String {
31+
switch self {
32+
case .diacriticInsensitive: return "d"
33+
case .caseInsensitive: return "c"
34+
case .diacriticAndCaseInsensitive: return "cd"
35+
case .normalized: return "n"
36+
}
37+
}
38+
}
39+
40+
extension Optional where Wrapped == Builders.StringComparisonOptions {
41+
42+
/// If the option is not nil, add the flag to the operator. Otherwise return unchanged operator.
43+
func transformOperator(_ operatorString: String) -> String {
44+
switch self {
45+
case .none:
46+
return operatorString
47+
case let .some(wrapped):
48+
return "\(wrapped)[\(wrapped.stringValue)]"
49+
}
50+
}
51+
}

Sources/SafeFetching/Request/RequestBuilder+BuildingSteps.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public extension Builders.PreRequest where Step == CreationStep {
2828

2929
/// Stops after a first element is fetched
3030
func first() -> Builders.Request<Entity, TargetStep, Fetched?> {
31-
Builders.Request<Entity, TargetStep, Fetched?>(request: request)
31+
request.fetchLimit = 1
32+
return Builders.Request<Entity, TargetStep, Fetched?>(request: request)
3233
}
3334

3435
/// Stops after the first nth elements are fetched

Sources/SafeFetching/SafeFetching.docc/Articles/build-requests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ StubEntity.request()
7979

8080
### Predicate
8181

82-
Specify a predicate with the ``Builders/Request/where(_:)-6r3wg`` function after the target.
82+
Specify a predicate with the ``Builders/Request/where(_:)-5ar9o`` function after the target.
8383

8484
```swift
8585
StubEntity.request()

Sources/SafeFetching/SafeFetching.docc/SafeFetching.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This library offers a DSL (Domain Specific Language) to build predicates and req
99
## Topics
1010

1111
### Build predicates
12+
Predicate are used within a `where()` function when building a request. Meanwhile, a convenience function `safe(_:)` is provided to create a `NSPredicate` from a SafeFetching predicate.
1213

1314
- <doc:build-predicates>
1415
- ``Builders/Predicate``
@@ -31,7 +32,7 @@ Key paths predicate work with key paths that target a property exposed to Object
3132

3233
### Key path advanced predicates
3334

34-
Advanced predicates like string comparison ``Builders/KeyPathPredicateRightValue/hasPrefix(_:)-3w8p3`` or membership ``Builders/KeyPathPredicateRightValue/isIn(_:)-8lnwn`` can be used with the special operator `*`.
35+
Advanced predicates like string comparison ``Builders/KeyPathPredicateRightValue/hasPrefix(_:options:)-8tv6z`` or membership ``Builders/KeyPathPredicateRightValue/isIn(_:)-8lnwn`` can be used with the special operator `*`.
3536

3637
- ``*(_:_:)-9ve4y``
3738

Tests/SafeFetchingTests/BooleanPredicateBuilderTests.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import SafeFetching
77
import XCTest
8+
import CoreData
89

910
final class BooleanPredicateTests: XCTestCase {
1011

@@ -56,10 +57,10 @@ final class BooleanPredicateTests: XCTestCase {
5657
}
5758

5859
func testOptionSet() {
59-
testNSFormat(predicate: .stubOption * .intersects([.foo, .bar]), expecting: "stubOption & 3 == stubOption")
60-
testNSFormat(predicate: .stubOption * .doesNotIntersect([.foo, .bar]), expecting: "stubOption & 3 != stubOption")
61-
testNSFormat(predicate: .stubForcedOption * .intersects([.foo, .bar]), expecting: "stubForcedOption & 3 == stubForcedOption")
62-
testNSFormat(predicate: .stubForcedOption * .doesNotIntersect([.foo, .bar]), expecting: "stubForcedOption & 3 != stubForcedOption")
60+
testNSFormat(predicate: .stubOption * .intersects([.foo, .bar]), expecting: "stubOption & 3 == 3")
61+
testNSFormat(predicate: .stubOption * .doesNotIntersect([.foo, .bar]), expecting: "stubOption & 3 != 3")
62+
testNSFormat(predicate: .stubForcedOption * .intersects([.foo, .bar]), expecting: "stubForcedOption & 3 == 3")
63+
testNSFormat(predicate: .stubForcedOption * .doesNotIntersect([.foo, .bar]), expecting: "stubForcedOption & 3 != 3")
6364
}
6465

6566
func testStringKeyPath_DatabaseValue() {

Tests/SafeFetchingTests/RequestBuilderTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import SafeFetching
77
import XCTest
8+
import CoreData
89

910
final class RequestBuilderTests: XCTestCase {
1011

0 commit comments

Comments
 (0)