mirror of https://github.com/oxen-io/session-ios
Merge branch 'mkirk/dry-up-param-parsing'
commit
2c8a64a48f
@ -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