mirror of https://github.com/oxen-io/session-ios
				
				
				
			Dry up Parameter parsing logic
							parent
							
								
									c2ed507d66
								
							
						
					
					
						commit
						bae2e8649d
					
				@ -1 +1 @@
 | 
			
		||||
Subproject commit 7c62088f5a59230b1d3435c12ab4273b97c594e9
 | 
			
		||||
Subproject commit 653107b632ab7b3e8449bfaad591ac950eae41ff
 | 
			
		||||
@ -0,0 +1,89 @@
 | 
			
		||||
//
 | 
			
		||||
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
import XCTest
 | 
			
		||||
 | 
			
		||||
class ParamParserTest: XCTestCase {
 | 
			
		||||
 | 
			
		||||
    override func setUp() {
 | 
			
		||||
        super.setUp()
 | 
			
		||||
        // Put setup code here. This method is called before the invocation of each test method in the class.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override func tearDown() {
 | 
			
		||||
        // Put teardown code here. This method is called after the invocation of each test method in the class.
 | 
			
		||||
        super.tearDown()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let dict: [String: Any] = ["some_int": 11, "some_string": "asdf", "large_int": Int64.max, "negative_int": -10]
 | 
			
		||||
    var parser: ParamParser {
 | 
			
		||||
        return ParamParser(dictionary: dict)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func testExample() {
 | 
			
		||||
        XCTAssertEqual(11, try parser.required(key: "some_int"))
 | 
			
		||||
        XCTAssertEqual(11, try parser.optional(key: "some_int"))
 | 
			
		||||
 | 
			
		||||
        let expectedString: String = "asdf"
 | 
			
		||||
        XCTAssertEqual(expectedString, try parser.required(key: "some_string"))
 | 
			
		||||
        XCTAssertEqual(expectedString, try parser.optional(key: "some_string"))
 | 
			
		||||
 | 
			
		||||
        XCTAssertEqual(nil, try parser.optional(key: "does_not_exist") as String?)
 | 
			
		||||
        XCTAssertThrowsError(try parser.required(key: "does_not_exist") as String)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func testNumeric() {
 | 
			
		||||
        let expectedInt32: Int32 = 11
 | 
			
		||||
        XCTAssertEqual(expectedInt32, try parser.required(key: "some_int"))
 | 
			
		||||
        XCTAssertEqual(expectedInt32, try parser.optional(key: "some_int"))
 | 
			
		||||
 | 
			
		||||
        let expectedInt64: Int64 = 11
 | 
			
		||||
        XCTAssertEqual(expectedInt64, try parser.required(key: "some_int"))
 | 
			
		||||
        XCTAssertEqual(expectedInt64, try parser.optional(key: "some_int"))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func testNumericSizeFailures() {
 | 
			
		||||
        XCTAssertThrowsError(try {
 | 
			
		||||
            let _: Int32 = try parser.required(key: "large_int")
 | 
			
		||||
        }())
 | 
			
		||||
 | 
			
		||||
        XCTAssertThrowsError(try {
 | 
			
		||||
            let _: Int32? = try parser.optional(key: "large_int")
 | 
			
		||||
        }())
 | 
			
		||||
 | 
			
		||||
        XCTAssertNoThrow(try {
 | 
			
		||||
            let _: Int64 = try parser.required(key: "large_int")
 | 
			
		||||
        }())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func testNumericSignFailures() {
 | 
			
		||||
        XCTAssertNoThrow(try {
 | 
			
		||||
            let _: Int = try parser.required(key: "negative_int")
 | 
			
		||||
        }())
 | 
			
		||||
 | 
			
		||||
        XCTAssertNoThrow(try {
 | 
			
		||||
            let _: Int64 = try parser.required(key: "negative_int")
 | 
			
		||||
        }())
 | 
			
		||||
 | 
			
		||||
        XCTAssertThrowsError(try {
 | 
			
		||||
            let _: UInt64 = try parser.required(key: "negative_int")
 | 
			
		||||
        }())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func testBase64Data() {
 | 
			
		||||
        let originalString = "asdf"
 | 
			
		||||
        let utf8Data: Data = originalString.data(using: .utf8)!
 | 
			
		||||
        let base64EncodedString = utf8Data.base64EncodedString()
 | 
			
		||||
 | 
			
		||||
        let dict: [String: Any] = ["some_data": base64EncodedString]
 | 
			
		||||
        let parser = ParamParser(dictionary: dict)
 | 
			
		||||
 | 
			
		||||
        XCTAssertEqual(utf8Data, try parser.requiredBase64EncodedData(key: "some_data"))
 | 
			
		||||
        XCTAssertEqual(utf8Data, try parser.optionalBase64EncodedData(key: "some_data"))
 | 
			
		||||
 | 
			
		||||
        let data: Data = try! parser.requiredBase64EncodedData(key: "some_data")
 | 
			
		||||
        let roundTripString = String(data: data, encoding: .utf8)
 | 
			
		||||
        XCTAssertEqual(originalString, roundTripString)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,126 @@
 | 
			
		||||
//
 | 
			
		||||
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
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 last_seen: 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 = AnyHashable
 | 
			
		||||
 | 
			
		||||
    let dictionary: Dictionary<Key, Any>
 | 
			
		||||
 | 
			
		||||
    public init(dictionary: Dictionary<Key, Any>) {
 | 
			
		||||
        self.dictionary = dictionary
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MARK: Errors
 | 
			
		||||
 | 
			
		||||
    public enum ParseError: Error {
 | 
			
		||||
        case missingField(Key)
 | 
			
		||||
        case invalidFormat(Key)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func invalid(key: Key) -> ParseError {
 | 
			
		||||
        return ParseError.invalidFormat(key)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 let typedValue = someValue as? T else {
 | 
			
		||||
            throw invalid(key: key)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return typedValue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MARK: FixedWidthIntegers (e.g. Int, Int32, UInt, UInt32, etc.)
 | 
			
		||||
 | 
			
		||||
    // You can't blindly cast accross 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 = dictionary[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 invalid(key: key)
 | 
			
		||||
            }
 | 
			
		||||
            return T(int)
 | 
			
		||||
        default:
 | 
			
		||||
            throw invalid(key: key)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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.invalidFormat(key)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue