ModelInfoRetriever.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseCore
  15. import FirebaseInstallations
  16. import Foundation
  17. /// URL Session to use while retrieving model info.
  18. protocol ModelInfoRetrieverSession {
  19. func getModelInfo(with request: URLRequest,
  20. completion: @escaping (Data?, URLResponse?, Error?) -> Void)
  21. }
  22. /// Extension to customize data task requests.
  23. extension URLSession: ModelInfoRetrieverSession {
  24. func getModelInfo(with request: URLRequest,
  25. completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
  26. let task = dataTask(with: request) { data, response, error in
  27. completion(data, response, error)
  28. }
  29. task.resume()
  30. }
  31. }
  32. /// Model info result type.
  33. /// Downloading model info will return new model info only if it different from local model info.
  34. enum DownloadModelInfoResult {
  35. case modelInfo(RemoteModelInfo)
  36. case notModified
  37. }
  38. /// Model info response.
  39. private struct ModelInfoResponse: Codable {
  40. let downloadURL: String
  41. let urlExpiryTime: String
  42. let size: String
  43. /// Properties for server response keys.
  44. enum CodingKeys: String, CodingKey {
  45. case downloadURL = "downloadUri"
  46. case urlExpiryTime = "expireTime"
  47. case size = "sizeBytes"
  48. }
  49. }
  50. /// Fetch model info for a model from server.
  51. class ModelInfoRetriever {
  52. /// Model name.
  53. private let modelName: String
  54. /// Current Firebase app project ID.
  55. private let projectID: String
  56. /// Current Firebase app API key.
  57. private let apiKey: String
  58. /// Current Firebase app name.
  59. private let appName: String
  60. /// Auth token provider.
  61. typealias AuthTokenProvider = (_ completion: @escaping (Result<String, DownloadError>) -> Void)
  62. -> Void
  63. private let authTokenProvider: AuthTokenProvider
  64. /// URL session for model info request.
  65. private let session: ModelInfoRetrieverSession
  66. /// Local model info to validate model freshness.
  67. private let localModelInfo: LocalModelInfo?
  68. /// Telemetry logger.
  69. private let telemetryLogger: TelemetryLogger?
  70. /// Associate model info retriever with current Firebase app, and model name.
  71. init(modelName: String,
  72. projectID: String,
  73. apiKey: String,
  74. appName: String,
  75. authTokenProvider: @escaping AuthTokenProvider,
  76. session: ModelInfoRetrieverSession? = nil,
  77. localModelInfo: LocalModelInfo? = nil,
  78. telemetryLogger: TelemetryLogger? = nil) {
  79. self.modelName = modelName
  80. self.projectID = projectID
  81. self.apiKey = apiKey
  82. self.appName = appName
  83. self.authTokenProvider = authTokenProvider
  84. self.session = session ?? URLSession(configuration: .ephemeral)
  85. self.localModelInfo = localModelInfo
  86. self.telemetryLogger = telemetryLogger
  87. }
  88. /// Convenience init to use FirebaseInstallations as auth token provider.
  89. convenience init(modelName: String,
  90. projectID: String,
  91. apiKey: String,
  92. appName: String,
  93. installations: Installations,
  94. session: ModelInfoRetrieverSession? = nil,
  95. localModelInfo: LocalModelInfo? = nil,
  96. telemetryLogger: TelemetryLogger? = nil) {
  97. self.init(modelName: modelName,
  98. projectID: projectID,
  99. apiKey: apiKey,
  100. appName: appName,
  101. authTokenProvider: ModelInfoRetriever.authTokenProvider(installation: installations),
  102. session: session,
  103. localModelInfo: localModelInfo,
  104. telemetryLogger: telemetryLogger)
  105. }
  106. /// Auth token provider to validate credentials.
  107. private static func authTokenProvider(installation: Installations) -> AuthTokenProvider {
  108. return { completion in
  109. installation.authToken { tokenResult, error in
  110. guard let result = tokenResult
  111. else {
  112. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  113. .authTokenError)))
  114. return
  115. }
  116. completion(.success(result.authToken))
  117. }
  118. }
  119. }
  120. /// Get model info from server.
  121. func downloadModelInfo(completion: @escaping (Result<DownloadModelInfoResult, DownloadError>)
  122. -> Void) {
  123. authTokenProvider { result in
  124. switch result {
  125. // Successfully received FIS token.
  126. case let .success(authToken):
  127. DeviceLogger.logEvent(level: .debug,
  128. message: ModelInfoRetriever.DebugDescription
  129. .receivedAuthToken,
  130. messageCode: .validAuthToken)
  131. // Get model info fetch URL with appropriate HTTP headers.
  132. guard let request = self.getModelInfoFetchURLRequest(token: authToken) else {
  133. DeviceLogger.logEvent(level: .debug,
  134. message: ModelInfoRetriever.ErrorDescription
  135. .invalidModelInfoFetchURL,
  136. messageCode: .invalidModelInfoFetchURL)
  137. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  138. status: .modelInfoRetrievalFailed,
  139. model: CustomModel(name: self.modelName,
  140. size: 0,
  141. path: "",
  142. hash: ""),
  143. modelInfoErrorCode: .connectionFailed)
  144. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  145. .invalidModelInfoFetchURL)))
  146. return
  147. }
  148. // Download model info.
  149. self.session.getModelInfo(with: request) {
  150. data, response, error in
  151. if let downloadError = error {
  152. let description = ModelInfoRetriever.ErrorDescription
  153. .failedModelInfoRetrieval(downloadError.localizedDescription)
  154. DeviceLogger.logEvent(level: .debug,
  155. message: description,
  156. messageCode: .modelInfoRetrievalError)
  157. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  158. status: .modelInfoRetrievalFailed,
  159. model: CustomModel(
  160. name: self.modelName,
  161. size: 0,
  162. path: "",
  163. hash: ""
  164. ),
  165. modelInfoErrorCode: .connectionFailed)
  166. completion(.failure(.internalError(description: description)))
  167. } else {
  168. guard let httpResponse = response as? HTTPURLResponse else {
  169. DeviceLogger.logEvent(level: .debug,
  170. message: ModelInfoRetriever.ErrorDescription
  171. .invalidHTTPResponse,
  172. messageCode: .invalidHTTPResponse)
  173. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  174. status: .modelInfoRetrievalFailed,
  175. model: CustomModel(
  176. name: self.modelName,
  177. size: 0,
  178. path: "",
  179. hash: ""
  180. ),
  181. modelInfoErrorCode: .connectionFailed)
  182. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  183. .invalidHTTPResponse)))
  184. return
  185. }
  186. DeviceLogger.logEvent(level: .debug,
  187. message: ModelInfoRetriever.DebugDescription
  188. .receivedServerResponse,
  189. messageCode: .validHTTPResponse)
  190. switch httpResponse.statusCode {
  191. case 200:
  192. guard let modelHash = httpResponse
  193. .allHeaderFields[ModelInfoRetriever.etagHTTPHeader] as? String else {
  194. DeviceLogger.logEvent(level: .debug,
  195. message: ModelInfoRetriever.ErrorDescription.missingModelHash,
  196. messageCode: .missingModelHash)
  197. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  198. status: .modelInfoRetrievalFailed,
  199. model: CustomModel(
  200. name: self.modelName,
  201. size: 0,
  202. path: "",
  203. hash: ""
  204. ),
  205. modelInfoErrorCode: .noHash)
  206. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  207. .missingModelHash)))
  208. return
  209. }
  210. guard let data = data else {
  211. DeviceLogger.logEvent(level: .debug,
  212. message: ModelInfoRetriever.ErrorDescription
  213. .invalidHTTPResponse,
  214. messageCode: .invalidHTTPResponse)
  215. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  216. status: .modelInfoRetrievalFailed,
  217. model: CustomModel(
  218. name: self.modelName,
  219. size: 0,
  220. path: "",
  221. hash: ""
  222. ),
  223. modelInfoErrorCode: .unknown)
  224. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  225. .invalidHTTPResponse)))
  226. return
  227. }
  228. do {
  229. // Parse model info from HTTP response.
  230. let modelInfo = try self.getRemoteModelInfoFromResponse(data, modelHash: modelHash)
  231. DeviceLogger.logEvent(level: .debug,
  232. message: ModelInfoRetriever.DebugDescription
  233. .modelInfoDownloaded,
  234. messageCode: .modelInfoDownloaded)
  235. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  236. status: .modelInfoRetrievalSucceeded,
  237. model: CustomModel(
  238. name: self.modelName,
  239. size: modelInfo.size,
  240. path: "",
  241. hash: modelInfo.modelHash
  242. ),
  243. modelInfoErrorCode: .noError)
  244. completion(.success(.modelInfo(modelInfo)))
  245. } catch {
  246. let description = ModelInfoRetriever.ErrorDescription
  247. .invalidModelInfoJSON(error.localizedDescription)
  248. DeviceLogger.logEvent(level: .debug,
  249. message: description,
  250. messageCode: .invalidModelInfoJSON)
  251. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  252. status: .modelInfoRetrievalFailed,
  253. model: CustomModel(
  254. name: self.modelName,
  255. size: 0,
  256. path: "",
  257. hash: ""
  258. ),
  259. modelInfoErrorCode: .unknown)
  260. completion(
  261. .failure(.internalError(description: description))
  262. )
  263. }
  264. case 304:
  265. // For this case to occur, local model info has to already be available on device.
  266. guard let localInfo = self.localModelInfo else {
  267. DeviceLogger.logEvent(level: .debug,
  268. message: ModelInfoRetriever.ErrorDescription
  269. .unexpectedModelInfoDeletion,
  270. messageCode: .modelInfoDeleted)
  271. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  272. status: .modelInfoRetrievalFailed,
  273. model: CustomModel(
  274. name: self.modelName,
  275. size: 0,
  276. path: "",
  277. hash: ""
  278. ),
  279. modelInfoErrorCode: .unknown)
  280. completion(
  281. .failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  282. .unexpectedModelInfoDeletion))
  283. )
  284. return
  285. }
  286. guard let modelHash = httpResponse
  287. .allHeaderFields[ModelInfoRetriever.etagHTTPHeader] as? String else {
  288. DeviceLogger.logEvent(level: .debug,
  289. message: ModelInfoRetriever.ErrorDescription
  290. .missingModelHash,
  291. messageCode: .noModelHash)
  292. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  293. status: .modelInfoRetrievalFailed,
  294. model: CustomModel(
  295. name: self.modelName,
  296. size: 0,
  297. path: "",
  298. hash: ""
  299. ),
  300. modelInfoErrorCode: .noHash)
  301. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  302. .missingModelHash)))
  303. return
  304. }
  305. // Ensure that there is local model info on device with matching hash.
  306. guard modelHash == localInfo.modelHash else {
  307. DeviceLogger.logEvent(level: .debug,
  308. message: ModelInfoRetriever.ErrorDescription
  309. .modelHashMismatch,
  310. messageCode: .modelHashMismatchError)
  311. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  312. status: .modelInfoRetrievalFailed,
  313. model: CustomModel(
  314. name: self.modelName,
  315. size: 0,
  316. path: "",
  317. hash: modelHash
  318. ),
  319. modelInfoErrorCode: .hashMismatch)
  320. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  321. .modelHashMismatch)))
  322. return
  323. }
  324. DeviceLogger.logEvent(level: .debug,
  325. message: ModelInfoRetriever.DebugDescription
  326. .modelInfoUnmodified,
  327. messageCode: .modelInfoUnmodified)
  328. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  329. status: .modelInfoRetrievalSucceeded,
  330. model: CustomModel(
  331. name: self.modelName,
  332. size: localInfo.size,
  333. path: "",
  334. hash: localInfo.modelHash
  335. ),
  336. modelInfoErrorCode: .noError)
  337. completion(.success(.notModified))
  338. case 400:
  339. let errorMessage = self.getErrorFromResponse(data)
  340. let description = errorMessage ?? ModelInfoRetriever.ErrorDescription
  341. .invalidArgument(self.modelName)
  342. DeviceLogger.logEvent(level: .debug,
  343. message: description,
  344. messageCode: .invalidArgument)
  345. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  346. status: .modelInfoRetrievalFailed,
  347. model: CustomModel(
  348. name: self.modelName,
  349. size: 0,
  350. path: "",
  351. hash: ""
  352. ),
  353. modelInfoErrorCode: .httpError(code: httpResponse
  354. .statusCode))
  355. completion(.failure(.invalidArgument))
  356. case 401, 403:
  357. // Error could be due to FirebaseML API not enabled for project, or invalid
  358. // permissions.
  359. let errorMessage = self.getErrorFromResponse(data)
  360. let description = errorMessage ?? ModelInfoRetriever.ErrorDescription.permissionDenied
  361. DeviceLogger.logEvent(level: .debug,
  362. message: description,
  363. messageCode: .permissionDenied)
  364. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  365. status: .modelInfoRetrievalFailed,
  366. model: CustomModel(
  367. name: self.modelName,
  368. size: 0,
  369. path: "",
  370. hash: ""
  371. ),
  372. modelInfoErrorCode: .httpError(code: httpResponse
  373. .statusCode))
  374. completion(.failure(.permissionDenied))
  375. case 404:
  376. let errorMessage = self.getErrorFromResponse(data)
  377. let description = errorMessage ?? ModelInfoRetriever.ErrorDescription
  378. .modelNotFound(self.modelName)
  379. DeviceLogger.logEvent(level: .debug,
  380. message: description,
  381. messageCode: .modelNotFound)
  382. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  383. status: .modelInfoRetrievalFailed,
  384. model: CustomModel(
  385. name: self.modelName,
  386. size: 0,
  387. path: "",
  388. hash: ""
  389. ),
  390. modelInfoErrorCode: .httpError(code: httpResponse
  391. .statusCode))
  392. completion(.failure(.notFound))
  393. case 429:
  394. let errorMessage = self.getErrorFromResponse(data)
  395. let description = errorMessage ?? ModelInfoRetriever.ErrorDescription
  396. .resourceExhausted
  397. DeviceLogger.logEvent(level: .debug,
  398. message: description,
  399. messageCode: .resourceExhausted)
  400. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  401. status: .modelInfoRetrievalFailed,
  402. model: CustomModel(
  403. name: self.modelName,
  404. size: 0,
  405. path: "",
  406. hash: ""
  407. ),
  408. modelInfoErrorCode: .httpError(code: httpResponse
  409. .statusCode))
  410. completion(.failure(.resourceExhausted))
  411. default:
  412. let errorMessage = self.getErrorFromResponse(data)
  413. let description = errorMessage ?? ModelInfoRetriever.ErrorDescription
  414. .modelInfoRetrievalFailed(httpResponse.statusCode)
  415. DeviceLogger.logEvent(level: .debug,
  416. message: description,
  417. messageCode: .modelInfoRetrievalError)
  418. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  419. status: .modelInfoRetrievalFailed,
  420. model: CustomModel(
  421. name: self.modelName,
  422. size: 0,
  423. path: "",
  424. hash: ""
  425. ),
  426. modelInfoErrorCode: .httpError(code: httpResponse
  427. .statusCode))
  428. completion(.failure(.internalError(description: description)))
  429. }
  430. }
  431. }
  432. // Error retrieving auth token.
  433. case .failure:
  434. DeviceLogger.logEvent(level: .debug,
  435. message: ModelInfoRetriever.ErrorDescription
  436. .authTokenError,
  437. messageCode: .authTokenError)
  438. self.telemetryLogger?.logModelInfoRetrievalEvent(eventName: .modelDownload,
  439. status: .modelInfoRetrievalFailed,
  440. model: CustomModel(
  441. name: self.modelName,
  442. size: 0,
  443. path: "",
  444. hash: ""
  445. ),
  446. modelInfoErrorCode: .unknown)
  447. completion(.failure(.internalError(description: ModelInfoRetriever.ErrorDescription
  448. .authTokenError)))
  449. return
  450. }
  451. }
  452. }
  453. }
  454. /// Extension with helper methods to handle fetching model info from server.
  455. extension ModelInfoRetriever {
  456. /// HTTP request headers.
  457. private static let fisTokenHTTPHeader = "x-goog-firebase-installations-auth"
  458. private static let hashMatchHTTPHeader = "if-none-match"
  459. private static let bundleIDHTTPHeader = "x-ios-bundle-identifier"
  460. /// HTTP response headers.
  461. private static let etagHTTPHeader = "Etag"
  462. /// Construct model fetch base URL.
  463. var modelInfoFetchURL: URL? {
  464. var components = URLComponents()
  465. components.scheme = "https"
  466. components.host = "firebaseml.googleapis.com"
  467. components.path = "/v1beta2/projects/\(projectID)/models/\(modelName):download"
  468. components.queryItems = [URLQueryItem(name: "key", value: apiKey)]
  469. return components.url
  470. }
  471. /// Construct model fetch URL request.
  472. func getModelInfoFetchURLRequest(token: String) -> URLRequest? {
  473. guard let fetchURL = modelInfoFetchURL else { return nil }
  474. var request = URLRequest(url: fetchURL)
  475. request.httpMethod = "GET"
  476. let bundleID = Bundle.main.bundleIdentifier ?? ""
  477. request.setValue(bundleID, forHTTPHeaderField: ModelInfoRetriever.bundleIDHTTPHeader)
  478. request.setValue(token, forHTTPHeaderField: ModelInfoRetriever.fisTokenHTTPHeader)
  479. // Get model hash if local model info is available on device.
  480. if let modelInfo = localModelInfo {
  481. request.setValue(
  482. modelInfo.modelHash,
  483. forHTTPHeaderField: ModelInfoRetriever.hashMatchHTTPHeader
  484. )
  485. }
  486. return request
  487. }
  488. /// Parse error message from server response.
  489. private func getErrorFromResponse(_ data: Data?) -> String? {
  490. if let data = data,
  491. let responseJSON = try? JSONSerialization
  492. .jsonObject(with: data, options: []) as? [String: Any],
  493. let error = responseJSON["error"] as? [String: Any],
  494. let errorMessage = error["message"] as? String {
  495. return errorMessage
  496. }
  497. return nil
  498. }
  499. /// Parse date from string - used to get download URL expiry time.
  500. private static func getDateFromString(_ strDate: String) -> Date? {
  501. if #available(iOS 11, macOS 10.13, macCatalyst 13.0, tvOS 11.0, watchOS 4.0, *) {
  502. let dateFormatter = ISO8601DateFormatter()
  503. dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
  504. dateFormatter.formatOptions = [.withFractionalSeconds]
  505. return dateFormatter.date(from: strDate)
  506. } else {
  507. let dateFormatter = DateFormatter()
  508. dateFormatter.locale = Locale(identifier: "en-US_POSIX")
  509. dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
  510. dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
  511. return dateFormatter.date(from: strDate)
  512. }
  513. }
  514. /// Return model info created from server response.
  515. private func getRemoteModelInfoFromResponse(_ data: Data,
  516. modelHash: String) throws -> RemoteModelInfo {
  517. guard let modelInfoJSON = try? JSONDecoder().decode(ModelInfoResponse.self, from: data) else {
  518. throw DownloadError
  519. .internalError(description: ModelInfoRetriever.ErrorDescription.decodeModelInfoResponse)
  520. }
  521. guard let downloadURL = URL(string: modelInfoJSON.downloadURL) else {
  522. throw DownloadError
  523. .internalError(description: ModelInfoRetriever.ErrorDescription
  524. .invalidModelDownloadURL)
  525. }
  526. guard let size = Int(modelInfoJSON.size) else {
  527. throw DownloadError
  528. .internalError(description: ModelInfoRetriever.ErrorDescription
  529. .invalidModelSize)
  530. }
  531. guard let expiryTime = ModelInfoRetriever.getDateFromString(modelInfoJSON.urlExpiryTime) else {
  532. throw DownloadError
  533. .internalError(description: ModelInfoRetriever.ErrorDescription
  534. .invalidModelDownloadURLExpiryTime)
  535. }
  536. return RemoteModelInfo(
  537. name: modelName,
  538. downloadURL: downloadURL,
  539. modelHash: modelHash,
  540. size: size,
  541. urlExpiryTime: expiryTime
  542. )
  543. }
  544. }
  545. /// Model info retrieval error codes.
  546. enum ModelInfoErrorCode {
  547. case noError
  548. case noHash
  549. case httpError(code: Int)
  550. case connectionFailed
  551. case hashMismatch
  552. case unknown
  553. }
  554. /// Possible error messages for model info retrieval.
  555. extension ModelInfoRetriever {
  556. /// Debug descriptions.
  557. private enum DebugDescription {
  558. static let receivedAuthToken = "Generated valid auth token."
  559. static let receivedServerResponse = "Received a valid response from model info server."
  560. static let modelInfoDownloaded = "Successfully downloaded model info."
  561. static let modelInfoUnmodified = "Local model info matches the latest on server."
  562. }
  563. /// Error descriptions.
  564. private enum ErrorDescription {
  565. static let authTokenError = "Error retrieving auth token."
  566. static let invalidModelInfoFetchURL = "Unable to create URL to fetch model info."
  567. static let invalidHTTPResponse =
  568. "Could not get a valid HTTP response for model info retrieval."
  569. static let serverResponseError = { (errorCode: Int) in
  570. "Server returned with HTTP error code: \(errorCode)."
  571. }
  572. static let missingModelHash = "Model hash missing in model info server response."
  573. static let modelHashMismatch = "Unexpected model hash value."
  574. static let unexpectedModelInfoDeletion = "Model info was deleted unexpectedly."
  575. static let modelNotFound = { (name: String) in
  576. "No model found with name: \(name)"
  577. }
  578. static let invalidArgument = { (name: String) in
  579. "Invalid argument for model name: \(name)"
  580. }
  581. static let permissionDenied = "Invalid or missing permissions to retrieve model info."
  582. static let resourceExhausted = "Resource exhausted due to too many requests."
  583. static let modelInfoRetrievalFailed = { (code: Int) in
  584. "Model info retrieval failed with HTTP error code: \(code)"
  585. }
  586. static let decodeModelInfoResponse =
  587. "Unable to decode model info response from server."
  588. static let invalidModelDownloadURL =
  589. "Invalid model download URL from server."
  590. static let invalidModelSize = "Invalid model size from server."
  591. static let invalidModelDownloadURLExpiryTime =
  592. "Invalid download URL expiry time from server."
  593. static let invalidModelInfoJSON = { (error: String) in
  594. "Failed to parse model info: \(error)"
  595. }
  596. static let failedModelInfoRetrieval = { (error: String) in
  597. "Failed to retrieve model info: \(error)"
  598. }
  599. }
  600. }