Skip to content

Commit de16a54

Browse files
Iron-HamTizianoCoroneo
authored andcommitted
Allow periods in arguments to be ignored when parsing cacheKeys (apollographql#2057)
* Allow commas in arguments to be ignored when parsing cacheKeys * Update * More tests * No-split case
1 parent 3e586d0 commit de16a54

4 files changed

Lines changed: 160 additions & 1 deletion

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/* Begin PBXBuildFile section */
1010
19E9F6AC26D58A9A003AB80E /* OperationMessageIdCreatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */; };
1111
19E9F6B526D6BF25003AB80E /* OperationMessageIdCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E9F6A826D5867E003AB80E /* OperationMessageIdCreator.swift */; };
12+
2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE7FFCF276802E30035DC39 /* CacheKeyConstructionTests.swift */; };
1213
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
1314
5AC6CA4322AAF7B200B7C94D /* GraphQLHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */; };
1415
5BB2C0232380836100774170 /* VersionNumberTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB2C0222380836100774170 /* VersionNumberTests.swift */; };
@@ -617,6 +618,7 @@
617618
/* Begin PBXFileReference section */
618619
19E9F6A826D5867E003AB80E /* OperationMessageIdCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageIdCreator.swift; sourceTree = "<group>"; };
619620
19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageIdCreatorTests.swift; sourceTree = "<group>"; };
621+
2EE7FFCF276802E30035DC39 /* CacheKeyConstructionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheKeyConstructionTests.swift; sourceTree = "<group>"; };
620622
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
621623
5AC6CA4222AAF7B200B7C94D /* GraphQLHTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLHTTPMethod.swift; sourceTree = "<group>"; };
622624
5BB2C0222380836100774170 /* VersionNumberTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNumberTests.swift; sourceTree = "<group>"; };
@@ -2225,6 +2227,7 @@
22252227
9F8622F71EC2004200C38162 /* ReadWriteFromStoreTests.swift */,
22262228
9FD03C2D25527CE6002227DC /* StoreConcurrencyTests.swift */,
22272229
9FA6ABCB1EC0A9F7000017BE /* WatchQueryTests.swift */,
2230+
2EE7FFCF276802E30035DC39 /* CacheKeyConstructionTests.swift */,
22282231
);
22292232
path = Cache;
22302233
sourceTree = "<group>";
@@ -3400,6 +3403,7 @@
34003403
9B21FD752422C29D00998B5C /* GraphQLFileTests.swift in Sources */,
34013404
DE2FCF4926E94D150057EA67 /* SelectionTests.swift in Sources */,
34023405
E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */,
3406+
2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */,
34033407
9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */,
34043408
DED45C2A2615319E0086EF63 /* DefaultInterceptorProviderTests.swift in Sources */,
34053409
9F21730E2567E6F000566121 /* DataLoaderTests.swift in Sources */,

Sources/ApolloSQLite/SQLiteNormalizedCache.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public final class SQLiteNormalizedCache {
3737
}
3838

