Skip to content

Commit fe3c6cf

Browse files
authored
SWIFT-916 Implement JSON Enum
1 parent 4437d9f commit fe3c6cf

File tree

2 files changed

+211
-0
lines changed

2 files changed

+211
-0
lines changed

Sources/BSON/JSON.swift

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Foundation
2+
3+
/// Enum representing a JSON value, used internally for modeling JSON
4+
/// during extendedJSON parsing/generation.
5+
internal enum JSON: Codable {
6+
case number(Double)
7+
case string(String)
8+
case bool(Bool)
9+
indirect case array([JSON])
10+
indirect case object([String: JSON])
11+
case null
12+
13+
/// Initialize a `JSON` from a decoder.
14+
/// Tries to decode into each of the JSON types one by one until one succeeds or
15+
/// throws an error indicating that the input is not a valid `JSON` type.
16+
internal init(from decoder: Decoder) throws {
17+
let container = try decoder.singleValueContainer()
18+
if let d = try? container.decode(Double.self) {
19+
self = .number(d)
20+
} else if let s = try? container.decode(String.self) {
21+
self = .string(s)
22+
} else if let b = try? container.decode(Bool.self) {
23+
self = .bool(b)
24+
} else if let a = try? container.decode([JSON].self) {
25+
self = .array(a)
26+
} else if let d = try? container.decode([String: JSON].self) {
27+
self = .object(d)
28+
} else if container.decodeNil() {
29+
self = .null
30+
} else {
31+
throw DecodingError.dataCorrupted(
32+
DecodingError.Context(
33+
codingPath: decoder.codingPath,
34+
debugDescription: "Not a valid JSON type"
35+
))
36+
}
37+
}
38+
39+
/// Encode a `JSON` to a container by encoding the type of this `JSON` instance.
40+
internal func encode(to encoder: Encoder) throws {
41+
var container = encoder.singleValueContainer()
42+
switch self {
43+
case let .number(n):
44+
try container.encode(n)
45+
case let .string(s):
46+
try container.encode(s)
47+
case let .bool(b):
48+
try container.encode(b)
49+
case let .array(a):
50+
try container.encode(a)
51+
case let .object(o):
52+
try container.encode(o)
53+
case .null:
54+
try container.encodeNil()
55+
}
56+
}
57+
}
58+
59+
extension JSON: ExpressibleByFloatLiteral {
60+
internal init(floatLiteral value: Double) {
61+
self = .number(value)
62+
}
63+
}
64+
65+
extension JSON: ExpressibleByIntegerLiteral {
66+
internal init(integerLiteral value: Int) {
67+
// The number `JSON` type is a Double, so we cast any integers to doubles.
68+
self = .number(Double(value))
69+
}
70+
}
71+
72+
extension JSON: ExpressibleByStringLiteral {
73+
internal init(stringLiteral value: String) {
74+
self = .string(value)
75+
}
76+
}
77+
78+
extension JSON: ExpressibleByBooleanLiteral {
79+
internal init(booleanLiteral value: Bool) {
80+
self = .bool(value)
81+
}
82+
}
83+
84+
extension JSON: ExpressibleByArrayLiteral {
85+
internal init(arrayLiteral elements: JSON...) {
86+
self = .array(elements)
87+
}
88+
}
89+
90+
extension JSON: ExpressibleByDictionaryLiteral {
91+
internal init(dictionaryLiteral elements: (String, JSON)...) {
92+
self = .object([String: JSON](uniqueKeysWithValues: elements))
93+
}
94+
}
95+
96+
/// Value Getters
97+
extension JSON {
98+
/// If this `JSON` is a `.double`, return it as a `Double`. Otherwise, return nil.
99+
internal var doubleValue: Double? {
100+
guard case let .number(n) = self else {
101+
return nil
102+
}
103+
return n
104+
}
105+
106+
/// If this `JSON` is a `.string`, return it as a `String`. Otherwise, return nil.
107+
internal var stringValue: String? {
108+
guard case let .string(s) = self else {
109+
return nil
110+
}
111+
return s
112+
}
113+
114+
/// If this `JSON` is a `.bool`, return it as a `Bool`. Otherwise, return nil.
115+
internal var boolValue: Bool? {
116+
guard case let .bool(b) = self else {
117+
return nil
118+
}
119+
return b
120+
}
121+
122+
/// If this `JSON` is a `.array`, return it as a `[JSON]`. Otherwise, return nil.
123+
internal var arrayValue: [JSON]? {
124+
guard case let .array(a) = self else {
125+
return nil
126+
}
127+
return a
128+
}
129+
130+
/// If this `JSON` is a `.object`, return it as a `[String: JSON]`. Otherwise, return nil.
131+
internal var objectValue: [String: JSON]? {
132+
guard case let .object(o) = self else {
133+
return nil
134+
}
135+
return o
136+
}
137+
}
138+
139+
extension JSON: Equatable {}

