Avoid Any?-typed responseBodyJson for all but one caller

This commit is contained in:
Sasha Weiss
2025-10-24 12:57:56 -07:00
committed by GitHub
parent 61a9b913e1
commit 7ddd3ca9fa
32 changed files with 144 additions and 230 deletions

View File

@@ -616,14 +616,10 @@ private class EmojiSearchIndex: NSObject {
throw OWSAssertionError("Bad response code for emoji manifest fetch")
}
guard let json = response.responseBodyJson as? [String: Any] else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Unable to generate JSON for emoji manifest from response body.")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Unable to parse emoji manifest from response body.")
}
let remoteVersion: Int = try parser.required(key: "version")
let remoteLocalizations: [String] = try parser.required(key: "languages")
if remoteVersion != searchIndexVersion {

View File

@@ -180,11 +180,11 @@ private extension RemoteMegaphoneFetcher {
method: .get
)
guard let responseJson = response.responseBodyJson else {
throw OWSAssertionError("Missing body JSON for manifest!")
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid body JSON for manifest!")
}
return try RemoteMegaphoneModel.Manifest.parseFrom(responseJson: responseJson)
return try RemoteMegaphoneModel.Manifest.parseFrom(parser: parser)
}
)
}
@@ -233,10 +233,10 @@ private extension RemoteMegaphoneFetcher {
}
Logger.info("Fetching remote megaphone translation")
let response = try await getUrlSession().performRequest(translationUrlPath, method: .get)
guard let responseJson = response.responseBodyJson else {
throw OWSAssertionError("Missing body JSON for translation!")
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid body JSON for translation!")
}
return try RemoteMegaphoneModel.Translation.parseFrom(responseJson: responseJson)
return try RemoteMegaphoneModel.Translation.parseFrom(parser: parser)
}
)
}
@@ -360,17 +360,11 @@ private extension RemoteMegaphoneModel.Manifest {
private static let secondaryCtaIdKey = "secondaryCtaId"
private static let secondaryCtaDataKey = "secondaryCtaData"
static func parseFrom(responseJson: Any?) throws -> [Self] {
guard let megaphonesArrayParser = ParamParser(responseObject: responseJson) else {
throw OWSAssertionError("Failed to create parser from response JSON!")
}
static func parseFrom(parser megaphonesArrayParser: ParamParser) throws -> [Self] {
let individualMegaphones: [[String: Any]] = try megaphonesArrayParser.required(key: Self.megaphonesKey)
return try individualMegaphones.compactMap { megaphoneObject throws -> Self? in
guard let megaphoneParser = ParamParser(responseObject: megaphoneObject) else {
throw OWSAssertionError("Failed to create parser from individual megaphone JSON!")
}
let megaphoneParser = ParamParser(megaphoneObject)
guard let iosMinVersion: String = try megaphoneParser.optional(key: Self.iosMinVersionKey) else {
return nil
@@ -442,11 +436,7 @@ private extension RemoteMegaphoneModel.Translation {
private static let primaryCtaTextKey = "primaryCtaText"
private static let secondaryCtaTextKey = "secondaryCtaText"
static func parseFrom(responseJson: Any?) throws -> Self {
guard let parser = ParamParser(responseObject: responseJson) else {
throw OWSAssertionError("Failed to create parser from response JSON!")
}
static func parseFrom(parser: ParamParser) throws -> Self {
let uuid: String = try parser.required(key: Self.uuidKey)
let imageUrl: String? = try parser.optional(key: Self.imageUrlKey)
let title: String = try parser.required(key: Self.titleKey)

View File

@@ -263,12 +263,9 @@ class BankTransferMandateViewController: OWSTableViewController2 {
let request = OWSRequestFactory.bankMandateRequest(bankTransferType: self.bankTransferType)
do {
let response = try await SSKEnvironment.shared.networkManagerRef.asyncRequest(request)
guard let json = response.responseBodyJson else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Failed to decode JSON response")
}
let mandateText: String = try parser.required(key: "mandate")
self.state = .loaded(mandateText: mandateText)
} catch {

View File

@@ -112,10 +112,9 @@ class BadgeIdsTest: XCTestCase {
}
}
class SubscriptionManagerDonationConfigurationTest: XCTestCase {
private typealias JSON = [String: Any]
private typealias DonationConfiguration = DonationSubscriptionConfiguration
// MARK: -
class DonationSubscriptionConfigurationTest: XCTestCase {
private enum CurrencyFixtures {
static let minimumAmount: Int = 5
@@ -137,8 +136,8 @@ class SubscriptionManagerDonationConfigurationTest: XCTestCase {
levelTwo: UInt? = LevelFixtures.levelTwo,
levelTwoAmount: Int = levelTwoAmount,
supportedPaymentMethods: [String] = supportedPaymentMethods
) -> JSON {
var result: JSON = [
) -> [String: Any] {
var result: [String: Any] = [
"minimum": minimumAmount,
"supportedPaymentMethods": supportedPaymentMethods
]
@@ -176,7 +175,7 @@ class SubscriptionManagerDonationConfigurationTest: XCTestCase {
}
private enum LevelFixtures {
private static let badgeJson: JSON = [
private static let badgeJson: [String: Any] = [
"id": "test-badge-1",
"category": "donor",
"name": "Test Badge 1",
@@ -198,7 +197,7 @@ class SubscriptionManagerDonationConfigurationTest: XCTestCase {
levelOne,
levelTwo
]
) -> JSON {
) -> [String: Any] {
levels.reduce(into: [:]) { partialResult, level in
partialResult["\(level)"] = [
"badge": badgeJson
@@ -207,11 +206,11 @@ class SubscriptionManagerDonationConfigurationTest: XCTestCase {
}
}
private enum DonationConfigurationFixtures {
private enum DonationSubscriptionConfigurationFixtures {
static func withDefaultValues(
currenciesJson: JSON = CurrencyFixtures.withDefaultValues(),
levelsJson: JSON = LevelFixtures.withDefaultValues()
) -> JSON {
currenciesJson: [String: Any] = CurrencyFixtures.withDefaultValues(),
levelsJson: [String: Any] = LevelFixtures.withDefaultValues()
) -> [String: Any] {
[
"sepaMaximumEuros": 10000,
"currencies": [
@@ -227,8 +226,8 @@ class SubscriptionManagerDonationConfigurationTest: XCTestCase {
}
func testParseValidDonationConfig() throws {
let config = try DonationConfiguration.from(
responseBodyJson: DonationConfigurationFixtures.withDefaultValues()
let config = try DonationSubscriptionConfiguration.from(
responseBodyDict: DonationSubscriptionConfigurationFixtures.withDefaultValues()
)
XCTAssertEqual(config.boost.level, LevelFixtures.boostLevel)
@@ -254,80 +253,80 @@ class SubscriptionManagerDonationConfigurationTest: XCTestCase {
}
func testParseConfigMissingThings() {
let missingBoost = DonationConfigurationFixtures.withDefaultValues(
let missingBoost = DonationSubscriptionConfigurationFixtures.withDefaultValues(
levelsJson: LevelFixtures.withDefaultValues(
levels: [LevelFixtures.giftLevel, LevelFixtures.levelOne, LevelFixtures.levelTwo]
)
)
let missingGift = DonationConfigurationFixtures.withDefaultValues(
let missingGift = DonationSubscriptionConfigurationFixtures.withDefaultValues(
levelsJson: LevelFixtures.withDefaultValues(
levels: [LevelFixtures.boostLevel, LevelFixtures.levelOne, LevelFixtures.levelTwo]
)
)
let missingBoostLevel = DonationConfigurationFixtures.withDefaultValues(
let missingBoostLevel = DonationSubscriptionConfigurationFixtures.withDefaultValues(
currenciesJson: CurrencyFixtures.withDefaultValues(
boostLevel: nil
)
)
let missingGiftLevel = DonationConfigurationFixtures.withDefaultValues(
let missingGiftLevel = DonationSubscriptionConfigurationFixtures.withDefaultValues(
currenciesJson: CurrencyFixtures.withDefaultValues(
giftLevel: nil
)
)
let missingSubscriptionLevel = DonationConfigurationFixtures.withDefaultValues(
let missingSubscriptionLevel = DonationSubscriptionConfigurationFixtures.withDefaultValues(
currenciesJson: CurrencyFixtures.withDefaultValues(
levelOne: nil
)
)
expect(
try DonationConfiguration.from(responseBodyJson: missingBoost),
try DonationSubscriptionConfiguration.from(responseBodyDict: missingBoost),
throwsParseError: .missingBoostBadge
)
expect(
try DonationConfiguration.from(responseBodyJson: missingGift),
try DonationSubscriptionConfiguration.from(responseBodyDict: missingGift),
throwsParseError: .missingGiftBadge
)
expect(
try DonationConfiguration.from(responseBodyJson: missingBoostLevel),
try DonationSubscriptionConfiguration.from(responseBodyDict: missingBoostLevel),
throwsParseError: .missingBoostPresetAmounts
)
expect(
try DonationConfiguration.from(responseBodyJson: missingGiftLevel),
try DonationSubscriptionConfiguration.from(responseBodyDict: missingGiftLevel),
throwsParseError: .missingGiftPresetAmount
)
expect(
try DonationConfiguration.from(responseBodyJson: missingSubscriptionLevel),
try DonationSubscriptionConfiguration.from(responseBodyDict: missingSubscriptionLevel),
throwsParseError: .missingAmountForLevel(LevelFixtures.levelOne)
)
}
func testParseConfigWithUnrecognizedPaymentMethod() throws {
let unexpectedPaymentMethod = DonationConfigurationFixtures.withDefaultValues(
let unexpectedPaymentMethod = DonationSubscriptionConfigurationFixtures.withDefaultValues(
currenciesJson: CurrencyFixtures.withDefaultValues(
supportedPaymentMethods: CurrencyFixtures.supportedPaymentMethods + ["cash money"]
)
)
_ = try DonationConfiguration.from(responseBodyJson: unexpectedPaymentMethod)
_ = try DonationSubscriptionConfiguration.from(responseBodyDict: unexpectedPaymentMethod)
}
// MARK: Utilities
private func expect(
_ expression: @autoclosure () throws -> DonationConfiguration,
throwsParseError expectedParseError: DonationConfiguration.ParseError
_ expression: @autoclosure () throws -> DonationSubscriptionConfiguration,
throwsParseError expectedParseError: DonationSubscriptionConfiguration.ParseError
) {
do {
let config = try expression()
XCTFail("Unexpectedly parsed successfully: \(config)")
} catch let error {
if
let parseError = error as? DonationConfiguration.ParseError,
let parseError = error as? DonationSubscriptionConfiguration.ParseError,
expectedParseError == parseError
{
return

View File

@@ -353,25 +353,22 @@ private enum DebugLogUploader {
private static func getUploadParameters(fileUrl: URL) async throws -> UploadParameters {
let url = URL(string: "https://debuglogs.org/")!
let response = try await buildOWSURLSession().performRequest(url.absoluteString, method: .get, ignoreAppExpiry: true)
guard let responseObject = response.responseBodyJson else {
guard let params = response.responseBodyParamParser else {
throw OWSAssertionError("Invalid response.")
}
guard let params = ParamParser(responseObject: responseObject) else {
throw OWSAssertionError("Invalid response: \(String(describing: responseObject))")
}
let uploadUrl: String = try params.required(key: "url")
let fieldMap: [String: String] = try params.required(key: "fields")
guard !fieldMap.isEmpty else {
throw OWSAssertionError("Invalid response: \(String(describing: responseObject))")
throw OWSAssertionError("Empty fieldMap!")
}
for (key, value) in fieldMap {
guard nil != key.nilIfEmpty,
nil != value.nilIfEmpty else {
throw OWSAssertionError("Invalid response: \(String(describing: responseObject))")
throw OWSAssertionError("Empty key or value in fieldMap!")
}
}
guard let rawUploadKey = fieldMap["key"]?.nilIfEmpty else {
throw OWSAssertionError("Invalid response: \(String(describing: responseObject))")
throw OWSAssertionError("Missing rawUploadKey!")
}
guard let fileExtension = (fileUrl.lastPathComponent as NSString).pathExtension.nilIfEmpty else {
throw OWSAssertionError("Invalid fileUrl: \(fileUrl)")

View File

@@ -31,12 +31,9 @@ struct PreKeyTaskAPIClientImpl: PreKeyTaskAPIClient {
let request = OWSRequestFactory.availablePreKeysCountRequest(for: identity)
let response = try await networkManager.asyncRequest(request)
guard let json = response.responseBodyJson else {
guard let params = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON.")
}
guard let params = ParamParser(responseObject: json) else {
throw OWSAssertionError("Missing or invalid response.")
}
let ecCount: Int = try params.required(key: "count")
let pqCount: Int = try params.optional(key: "pqCount") ?? 0

View File

@@ -356,7 +356,7 @@ extension RemoteMegaphoneModel.Manifest {
private static let snoozeDurationDaysId: String = "snoozeDurationDays"
public static func parse(fromJson jsonObject: [String: Any]) throws -> Self? {
let parser = ParamParser(dictionary: jsonObject)
let parser = ParamParser(jsonObject)
if let snoozeDurationDays: [UInt] = try parser.optional(key: snoozeDurationDaysId) {
return .snoozeDurationDays(days: snoozeDurationDays)

View File

@@ -1026,7 +1026,7 @@ public class OWSIdentityManagerImpl: OWSIdentityManager {
throw OWSAssertionError("Unexpected response from batch identity request \(response.responseStatusCode)")
}
guard let json = response.responseBodyJson, let responseDictionary = json as? [String: AnyObject] else {
guard let responseDictionary = response.responseBodyDict else {
throw OWSAssertionError("Missing or invalid JSON")
}

View File

@@ -515,8 +515,7 @@ public class SystemStoryManager: SystemStoryManagerProtocol {
) async throws -> [String] {
let response = try await urlSession.performRequest(Constants.manifestPath, method: .get)
guard
let json = response.responseBodyJson,
let responseDictionary = json as? [String: AnyObject],
let responseDictionary = response.responseBodyDict,
let version = responseDictionary[Constants.manifestVersionKey] as? String,
let languages = responseDictionary[Constants.manifestLanguagesKey] as? [String: AnyObject]
else {

View File

@@ -25,10 +25,6 @@ public enum RequestMakerUDAuthError: Int, Error, IsRetryableProvider {
public struct RequestMakerResult {
public let response: HTTPResponse
public let wasSentByUD: Bool
public var responseJson: Any? {
response.responseBodyJson
}
}
/// A utility class that handles:

View File

@@ -387,12 +387,9 @@ public class OWSUDManagerImpl: OWSUDManager {
.asyncRequest(certificateRequest)
let certificateData: Data = try {
guard let json = certificateResponse.responseBodyJson else {
guard let parser = certificateResponse.responseBodyParamParser else {
throw OWSUDError.invalidData(description: "Missing or invalid JSON")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSUDError.invalidData(description: "Invalid sender certificate response")
}
return try parser.requiredBase64EncodedData(key: "certificate")
}()

View File

@@ -61,11 +61,11 @@ public enum GiphyAPI {
throw OWSAssertionError("Invalid URL")
}
let response = try await urlSession.performRequest(request: request, ignoreAppExpiry: false)
guard let json = response.responseBodyJson else {
guard let responseDict = response.responseBodyDict else {
throw OWSAssertionError("Missing or invalid JSON")
}
Logger.info("Request succeeded.")
guard let imageInfos = self.parseGiphyImages(responseJson: json) else {
guard let imageInfos = self.parseGiphyImages(responseDict: responseDict) else {
throw OWSAssertionError("unable to parse trending images")
}
return imageInfos
@@ -77,15 +77,7 @@ public enum GiphyAPI {
// MARK: Parse API Responses
private static func parseGiphyImages(responseJson: Any?) -> [GiphyImageInfo]? {
guard let responseJson = responseJson else {
Logger.error("Missing response.")
return nil
}
guard let responseDict = responseJson as? [String: Any] else {
Logger.error("Invalid response.")
return nil
}
private static func parseGiphyImages(responseDict: [String: Any]) -> [GiphyImageInfo]? {
guard let imageDicts = responseDict["data"] as? [[String: Any]] else {
Logger.error("Invalid response data.")
return nil

View File

@@ -39,6 +39,14 @@ public struct HTTPResponse {
)
}
public var responseBodyParamParser: ParamParser? {
responseBodyDict.map { ParamParser($0) }
}
public var responseBodyDict: [String: Any]? {
responseBodyJson as? [String: Any]
}
public var responseBodyJson: Any? {
responseBodyData.flatMap { try? JSONSerialization.jsonObject(with: $0) }
}

View File

@@ -198,19 +198,6 @@ public extension Error {
return error.responseHeaders
}
var httpResponseJson: Any? {
guard let data = httpResponseData else {
return nil
}
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
return json
} catch {
owsFailDebug("Could not parse JSON: \(error).")
return nil
}
}
/// Does this error represent a transient networking issue?
///
/// a.k.a. "the internet gave up" (see also `isTimeout`)

View File

@@ -44,11 +44,7 @@ public extension RemoteAttestation {
public let username: String
public let password: String
public init(authParams: Any) throws {
guard let authParamsDict = authParams as? [String: Any] else {
throw attestationError(reason: "Invalid auth response.")
}
public init(authParamsDict: [String: Any]) throws {
guard let password = authParamsDict["password"] as? String, !password.isEmpty else {
throw attestationError(reason: "missing or empty password")
}
@@ -80,11 +76,11 @@ fileprivate extension RemoteAttestation.Auth {
HTTPUtils.logCurl(for: request)
#endif
guard let json = response.responseBodyJson else {
guard let authParamsDict = response.responseBodyDict else {
throw attestationError(reason: "Missing or invalid JSON")
}
return try RemoteAttestation.Auth(authParams: json)
return try RemoteAttestation.Auth(authParamsDict: authParamsDict)
}
}

View File

@@ -171,20 +171,15 @@ public class PaymentsCurrenciesImpl: PaymentsCurrenciesSwift, PaymentsCurrencies
let request = OWSRequestFactory.currencyConversionRequest()
let response = try await SSKEnvironment.shared.networkManagerRef.asyncRequest(request)
guard let json = response.responseBodyJson else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Invalid responseObject.")
}
let timestamp: UInt64 = try parser.required(key: "timestamp")
let serviceDate = Date(millisecondsSince1970: timestamp)
let currencyObjects: [Any] = try parser.required(key: "currencies")
let currencyObjects: [[String: Any]] = try parser.required(key: "currencies")
var conversionRateMap = ConversionRateMap()
for currencyObject in currencyObjects {
guard let currencyParser = ParamParser(responseObject: currencyObject) else {
throw OWSAssertionError("Invalid currencyObject.")
}
let currencyParser = ParamParser(currencyObject)
let base: String = try currencyParser.required(key: "base")
guard base == PaymentsConstants.mobileCoinCurrencyIdentifier else {
continue

View File

@@ -35,7 +35,7 @@ public class ProfileBadge: Codable, Equatable {
}
public init(jsonDictionary: [String: Any]) throws {
let params = ParamParser(dictionary: jsonDictionary)
let params = ParamParser(jsonDictionary)
id = try params.required(key: "id")
category = Category(rawValue: try params.required(key: "category"))

View File

@@ -122,9 +122,12 @@ public class ProfileFetcherJob {
)
do {
let response = try await makeRequest(versionedProfileRequest.request)
guard let params = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON!")
}
let profile = try SignalServiceProfile.fromResponse(
serviceId: serviceId,
responseObject: response.responseBodyJson
params: params,
)
await versionedProfiles.didFetchProfile(profile: profile, profileRequest: versionedProfileRequest)
@@ -171,9 +174,12 @@ public class ProfileFetcherJob {
)
}
guard let params = result.response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON!")
}
let profile = try SignalServiceProfile.fromResponse(
serviceId: serviceId,
responseObject: result.responseJson
params: params,
)
return FetchedProfile(profile: profile, profileKey: nil)

View File

@@ -70,11 +70,7 @@ public class SignalServiceProfile {
self.capabilities = capabilities
}
public static func fromResponse(serviceId: ServiceId, responseObject: Any?) throws -> SignalServiceProfile {
guard let params = ParamParser(responseObject: responseObject) else {
throw ValidationError(description: "Invalid response JSON!")
}
public static func fromResponse(serviceId: ServiceId, params: ParamParser) throws -> SignalServiceProfile {
do {
let identityKey = try IdentityKey(bytes: try params.requiredBase64EncodedData(key: "identityKey"))
let profileNameEncrypted = try params.optionalBase64EncodedData(key: "name")
@@ -112,7 +108,7 @@ public class SignalServiceProfile {
private static func parseBadges(params: ParamParser) throws -> [(OWSUserProfileBadgeInfo, ProfileBadge)] {
if let badgeArray: [[String: Any]] = try params.optional(key: "badges") {
return try badgeArray.compactMap { badgeDict in
let badgeParams = ParamParser(dictionary: badgeDict)
let badgeParams = ParamParser(badgeDict)
let isVisible: Bool? = try badgeParams.optional(key: "visible")
let expiration: TimeInterval? = try badgeParams.optional(key: "expiration")
let expirationMills = expiration.flatMap { UInt64($0 * 1000) }
@@ -132,16 +128,13 @@ public class SignalServiceProfile {
}
private static func parseCapabilities(params: ParamParser) throws -> Capabilities {
guard
let capabilitiesJson: Any? = try params.required(key: "capabilities"),
let capabilitiesParser = ParamParser(responseObject: capabilitiesJson)
else {
guard let capabilitiesDict: [String: Any] = try params.required(key: "capabilities") else {
throw ValidationError(description: "Missing or invalid capabilities JSON!")
}
return Capabilities(
dummyCapability: parseCapabilityFlag(
capabilitiesParser: capabilitiesParser,
capabilitiesParser: ParamParser(capabilitiesDict),
capabilityKey: Capabilities.dummyCapabilityKey
),
)

View File

@@ -530,8 +530,7 @@ public enum DonationSubscriptionManager {
}
guard
let json = httpResponse.responseBodyJson,
let parser = ParamParser(responseObject: json),
let parser = httpResponse.responseBodyParamParser,
let receiptCredentialResponseData = Data(
base64Encoded: (try parser.required(key: "receiptCredentialResponse") as String)
)
@@ -577,8 +576,9 @@ public enum DonationSubscriptionManager {
if
case .paymentFailed = errorCode,
let parser = ParamParser(responseObject: error.httpResponseJson),
let chargeFailureDict: [String: Any] = try? parser.optional(key: "chargeFailure"),
let httpResponseData = error.httpResponseData,
let httpResponseDict = try? JSONSerialization.jsonObject(with: httpResponseData) as? [String: Any],
let chargeFailureDict = httpResponseDict["chargeFailure"] as? [String: Any],
let chargeFailureCode = chargeFailureDict["code"] as? String
{
return KnownReceiptCredentialRequestError(

View File

@@ -24,7 +24,7 @@ public extension Paypal {
let response = try await SSKEnvironment.shared.networkManagerRef
.asyncRequest(createBoostRequest, retryPolicy: .hopefullyRecoverable)
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("[Donations] Failed to decode JSON response")
}
@@ -58,7 +58,7 @@ public extension Paypal {
let response = try await SSKEnvironment.shared.networkManagerRef
.asyncRequest(confirmOneTimePaymentRequest, retryPolicy: .hopefullyRecoverable)
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("[Donations] Failed to decode JSON response")
}
@@ -95,7 +95,7 @@ public extension Paypal {
let response = try await SSKEnvironment.shared.networkManagerRef
.asyncRequest(request, retryPolicy: .hopefullyRecoverable)
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("[Donations] Missing or invalid response.")
}

View File

@@ -21,9 +21,9 @@ extension Stripe {
/// Parse the redirect URL from a Stripe response. See [Stripe's docs][0].
///
/// [0]: https://stripe.com/docs/api/payment_intents/object#payment_intent_object-next_action-redirect_to_url-return_url
static func parseNextActionRedirectUrl(from responseBodyJson: Any?) -> URL? {
static func parseNextActionRedirectUrl(from responseDict: [String: Any]?) -> URL? {
if
let responseDict = responseBodyJson as? [String: Any?],
let responseDict,
let nextAction = responseDict["next_action"] as? [String: Any?],
let nextActionType = nextAction["type"] as? String,
nextActionType == "redirect_to_url",

View File

@@ -23,7 +23,7 @@ extension Stripe {
throw OWSAssertionError("Got bad response code \(statusCode).")
}
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid response.")
}

View File

@@ -64,12 +64,9 @@ public struct Stripe {
let response = try await SSKEnvironment.shared.networkManagerRef
.asyncRequest(request, retryPolicy: .hopefullyRecoverable)
guard let json = response.responseBodyJson else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Failed to decode JSON response")
}
return try PaymentIntent(
clientSecret: try parser.required(key: "clientSecret")
)
@@ -81,11 +78,8 @@ public struct Stripe {
) async throws -> PaymentMethodID {
do {
let response = try await requestPaymentMethod(with: paymentMethod)
guard let json = response.responseBodyJson else {
throw OWSAssertionError("Missing responseBodyJson")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Failed to decode JSON response")
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON!")
}
return try parser.required(key: "id")
} catch {
@@ -204,7 +198,7 @@ public struct Stripe {
return .init(
paymentIntentId: paymentIntentId,
paymentMethodId: paymentMethodId,
redirectToUrl: parseNextActionRedirectUrl(from: response.responseBodyJson)
redirectToUrl: parseNextActionRedirectUrl(from: response.responseBodyDict)
)
} catch {
throw convertToStripeErrorIfPossible(error)
@@ -231,17 +225,14 @@ public struct Stripe {
)
)
guard let json = response.responseBodyJson else {
throw OWSAssertionError("Missing responseBodyJson")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Failed to decode JSON response")
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON!")
}
let setupIntentId: String = try parser.required(key: "id")
return .init(
setupIntentId: setupIntentId,
paymentMethodId: paymentMethodId,
redirectToUrl: parseNextActionRedirectUrl(from: response.responseBodyJson)
redirectToUrl: parseNextActionRedirectUrl(from: response.responseBodyDict)
)
} catch {
throw convertToStripeErrorIfPossible(error)
@@ -346,11 +337,8 @@ fileprivate extension Stripe {
/// Step 3 of the process. Payment source tokenization
static func createToken(with tokenizationParameters: [String: any StripeQueryParamValue]) async throws -> Token {
let response = try await postForm(endpoint: "tokens", parameters: tokenizationParameters)
guard let json = response.responseBodyJson else {
throw OWSAssertionError("Missing responseBodyJson")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Failed to decode JSON response")
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON!")
}
return try parser.required(key: "id")
}
@@ -433,8 +421,9 @@ extension Dictionary<String, any StripeQueryParamValue>: StripeQueryParamValue {
extension Stripe {
private static func convertToStripeErrorIfPossible(_ error: Error) -> Error {
guard
let responseJson = error.httpResponseJson as? [String: Any],
let errorJson = responseJson["error"] as? [String: Any],
let responseData = error.httpResponseData,
let responseDict = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any],
let errorJson = responseDict["error"] as? [String: Any],
let code = errorJson["code"] as? String,
!code.isEmpty
else {

View File

@@ -268,18 +268,18 @@ public struct DonationSubscriptionConfiguration {
static func from(responseBodyData: Data) throws -> Self {
guard
let responseBodyJson = try? JSONSerialization
let responseBodyDict = try? JSONSerialization
.jsonObject(with: responseBodyData) as? [String: Any]
else {
throw OWSAssertionError("Failed to get dictionary from body data!")
}
return try .from(responseBodyJson: responseBodyJson)
return try .from(responseBodyDict: responseBodyDict)
}
/// Parse a service configuration from a response body.
static func from(responseBodyJson: [String: Any]) throws -> Self {
let parser = ParamParser(dictionary: responseBodyJson)
static func from(responseBodyDict: [String: Any]) throws -> Self {
let parser = ParamParser(responseBodyDict)
let levels: BadgedLevels = try parseLevels(fromParser: parser)
let presetsByCurrency: PresetsByCurrency = try parsePresets(fromParser: parser, forLevels: levels)
@@ -401,7 +401,7 @@ public struct DonationSubscriptionConfiguration {
throw ParseError.invalidBadgeLevel(levelString: levelString)
}
let levelParser = ParamParser(dictionary: json)
let levelParser = ParamParser(json)
partialResult[level] = BadgedLevel(
value: level,
@@ -500,7 +500,7 @@ public struct DonationSubscriptionConfiguration {
forCurrency code: Currency.Code,
withLevels levels: BadgedLevels
) throws -> Presets {
let parser = ParamParser(dictionary: json)
let parser = ParamParser(json)
let (boostPresets, giftPreset) = try parseOneTimePresets(
fromParser: parser,

View File

@@ -28,10 +28,7 @@ public struct SubscriptionFetcher {
switch response.responseStatusCode {
case 200:
guard
let responseJson = response.responseBodyJson,
let parser = ParamParser(responseObject: responseJson)
else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid response body!")
}
@@ -78,7 +75,7 @@ public struct Subscription: Equatable {
public let code: String?
init(jsonDictionary: [String: Any]) {
code = try? ParamParser(dictionary: jsonDictionary).optional(key: "code")
code = try? ParamParser(jsonDictionary).optional(key: "code")
}
}
@@ -154,7 +151,7 @@ public struct Subscription: Equatable {
}
public init(subscriptionDict: [String: Any], chargeFailureDict: [String: Any]?) throws {
let params = ParamParser(dictionary: subscriptionDict)
let params = ParamParser(subscriptionDict)
level = try params.required(key: "level")
let currencyCode: Currency.Code = try {
let raw: String = try params.required(key: "currency")

View File

@@ -36,7 +36,7 @@ public class UsernameApiClientImpl: UsernameApiClient {
)
}
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError(
"Unexpectedly missing JSON response body!"
)
@@ -100,7 +100,7 @@ public class UsernameApiClientImpl: UsernameApiClient {
throw OWSAssertionError("Unexpected status code from successful request: \(response.responseStatusCode)")
}
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Unexpectedly missing JSON response body!")
}
@@ -170,7 +170,7 @@ public class UsernameApiClientImpl: UsernameApiClient {
throw OWSAssertionError("Unexpected response code: \(response.responseStatusCode)")
}
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Unexpectedly missing JSON response body!")
}
@@ -187,7 +187,7 @@ public class UsernameApiClientImpl: UsernameApiClient {
throw OWSAssertionError("Unexpected response code: \(response.responseStatusCode)")
}
guard let parser = ParamParser(responseObject: response.responseBodyJson) else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Unexpectedly missing JSON response body!")
}

View File

@@ -27,30 +27,20 @@ import Foundation
// }
//
public class ParamParser {
public typealias Key = String
private let dictionary: [String: Any]
let dictionary: [Key: Any]
public init(dictionary: [Key: Any]) {
public init(_ dictionary: [String: Any]) {
self.dictionary = dictionary
}
public convenience init?(responseObject: Any?) {
guard let responseDict = responseObject as? [String: AnyObject] else {
return nil
}
self.init(dictionary: responseDict)
}
// MARK: Errors
private enum ParseError: Error, CustomStringConvertible {
case missingField(Key)
case invalidType(Key, expectedType: Any.Type, actualType: Any.Type)
case intValueOutOfBounds(Key)
case invalidUuidString(Key)
case invalidBase64DataString(Key)
case missingField(String)
case invalidType(String, expectedType: Any.Type, actualType: Any.Type)
case intValueOutOfBounds(String)
case invalidUuidString(String)
case invalidBase64DataString(String)
var description: String {
switch self {
@@ -68,17 +58,17 @@ public class ParamParser {
}
}
private func badCast<T>(key: Key, expectedType: T.Type, castTarget: Any) -> ParseError {
private func badCast<T>(key: String, expectedType: T.Type, castTarget: Any) -> ParseError {
return .invalidType(key, expectedType: expectedType, actualType: type(of: castTarget))
}
private func missing(key: Key) -> ParseError {
private func missing(key: String) -> ParseError {
return ParseError.missingField(key)
}
// MARK: - Public API
public func required<T>(key: Key) throws -> T {
public func required<T>(key: String) throws -> T {
guard let value: T = try optional(key: key) else {
throw missing(key: key)
}
@@ -86,7 +76,7 @@ public class ParamParser {
return value
}
public func optional<T>(key: Key) throws -> T? {
public func optional<T>(key: String) throws -> T? {
guard let someValue = dictionary[key] else {
return nil
}
@@ -102,7 +92,7 @@ public class ParamParser {
return typedValue
}
public func hasKey(_ key: Key) -> Bool {
public func hasString(_ key: String) -> Bool {
return dictionary[key] != nil && !(dictionary[key] is NSNull)
}
@@ -111,7 +101,7 @@ public class ParamParser {
// You can't blindly cast across Integer types, so we need to specify and validate which Int type we want.
// In general, you'll find numeric types parsed into a Dictionary as `Int`.
public func required<T>(key: Key) throws -> T where T: FixedWidthInteger {
public func required<T>(key: String) throws -> T where T: FixedWidthInteger {
guard let value: T = try optional(key: key) else {
throw missing(key: key)
}
@@ -119,7 +109,7 @@ public class ParamParser {
return value
}
public func optional<T>(key: Key) throws -> T? where T: FixedWidthInteger {
public func optional<T>(key: String) throws -> T? where T: FixedWidthInteger {
guard let someValue: Any = try optional(key: key) else {
return nil
}
@@ -139,7 +129,7 @@ public class ParamParser {
// MARK: UUIDs
public func required(key: Key) throws -> UUID {
public func required(key: String) throws -> UUID {
guard let value: UUID = try optional(key: key) else {
throw missing(key: key)
}
@@ -147,7 +137,7 @@ public class ParamParser {
return value
}
public func optional(key: Key) throws -> UUID? {
public func optional(key: String) throws -> UUID? {
guard let uuidString: String = try optional(key: key) else {
return nil
}
@@ -161,7 +151,7 @@ public class ParamParser {
// MARK: Base64 Data
public func requiredBase64EncodedData(key: Key) throws -> Data {
public func requiredBase64EncodedData(key: String) throws -> Data {
guard let data: Data = try optionalBase64EncodedData(key: key) else {
throw ParseError.missingField(key)
}
@@ -169,7 +159,7 @@ public class ParamParser {
return data
}
public func optionalBase64EncodedData(key: Key) throws -> Data? {
public func optionalBase64EncodedData(key: String) throws -> Data? {
guard let encodedData: String = try self.optional(key: key) else {
return nil
}

View File

@@ -697,12 +697,9 @@ public struct StorageService {
let response = try await SSKEnvironment.shared.networkManagerRef.asyncRequest(request)
guard let json = response.responseBodyJson else {
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON.")
}
guard let parser = ParamParser(responseObject: json) else {
throw OWSAssertionError("Missing or invalid response.")
}
let username: String = try parser.required(key: "username")
let password: String = try parser.required(key: "password")

View File

@@ -10,9 +10,8 @@ final class StripeTest: XCTestCase {
private let unknownCurrency = "ZZZ"
func testParseNextActionRedirectUrl() {
let notFoundTestCases: [Any?] = [
let notFoundTestCases: [[String: Any]?] = [
nil,
"https://example.com",
["next_action": "https://example.com"],
[
"next_action": [

View File

@@ -10,7 +10,7 @@ import SignalServiceKit
class ParamParserTest: XCTestCase {
let dict: [String: Any] = ["some_int": 11, "some_string": "asdf", "large_int": Int64.max, "negative_int": -10]
var parser: ParamParser {
return ParamParser(dictionary: dict)
return ParamParser(dict)
}
func testExample() {
@@ -74,7 +74,7 @@ class ParamParserTest: XCTestCase {
func testUUID() {
let uuid = UUID()
let parser = ParamParser(dictionary: ["uuid": uuid.uuidString])
let parser = ParamParser(["uuid": uuid.uuidString])
XCTAssertEqual(uuid, try parser.required(key: "uuid"))
XCTAssertEqual(uuid, try parser.optional(key: "uuid"))
@@ -85,17 +85,17 @@ class ParamParserTest: XCTestCase {
func testUUIDFormatFailure() {
XCTAssertThrowsError(try {
let parser = ParamParser(dictionary: ["uuid": ""])
let parser = ParamParser(["uuid": ""])
let _: UUID = try parser.required(key: "uuid")
}())
XCTAssertThrowsError(try {
let parser = ParamParser(dictionary: ["uuid": "not-a-uuid"])
let parser = ParamParser(["uuid": "not-a-uuid"])
let _: UUID = try parser.required(key: "uuid")
}())
XCTAssertThrowsError(try {
let parser = ParamParser(dictionary: ["uuid": 0])
let parser = ParamParser(["uuid": 0])
let _: UUID = try parser.required(key: "uuid")
}())
}
@@ -108,7 +108,7 @@ class ParamParserTest: XCTestCase {
let base64EncodedString = utf8Data.base64EncodedString()
let dict: [String: Any] = ["some_data": base64EncodedString]
let parser = ParamParser(dictionary: dict)
let parser = ParamParser(dict)
XCTAssertEqual(utf8Data, try parser.requiredBase64EncodedData(key: "some_data"))
XCTAssertEqual(utf8Data, try parser.optionalBase64EncodedData(key: "some_data"))
@@ -120,7 +120,7 @@ class ParamParserTest: XCTestCase {
func testBase64Data_EmptyString() {
let dict: [String: Any] = ["some_data": ""]
let parser = ParamParser(dictionary: dict)
let parser = ParamParser(dict)
XCTAssertThrowsError(try parser.requiredBase64EncodedData(key: "some_data"))
XCTAssertEqual(nil, try parser.optionalBase64EncodedData(key: "some_data"))
@@ -128,7 +128,7 @@ class ParamParserTest: XCTestCase {
func testBase64Data_NSNull() {
let dict: [String: Any] = ["some_data": NSNull()]
let parser = ParamParser(dictionary: dict)
let parser = ParamParser(dict)
XCTAssertThrowsError(try parser.requiredBase64EncodedData(key: "some_data"))
XCTAssertEqual(nil, try parser.optionalBase64EncodedData(key: "some_data"))
@@ -139,7 +139,7 @@ class ParamParserTest: XCTestCase {
let base64EncodedString = "YXNkZg"
let dict: [String: Any] = ["some_data": base64EncodedString]
let parser = ParamParser(dictionary: dict)
let parser = ParamParser(dict)
XCTAssertThrowsError(try parser.requiredBase64EncodedData(key: "some_data"))
XCTAssertThrowsError(try parser.optionalBase64EncodedData(key: "some_data"))

View File

@@ -109,10 +109,7 @@ public class MobileCoinAPI {
try Self.buildAccount(forPaymentsEntropy: paymentsEntropy)
}
private static func parseAuthorizationResponse(responseObject: Any?) throws -> OWSAuthorization {
guard let params = ParamParser(responseObject: responseObject) else {
throw OWSAssertionError("Invalid responseObject.")
}
private static func parseAuthorizationResponse(params: ParamParser) throws -> OWSAuthorization {
let username: String = try params.required(key: "username")
let password: String = try params.required(key: "password")
return OWSAuthorization(username: username, password: password)
@@ -124,10 +121,10 @@ public class MobileCoinAPI {
}
let request = OWSRequestFactory.paymentsAuthenticationCredentialRequest()
let response = try await SSKEnvironment.shared.networkManagerRef.asyncRequest(request)
guard let json = response.responseBodyJson else {
guard let params = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid JSON")
}
let signalAuthorization = try Self.parseAuthorizationResponse(responseObject: json)
let signalAuthorization = try Self.parseAuthorizationResponse(params: params)
let localAccount = try Self.buildAccount(forPaymentsEntropy: paymentsEntropy)
let client = try localAccount.buildClient(signalAuthorization: signalAuthorization)
return try MobileCoinAPI(paymentsEntropy: paymentsEntropy, localAccount: localAccount, client: client)