3939
private func recordCacheKey(forFieldCacheKey fieldCacheKey: CacheKey) -> CacheKey {
40-
let components = fieldCacheKey.components(separatedBy: ".")
40+
let components = fieldCacheKey.splitIntoCacheKeyComponents()
4141
var updatedComponents = [String]()
4242
if components.first?.contains("_ROOT") == true {
4343
for component in components {
@@ -117,3 +117,44 @@ extension SQLiteNormalizedCache: NormalizedCache {
117117
try self.database.clearDatabase(shouldVacuumOnClear: self.shouldVacuumOnClear)
118118
}
119119
}
120+
121+
extension String {
122+
private var isBalanced: Bool {
123+
guard contains("(") || contains(")") else { return true }
124+
125+
var stack = [Character]()
126+
for character in self where ["(", ")"].contains(character) {
127+
if character == "(" {
128+
stack.append(character)
129+
} else if !stack.isEmpty && character == ")" {
130+
_ = stack.popLast()
131+
}
132+
}
133+
134+
return stack.isEmpty
135+
}
136+
137+
func splitIntoCacheKeyComponents() -> [String] {
138+
var result = [String]()
139+
var unbalancedString = ""
140+
let tmp = split(separator: ".", omittingEmptySubsequences: false)
141+
tmp
142+
.enumerated()
143+
.forEach { index, item in
144+
let value = String(item)
145+
if value.isBalanced && unbalancedString == "" {
146+
result.append(value)
147+
} else {
148+
unbalancedString += unbalancedString == "" ? value : ".\(value)"
149+
if unbalancedString.isBalanced {
150+
result.append(unbalancedString)
151+
unbalancedString = ""
152+
}
153+
}
154+
if unbalancedString != "" && index == tmp.count - 1 {
155+
result.append(unbalancedString)
156+
}
157+
}
158+
return result
159+
}
160+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import XCTest
2+
@testable import ApolloSQLite
3+
4+
final class CacheKeyConstructionTests: XCTestCase {
5+
func testCacheKeySplitsPeriods() {
6+
let input = "my.chemical.romance"
7+
let expected = ["my", "chemical", "romance"]
8+
9+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
10+
}
11+
12+
func testCacheKeySplitsPeriodsButIgnoresParentheses() {
13+
let input = "my.chemical.romance(xWv.CD-RIP.whole-album)"
14+
let expected = ["my", "chemical", "romance(xWv.CD-RIP.whole-album)"]
15+
16+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
17+
}
18+
19+
func testCacheKeyIgnoresNestedParentheses() {
20+
let input = "my.chemical.romance(the.(very)hidden.albums)"
21+
let expected = ["my", "chemical", "romance(the.(very)hidden.albums)"]
22+
23+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
24+
}
25+
26+
func testDoubleNestedInput() {
27+
let input = "my.chemical.romance(name:imnotokay.rip(xWv(the.original).HIGH-QUALITY)).mp3"
28+
let expected = ["my", "chemical", "romance(name:imnotokay.rip(xWv(the.original).HIGH-QUALITY))", "mp3"]
29+
30+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
31+
}
32+
33+
func testUnbalancedInput() {
34+
let input = "my.chemical.romance(name: )(.thebest.)()"
35+
let expected = ["my", "chemical", "romance(name: )(.thebest.)()"]
36+
37+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
38+
}
39+
40+
func testUnbalancedInputContinued() {
41+
let input = "my.chemical.romance(name: )(.thebest.)().count"
42+
let expected = ["my", "chemical", "romance(name: )(.thebest.)()", "count"]
43+
44+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
45+
}
46+
47+
func testNoSplits() {
48+
let input = "mychemicalromance"
49+
let expected = ["mychemicalromance"]
50+
51+
XCTAssertEqual(input.splitIntoCacheKeyComponents(), expected)
52+
}
53+
}

Tests/ApolloTests/Cache/SQLite/CachePersistenceTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,67 @@ class CachePersistenceTests: XCTestCase {
8080
}
8181
}
8282

83+
func testFetchAndPersistWithPeriodArguments() throws {
84+
let query = SearchQuery(term: "Luke.Skywalker")
85+
let sqliteFileURL = SQLiteTestCacheProvider.temporarySQLiteFileURL()
86+
87+
try SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { (cache) in
88+
let store = ApolloStore(cache: cache)
89+
90+
let server = MockGraphQLServer()
91+
let networkTransport = MockNetworkTransport(server: server, store: store)
92+
93+
let client = ApolloClient(networkTransport: networkTransport, store: store)
94+
95+
_ = server.expect(SearchQuery.self) { request in
96+
[
97+
"data": [
98+
"search": [
99+
[
100+
"id": "1000",
101+
"name": "Luke Skywalker",
102+
"__typename": "Human"
103+
]
104+
]
105+
]
106+
]
107+
}
108+
let networkExpectation = self.expectation(description: "Fetching query from network")
109+
let newCacheExpectation = self.expectation(description: "Fetch query from new cache")
110+
111+
client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in
112+
defer { networkExpectation.fulfill() }
113+
114+
switch outerResult {
115+
case .failure(let error):
116+
XCTFail("Unexpected error: \(error)")
117+
return
118+
case .success(let graphQLResult):
119+
XCTAssertEqual(graphQLResult.data?.search?.first??.asHuman?.name, "Luke Skywalker")
120+
// Do another fetch from cache to ensure that data is cached before creating new cache
121+
client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in
122+
try! SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { cache in
123+
let newStore = ApolloStore(cache: cache)
124+
let newClient = ApolloClient(networkTransport: networkTransport, store: newStore)
125+
newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in
126+
defer { newCacheExpectation.fulfill() }
127+
switch newClientResult {
128+
case .success(let newClientGraphQLResult):
129+
XCTAssertEqual(newClientGraphQLResult.data?.search?.first??.asHuman?.name, "Luke Skywalker")
130+
case .failure(let error):
131+
XCTFail("Unexpected error with new client: \(error)")
132+
}
133+
_ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
self.waitForExpectations(timeout: 2, handler: nil)
141+
}
142+
}
143+
83144
func testPassInConnectionDoesNotThrow() {
84145
do {
85146
let database = try SQLiteDotSwiftDatabase(connection: Connection())

0 commit comments

Comments
 (0)