Skip to content

Commit 31353c2

Browse files
authored
Add types to represent JSON values (#112)
1 parent ac4aea1 commit 31353c2

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

Sources/GoogleAI/JSONValue.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// A collection of name-value pairs representing a JSON object.
18+
///
19+
/// This may be decoded from, or encoded to, a
20+
/// [`google.protobuf.Struct`](https://protobuf.dev/reference/protobuf/google.protobuf/#struct).
21+
public typealias JSONObject = [String: JSONValue]
22+
23+
/// Represents a value in one of JSON's data types.
24+
///
25+
/// This may be decoded from, or encoded to, a
26+
/// [`google.protobuf.Value`](https://protobuf.dev/reference/protobuf/google.protobuf/#value).
27+
public enum JSONValue {
28+
/// A `null` value.
29+
case null
30+
31+
/// A numeric value.
32+
case number(Double)
33+
34+
/// A string value.
35+
case string(String)
36+
37+
/// A boolean value.
38+
case bool(Bool)
39+
40+
/// A JSON object.
41+
case object(JSONObject)
42+
43+
/// An array of `JSONValue`s.
44+
case array([JSONValue])
45+
}
46+
47+
extension JSONValue: Decodable {
48+
public init(from decoder: Decoder) throws {
49+
let container = try decoder.singleValueContainer()
50+
if container.decodeNil() {
51+
self = .null
52+
} else if let numberValue = try? container.decode(Double.self) {
53+
self = .number(numberValue)
54+
} else if let stringValue = try? container.decode(String.self) {
55+
self = .string(stringValue)
56+
} else if let boolValue = try? container.decode(Bool.self) {
57+
self = .bool(boolValue)
58+
} else if let objectValue = try? container.decode(JSONObject.self) {
59+
self = .object(objectValue)
60+
} else if let arrayValue = try? container.decode([JSONValue].self) {
61+
self = .array(arrayValue)
62+
} else {
63+
throw DecodingError.dataCorruptedError(
64+
in: container,
65+
debugDescription: "Failed to decode JSON value."
66+
)
67+
}
68+
}
69+
}
70+
71+
extension JSONValue: Equatable {}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
import XCTest
15+
16+
@testable import GoogleGenerativeAI
17+
18+
final class JSONValueTests: XCTestCase {
19+
func testDecodeNull() throws {
20+
let jsonData = try XCTUnwrap("null".data(using: .utf8))
21+
22+
let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData))
23+
24+
XCTAssertEqual(jsonObject, .null)
25+
}
26+
27+
func testDecodeNumber() throws {
28+
let expectedNumber = 3.14159
29+
let jsonData = try XCTUnwrap("\(expectedNumber)".data(using: .utf8))
30+
31+
let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData))
32+
33+
XCTAssertEqual(jsonObject, .number(expectedNumber))
34+
}
35+
36+
func testDecodeString() throws {
37+
let expectedString = "hello-world"
38+
let jsonData = try XCTUnwrap("\"\(expectedString)\"".data(using: .utf8))
39+
40+
let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData))
41+
42+
XCTAssertEqual(jsonObject, .string(expectedString))
43+
}
44+
45+
func testDecodeBool() throws {
46+
let expectedBool = true
47+
let jsonData = try XCTUnwrap("\(expectedBool)".data(using: .utf8))
48+
49+
let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData))
50+
51+
XCTAssertEqual(jsonObject, .bool(expectedBool))
52+
}
53+
54+
func testDecodeObject() throws {
55+
let numberKey = "pi"
56+
let numberValue = 3.14159
57+
let stringKey = "hello"
58+
let stringValue = "world"
59+
let expectedObject: JSONObject = [
60+
numberKey: .number(numberValue),
61+
stringKey: .string(stringValue),
62+
]
63+
let json = """
64+
{
65+
"\(numberKey)": \(numberValue),
66+
"\(stringKey)": "\(stringValue)"
67+
}
68+
"""
69+
let jsonData = try XCTUnwrap(json.data(using: .utf8))
70+
71+
let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData))
72+
73+
XCTAssertEqual(jsonObject, .object(expectedObject))
74+
}
75+
76+
func testDecodeArray() throws {
77+
let numberValue = 3.14159
78+
let expectedArray: [JSONValue] = [.null, .number(numberValue)]
79+
let jsonData = try XCTUnwrap("[ null, \(numberValue) ]".data(using: .utf8))
80+
81+
let jsonObject = try XCTUnwrap(JSONDecoder().decode(JSONValue.self, from: jsonData))
82+
83+
XCTAssertEqual(jsonObject, .array(expectedArray))
84+
}
85+
}

0 commit comments

Comments
 (0)