TM-SGNL-iOS/SignalServiceKit/Util/ParamParser.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

193 lines
5.6 KiB
Swift

//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
// A DSL for parsing expected and optional values from a Dictionary, appropriate for
// validating a service response.
//
// Additionally it includes some helpers to DRY up common conversions.
//
// Rather than exhaustively enumerate accessors for types like `requireUInt32`, `requireInt64`, etc.
// We instead leverage generics at the call site.
//
// do {
// // Required
// let name: String = try paramParser.required(key: "name")
// let count: UInt32 = try paramParser.required(key: "count")
//
// // Optional
// let lastSeen: Date? = try paramParser.optional(key: "last_seen")
//
// return Foo(name: name, count: count, isNew: lastSeen == nil)
// } catch {
// handleInvalidResponse(error: error)
// }
//
public class ParamParser {
public typealias Key = String
let dictionary: [Key: Any]
public init(dictionary: [Key: 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)
var description: String {
switch self {
case .missingField(let key):
return "ParseError: Missing field for key \(key)!"
case .invalidType(let key, let expectedType, let actualType):
return "ParseError: Invalid type for key \(key)! Expected \(expectedType), found \(actualType)"
case .intValueOutOfBounds(let key):
return "ParseError: Int value was out of bounds for key \(key)!"
case .invalidUuidString(let key):
return "ParseError: Invalid UUID string for key \(key)!"
case .invalidBase64DataString(let key):
return "ParseError: Invalid base64 data string for key \(key)!"
}
}
}
private func badCast<T>(key: Key, expectedType: T.Type, castTarget: Any) -> ParseError {
return .invalidType(key, expectedType: expectedType, actualType: type(of: castTarget))
}
private func missing(key: Key) -> ParseError {
return ParseError.missingField(key)
}
// MARK: - Public API
public func required<T>(key: Key) throws -> T {
guard let value: T = try optional(key: key) else {
throw missing(key: key)
}
return value
}
public func optional<T>(key: Key) throws -> T? {
guard let someValue = dictionary[key] else {
return nil
}
guard !(someValue is NSNull) else {
return nil
}
guard let typedValue = someValue as? T else {
throw badCast(key: key, expectedType: T.self, castTarget: someValue)
}
return typedValue
}
public func hasKey(_ key: Key) -> Bool {
return dictionary[key] != nil && !(dictionary[key] is NSNull)
}
// MARK: FixedWidthIntegers (e.g. Int, Int32, UInt, UInt32, etc.)
// 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 {
guard let value: T = try optional(key: key) else {
throw missing(key: key)
}
return value
}
public func optional<T>(key: Key) throws -> T? where T: FixedWidthInteger {
guard let someValue: Any = try optional(key: key) else {
return nil
}
switch someValue {
case let typedValue as T:
return typedValue
case let int as Int:
guard int >= T.min, int <= T.max else {
throw ParseError.intValueOutOfBounds(key)
}
return T(int)
default:
throw badCast(key: key, expectedType: T.self, castTarget: someValue)
}
}
// MARK: UUIDs
public func required(key: Key) throws -> UUID {
guard let value: UUID = try optional(key: key) else {
throw missing(key: key)
}
return value
}
public func optional(key: Key) throws -> UUID? {
guard let uuidString: String = try optional(key: key) else {
return nil
}
guard let uuid = UUID(uuidString: uuidString) else {
throw ParseError.invalidUuidString(key)
}
return uuid
}
// MARK: Base64 Data
public func requiredBase64EncodedData(key: Key) throws -> Data {
guard let data: Data = try optionalBase64EncodedData(key: key) else {
throw ParseError.missingField(key)
}
return data
}
public func optionalBase64EncodedData(key: Key) throws -> Data? {
guard let encodedData: String = try self.optional(key: key) else {
return nil
}
guard let data = Data(base64Encoded: encodedData) else {
throw ParseError.invalidBase64DataString(key)
}
guard data.count > 0 else {
return nil
}
return data
}
}
extension ParamParser: CustomDebugStringConvertible {
public var debugDescription: String {
"<ParamParser: \(dictionary)>"
}
}