Tests/BSONTests/JSONTests.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
@testable import BSON
2+
import Foundation
3+
import Nimble
4+
import NIO
5+
import XCTest
6+
7+
open class JSONTestCase: XCTestCase {
8+
let encoder = JSONEncoder()
9+
let decoder = JSONDecoder()
10+
11+
func testInteger() throws {
12+
// Initializing a JSON with an int works, but it will be cast to a double.
13+
let intJSON: JSON = 12
14+
let encoded = try encoder.encode([intJSON])
15+
/* JSONEncoder currently cannot encode non-object/array top level values.
16+
To get around this, the generated JSON will need to be wrapped in an array
17+
and unwrapped again at the end as a workaround.
18+
This workaround can be removed when Swift 5.3 is the minimum supported version by the BSON library. */
19+
expect(Double(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)!)!)
20+
.to(beCloseTo(12))
21+
22+
let decoded = try decoder.decode([JSON].self, from: encoded)[0]
23+
expect(decoded.doubleValue).to(beCloseTo(intJSON.doubleValue!))
24+
}
25+
26+
func testDouble() throws {
27+
let doubleJSON: JSON = 12.3
28+
let encoded = try encoder.encode([doubleJSON])
29+
expect(Double(String(data: encoded.dropFirst().dropLast(), encoding: .utf8)!)!)
30+
.to(beCloseTo(12.3))
31+
32+
let decoded = try decoder.decode([JSON].self, from: encoded)[0]
33+
expect(decoded.doubleValue).to(beCloseTo(doubleJSON.doubleValue!))
34+
}
35+
36+
func testString() throws {
37+
let stringJSON: JSON = "I am a String"
38+
let encoded = try encoder.encode([stringJSON])
39+
expect(String(data: encoded.dropFirst().dropLast(), encoding: .utf8))
40+
.to(equal("\"I am a String\""))
41+
let decoded = try decoder.decode([JSON].self, from: encoded)[0]
42+
expect(decoded).to(equal(stringJSON))
43+
}
44+
45+
func testBool() throws {
46+
let boolJSON: JSON = true
47+
let encoded = try encoder.encode([boolJSON])
48+
expect(String(data: encoded.dropFirst().dropLast(), encoding: .utf8))
49+
.to(equal("true"))
50+
let decoded = try decoder.decode([JSON].self, from: encoded)[0]
51+
expect(decoded).to(equal(boolJSON))
52+
}
53+
54+
func testArray() throws {
55+
let arrayJSON: JSON = ["I am a string in an array"]
56+
let encoded = try encoder.encode(arrayJSON)
57+
let decoded = try decoder.decode(JSON.self, from: encoded)
58+
expect(String(data: encoded, encoding: .utf8))
59+
.to(equal("[\"I am a string in an array\"]"))
60+
expect(decoded).to(equal(arrayJSON))
61+
}
62+
63+
func testObject() throws {
64+
let objectJSON: JSON = ["Key": "Value"]
65+
let encoded = try encoder.encode(objectJSON)
66+
let decoded = try decoder.decode(JSON.self, from: encoded)
67+
expect(String(data: encoded, encoding: .utf8))
68+
.to(equal("{\"Key\":\"Value\"}"))
69+
expect(objectJSON.objectValue!["Key"]!.stringValue!).to(equal("Value"))
70+
expect(decoded).to(equal(objectJSON))
71+
}
72+
}

0 commit comments

Comments
 (0)