|
| 1 | +import Foundation |
| 2 | +#if !COCOAPODS |
| 3 | +import ApolloAPI |
| 4 | +#endif |
| 5 | + |
| 6 | +enum FieldPolicyResult { |
| 7 | + case single(CacheKeyInfo) |
| 8 | + case list([CacheKeyInfo]) |
| 9 | +} |
| 10 | + |
| 11 | +struct FieldPolicyDirectiveEvaluator { |
| 12 | + let field: Selection.Field |
| 13 | + let fieldPolicy: Selection.FieldPolicy |
| 14 | + let arguments: [String: InputValue] |
| 15 | + let variables: GraphQLOperation.Variables? |
| 16 | + |
| 17 | + init?( |
| 18 | + field: Selection.Field, |
| 19 | + variables: GraphQLOperation.Variables? |
| 20 | + ) { |
| 21 | + self.field = field |
| 22 | + self.variables = variables |
| 23 | + |
| 24 | + guard let fieldPolicy = field.fieldPolicy else { |
| 25 | + return nil |
| 26 | + } |
| 27 | + self.fieldPolicy = fieldPolicy |
| 28 | + |
| 29 | + guard let arguments = field.arguments else { |
| 30 | + return nil |
| 31 | + } |
| 32 | + self.arguments = arguments |
| 33 | + } |
| 34 | + |
| 35 | + func resolveFieldPolicy() -> FieldPolicyResult? { |
| 36 | + let keyArgs = parseKeyArgs(for: fieldPolicy) |
| 37 | + |
| 38 | + var singleValueArgs = [String?](repeating: nil, count: keyArgs.count) |
| 39 | + var listArgIndex: Int? = nil |
| 40 | + var listArgValues: [String] = [] |
| 41 | + |
| 42 | + for (index, arg) in keyArgs.enumerated() { |
| 43 | + guard let argVal = arguments[arg.name] else { |
| 44 | + return nil |
| 45 | + } |
| 46 | + |
| 47 | + guard let resolved = argVal.resolveValue( |
| 48 | + keyPath: arg.path, |
| 49 | + variables: variables |
| 50 | + ), !resolved.isEmpty else { |
| 51 | + return nil |
| 52 | + } |
| 53 | + |
| 54 | + if resolved.count > 1 { |
| 55 | + listArgIndex = index |
| 56 | + listArgValues = resolved |
| 57 | + } else { |
| 58 | + guard let value = resolved.first else { |
| 59 | + return nil |
| 60 | + } |
| 61 | + singleValueArgs[index] = value |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + if let listArgIndex = listArgIndex { |
| 66 | + guard let cacheKeyList = processKeyList( |
| 67 | + listArgIndex: listArgIndex, |
| 68 | + listArgValues: listArgValues, |
| 69 | + singleValueArgs: singleValueArgs, |
| 70 | + keyArgs: keyArgs |
| 71 | + ) else { |
| 72 | + return nil |
| 73 | + } |
| 74 | + return .list(cacheKeyList) |
| 75 | + } else { |
| 76 | + let parts = singleValueArgs.compactMap { $0 } |
| 77 | + return .single(CacheKeyInfo(id: parts.joined(separator: "+"))) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + private func processKeyList( |
| 82 | + listArgIndex: Int, |
| 83 | + listArgValues: [String], |
| 84 | + singleValueArgs: [String?], |
| 85 | + keyArgs: [ParsedKey] |
| 86 | + ) -> [CacheKeyInfo]? { |
| 87 | + var keys: [CacheKeyInfo] = [] |
| 88 | + keys.reserveCapacity(listArgValues.count) |
| 89 | + for value in listArgValues { |
| 90 | + var parts = [String]() |
| 91 | + parts.reserveCapacity(keyArgs.count) |
| 92 | + for keyIndex in 0..<keyArgs.count { |
| 93 | + if keyIndex == listArgIndex { |
| 94 | + parts.append(value) |
| 95 | + } else if let singleValue = singleValueArgs[keyIndex] { |
| 96 | + parts.append(singleValue) |
| 97 | + } else { |
| 98 | + return nil |
| 99 | + } |
| 100 | + } |
| 101 | + keys.append(CacheKeyInfo(id: parts.joined(separator: "+"))) |
| 102 | + } |
| 103 | + return keys |
| 104 | + } |
| 105 | + |
| 106 | + private func parseKeyArgs(for fieldPolicy: Selection.FieldPolicy) -> [ParsedKey] { |
| 107 | + fieldPolicy.keyArgs.map { key in |
| 108 | + if let dot = key.firstIndex(of: ".") { |
| 109 | + let name = String(key[..<dot]) |
| 110 | + let rest = key[key.index(after: dot)...] |
| 111 | + return ParsedKey(name: name, path: rest.split(separator: ".").map(String.init)) |
| 112 | + } else { |
| 113 | + return ParsedKey(name: key, path: nil) |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + private struct ParsedKey { |
| 119 | + let name: String |
| 120 | + let path: [String]? |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +extension ScalarType { |
| 125 | + fileprivate var cacheKeyComponentStringValue: String { |
| 126 | + switch self { |
| 127 | + case let strVal as String: |
| 128 | + return strVal |
| 129 | + |
| 130 | + case let boolVal as Bool: |
| 131 | + return boolVal ? "true" : "false" |
| 132 | + |
| 133 | + case let intVal as Int: |
| 134 | + return String(intVal) |
| 135 | + |
| 136 | + case let doubleVal as Double: |
| 137 | + return String(doubleVal) |
| 138 | + |
| 139 | + case let floatVal as Float: |
| 140 | + return String(floatVal) |
| 141 | + |
| 142 | + default: |
| 143 | + return String(describing: self) |
| 144 | + } |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +extension JSONValue { |
| 149 | + fileprivate func cacheKeyComponentStringValue(keyPath: [String]? = nil) -> [String]? { |
| 150 | + switch self { |
| 151 | + case let strVal as String: |
| 152 | + return [strVal] |
| 153 | + |
| 154 | + case let boolVal as Bool: |
| 155 | + return boolVal ? ["true"] : ["false"] |
| 156 | + |
| 157 | + case let intVal as Int: |
| 158 | + return [String(intVal)] |
| 159 | + |
| 160 | + case let doubleVal as Double: |
| 161 | + return [String(doubleVal)] |
| 162 | + |
| 163 | + case let floatVal as Float: |
| 164 | + return [String(floatVal)] |
| 165 | + |
| 166 | + case let arrVal as [JSONValue]: |
| 167 | + let values: [String] = arrVal.compactMap { $0.cacheKeyComponentStringValue()?.first } |
| 168 | + guard !values.isEmpty else { return nil } |
| 169 | + return values |
| 170 | + |
| 171 | + case let objVal as JSONObject: |
| 172 | + guard let keyPath, !keyPath.isEmpty else { return nil } |
| 173 | + guard let targetValue = objVal.traverse(to: keyPath[...]) else { return nil } |
| 174 | + return targetValue.cacheKeyComponentStringValue() |
| 175 | + |
| 176 | + default: |
| 177 | + return [String(describing: self)] |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +extension JSONObject { |
| 183 | + fileprivate func traverse( |
| 184 | + to path: ArraySlice<String> |
| 185 | + ) -> JSONValue? { |
| 186 | + guard let head = path.first else { return self } |
| 187 | + guard let next = self[head] else { return nil } |
| 188 | + if path.count == 1 { return next } |
| 189 | + if let nested = next as? JSONObject { |
| 190 | + return nested.traverse(to: path.dropFirst()) |
| 191 | + } |
| 192 | + return nil |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +extension InputValue { |
| 197 | + fileprivate func resolveValue(keyPath: [String]? = nil, variables: [String: (any GraphQLOperationVariableValue)]? = nil) -> [String]? { |
| 198 | + switch self { |
| 199 | + case .scalar(let scalar): |
| 200 | + return [scalar.cacheKeyComponentStringValue] |
| 201 | + case .variable(let varName): |
| 202 | + guard let varValue = variables?[varName] else { |
| 203 | + return nil |
| 204 | + } |
| 205 | + return varValue._jsonEncodableValue?._jsonValue.cacheKeyComponentStringValue(keyPath: keyPath) |
| 206 | + case .list(let list): |
| 207 | + if list.contains(where: { if case .list = $0 { return true } else { return false } }) { |
| 208 | + return nil |
| 209 | + } |
| 210 | + let values = list.compactMap { $0.resolveValue()?.first } |
| 211 | + guard !values.isEmpty else { return nil } |
| 212 | + return values |
| 213 | + case .object(let dict): |
| 214 | + guard let keyPath, !keyPath.isEmpty else { return nil } |
| 215 | + guard let targetValue = self.traverse(through: dict, to: keyPath[...]) else { return nil } |
| 216 | + return targetValue.resolveValue() |
| 217 | + default: |
| 218 | + return nil |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + fileprivate func traverse( |
| 223 | + through dict: [String: InputValue], |
| 224 | + to path: ArraySlice<String> |
| 225 | + ) -> InputValue? { |
| 226 | + guard let head = path.first else { return .object(dict) } |
| 227 | + guard let next = dict[head] else { return nil } |
| 228 | + if path.count == 1 { return next } |
| 229 | + if case .object(let nested) = next { |
| 230 | + return traverse(through: nested, to: path.dropFirst()) |
| 231 | + } |
| 232 | + return nil |
| 233 | + } |
| 234 | +} |
0 commit comments