diff --git a/Sources/ExtrasJSON/JSONValue.swift b/Sources/ExtrasJSON/JSONValue.swift index af6d851..7cdb99e 100644 --- a/Sources/ExtrasJSON/JSONValue.swift +++ b/Sources/ExtrasJSON/JSONValue.swift @@ -143,6 +143,59 @@ extension JSONValue { } } +/// Value Getters +public extension JSONValue { + /// If this `JSONValue` is a `.number` that can be losslessly represented as a `Double`, return it as a `Double`. + /// Otherwise, return nil. + var doubleValue: Double? { + guard case .number(let n) = self else { + return nil + } + return Double(n) + } + + /// If this `JSONValue` is a `.number` that can be losslessly represented as an `Int`, return it as a `Int`. + /// Otherwise, return nil. + var intValue: Int? { + guard case .number(let n) = self else { + return nil + } + return Int(n) + } + + /// If this `JSONValue` is a `.string`, return it as a `String`. Otherwise, return nil. + var stringValue: String? { + guard case .string(let s) = self else { + return nil + } + return s + } + + /// If this `JSONValue` is a `.bool`, return it as a `Bool`. Otherwise, return nil. + var boolValue: Bool? { + guard case .bool(let b) = self else { + return nil + } + return b + } + + /// If this `JSONValue` is a `.array`, return it as a `[JSONValue]`. Otherwise, return nil. + var arrayValue: [JSONValue]? { + guard case .array(let a) = self else { + return nil + } + return a + } + + /// If this `JSONValue` is a `.object`, return it as a `[String: JSONValue]`. Otherwise, return nil. + var objectValue: [String: JSONValue]? { + guard case .object(let o) = self else { + return nil + } + return o + } +} + extension JSONValue { var debugDataTypeDescription: String { switch self { @@ -217,3 +270,96 @@ public func == (lhs: JSONValue, rhs: JSONValue) -> Bool { return false } } + +extension JSONValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { + self = .number(String(value)) + } +} + +extension JSONValue: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .number(String(value)) + } +} + +extension JSONValue: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension JSONValue: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self = .bool(value) + } +} + +extension JSONValue: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: JSONValue...) { + self = .array(elements) + } +} + +extension JSONValue: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, JSONValue)...) { + self = .object(Dictionary(uniqueKeysWithValues: elements)) + } +} + +extension JSONValue: Codable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .number(let n): + if let int = self.intValue { + try container.encode(int) + } else if let double = self.doubleValue { + try container.encode(double) + } else { + throw EncodingError.invalidValue( + self, + EncodingError.Context( + codingPath: container.codingPath, + debugDescription: "Could not encode \"\(n)\" as a number" + ) + ) + } + case .string(let s): + try container.encode(s) + case .bool(let b): + try container.encode(b) + case .array(let a): + try container.encode(a) + case .object(let o): + try container.encode(o) + case .null: + try container.encodeNil() + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let int = try? container.decode(Int.self) { + self = .number(String(int)) + } else if let double = try? container.decode(Double.self) { + self = .number(String(double)) + } else if let string = try? container.decode(String.self) { + self = .string(string) + } else if let bool = try? container.decode(Bool.self) { + self = .bool(bool) + } else if let object = try? container.decode([String: JSONValue].self) { + self = .object(object) + } else if let array = try? container.decode([JSONValue].self) { + self = .array(array) + } else { + throw DecodingError.typeMismatch( + JSONValue.self, + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "No valid JSON type could be decoded from the provided input." + ) + ) + } + } +} diff --git a/Tests/ExtrasJSONTests/JSONValueExtensionsTests.swift b/Tests/ExtrasJSONTests/JSONValueExtensionsTests.swift new file mode 100644 index 0000000..4200db9 --- /dev/null +++ b/Tests/ExtrasJSONTests/JSONValueExtensionsTests.swift @@ -0,0 +1,58 @@ +import ExtrasJSON +import XCTest + +class JSONValueDecodingTests: XCTestCase { + let encoder = XJSONEncoder() + let decoder = XJSONDecoder() + + func testInteger() throws { + let intJSON: JSONValue = 12 + let encoded = try encoder.encode(intJSON) + XCTAssertEqual(Int(String(bytes: encoded, encoding: .utf8)!), intJSON.intValue) + + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded.intValue, intJSON.intValue) + } + + func testDouble() throws { + let doubleJSON: JSONValue = 12.3 + let encoded = try encoder.encode(doubleJSON) + XCTAssertEqual(Double(String(bytes: encoded, encoding: .utf8)!)!, doubleJSON.doubleValue!, accuracy: 0.0001) + + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded.doubleValue!, doubleJSON.doubleValue!, accuracy: 0.0001) + } + + func testString() throws { + let stringJSON: JSONValue = "I am a String" + let encoded = try encoder.encode(stringJSON) + XCTAssertEqual(String(bytes: encoded, encoding: .utf8), "\"I am a String\"") + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, stringJSON) + } + + func testBool() throws { + let boolJSON: JSONValue = true + let encoded = try encoder.encode(boolJSON) + XCTAssertEqual(String(bytes: encoded, encoding: .utf8), "true") + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(decoded, boolJSON) + } + + func testArray() throws { + let arrayJSON: JSONValue = ["I am a string in an array"] + let encoded = try encoder.encode(arrayJSON) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(String(bytes: encoded, encoding: .utf8), "[\"I am a string in an array\"]") + XCTAssertEqual(decoded, arrayJSON) + } + + func testObject() throws { + let objectJSON: JSONValue = ["Key": "Value"] + let encoded = try encoder.encode(objectJSON) + let decoded = try decoder.decode(JSONValue.self, from: encoded) + XCTAssertEqual(String(bytes: encoded, encoding: .utf8), "{\"Key\":\"Value\"}") + XCTAssertEqual(objectJSON.objectValue!["Key"]!.stringValue!, "Value") + XCTAssertEqual(decoded, objectJSON) + } +}