Skip to content

Commit abad720

Browse files
authored
Improve JSON by removing Any? (#43)
* refactor JSON * remove Any? on JSON * fix remaining tests * [ci] run builds and test in parallel
1 parent 8c38668 commit abad720

File tree

9 files changed

+303
-216
lines changed

9 files changed

+303
-216
lines changed

.github/workflows/pull_request.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ on: [pull_request, workflow_dispatch]
55
permissions: read-all
66

77
jobs:
8-
swift-bedrock-library:
8+
swift-bedrock-library-build:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: Checkout repository
1212
uses: actions/checkout@v4
1313
- name: Build
1414
run: swift build --configuration release
15+
16+
swift-bedrock-library-test:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
1521
- name: Tests
1622
run: swift test
1723

Sources/Converse/ContentBlocks/DocumentToJSON.swift

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,34 @@ import Foundation
2323

2424
// FIXME: avoid extensions on structs you do not control
2525
extension SmithyDocument {
26-
public func toJSON() throws -> JSON {
26+
private func toJSONValue() throws -> JSONValue {
2727
switch self.type {
2828
case .string:
29-
return JSON(with: try self.asString())
29+
return try JSONValue(self.asString())
3030
case .boolean:
31-
return JSON(with: try self.asBoolean())
31+
return try JSONValue(self.asBoolean())
3232
case .integer:
33-
return JSON(with: try self.asInteger())
33+
return try JSONValue(self.asInteger())
3434
case .double, .float:
35-
return JSON(with: try self.asDouble())
35+
return try JSONValue(self.asDouble())
3636
case .list:
37-
let array = try self.asList().map { try $0.toJSON() }
38-
return JSON(with: array)
37+
let array = try self.asList().map { try $0.toJSONValue() }
38+
return JSONValue(array)
3939
case .map:
4040
let map = try self.asStringMap()
41-
var result: [String: JSON] = [:]
42-
for (key, value) in map {
43-
result[key] = try value.toJSON()
44-
}
45-
return JSON(with: result)
41+
let newMap = try map.mapValues({ try $0.toJSONValue() })
42+
return JSONValue(newMap)
4643
case .blob:
4744
let data = try self.asBlob()
48-
return JSON(with: data)
45+
let json = try JSON(from: data)
46+
return json.value
4947
default:
5048
throw DocumentError.typeMismatch("Unsupported type for JSON conversion: \(self.type)")
5149
}
5250
}
51+
52+
public func toJSON() throws -> JSON {
53+
let value = try self.toJSONValue()
54+
return JSON(with: value)
55+
}
5356
}

Sources/Converse/ContentBlocks/JSON.swift

Lines changed: 130 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -19,57 +19,156 @@ import FoundationEssentials
1919
import Foundation
2020
#endif
2121

22-
public struct JSON: Codable, @unchecked Sendable { // FIXME: make Sendable
23-
public var value: Any?
22+
public enum JSONValue: Codable, Sendable {
23+
case null
24+
case int(Int)
25+
case double(Double)
26+
case string(String)
27+
case bool(Bool)
28+
case array([JSONValue])
29+
case object([String: JSONValue])
30+
31+
public init(_ value: Any?) {
32+
33+
guard let value else {
34+
self = .null
35+
return
36+
}
37+
switch value {
38+
case let v as Int:
39+
self = .int(v)
40+
break
41+
case let v as Double:
42+
self = .double(v)
43+
break
44+
case let v as String:
45+
self = .string(v)
46+
break
47+
case let v as Bool:
48+
self = .bool(v)
49+
break
50+
case let v as [Any]:
51+
self = .array(v.map { JSONValue($0) })
52+
break
53+
case let v as [String: JSONValue]:
54+
self = .object(v)
55+
break
56+
case let v as [JSONValue]:
57+
self = .array(v)
58+
break
59+
case let v as JSONValue:
60+
self = v
61+
break
62+
default:
63+
fatalError("JSONValue: Unsupported type: \(type(of: value))")
64+
}
65+
}
2466

25-
/// Returns the value inside the JSON object defined by the given key.
2667
public subscript<T>(key: String) -> T? {
2768
get {
28-
if let dictionary = value as? [String: JSON] {
29-
let json: JSON? = dictionary[key]
30-
let nestedValue: Any? = json?.getValue()
31-
return nestedValue as? T
69+
if case let .object(dictionary) = self {
70+
guard let jsonValue = dictionary[key] else {
71+
return nil
72+
}
73+
switch jsonValue {
74+
case .int(let v): return v as? T
75+
case .double(let v): return v as? T
76+
case .string(let v): return v as? T
77+
case .bool(let v): return v as? T
78+
case .array(let v): return v as? T
79+
case .object(let v): return v as? T
80+
case .null: return nil
81+
}
3282
}
3383
return nil
3484
}
3585
}
3686

37-
/// Returns the JSON object defined by the given key.
38-
public subscript(key: String) -> JSON? {
87+
public init(from decoder: Decoder) throws {
88+
let container = try decoder.singleValueContainer()
89+
if container.decodeNil() {
90+
self = .null
91+
} else if let intValue = try? container.decode(Int.self) {
92+
self = .int(intValue)
93+
} else if let doubleValue = try? container.decode(Double.self) {
94+
self = .double(doubleValue)
95+
} else if let stringValue = try? container.decode(String.self) {
96+
self = .string(stringValue)
97+
} else if let boolValue = try? container.decode(Bool.self) {
98+
self = .bool(boolValue)
99+
} else if let arrayValue = try? container.decode([JSONValue].self) {
100+
self = .array(arrayValue)
101+
} else if let dictionaryValue = try? container.decode([String: JSONValue].self) {
102+
self = .object(dictionaryValue)
103+
} else {
104+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type")
105+
}
106+
}
107+
108+
// MARK: Public Methods
109+
110+
public func encode(to encoder: Encoder) throws {
111+
var container = encoder.singleValueContainer()
112+
switch self {
113+
case .null:
114+
try container.encodeNil()
115+
case .int(let v):
116+
try container.encode(v)
117+
case .double(let v):
118+
try container.encode(v)
119+
case .string(let v):
120+
try container.encode(v)
121+
case .bool(let v):
122+
try container.encode(v)
123+
case .array(let v):
124+
try container.encode(v)
125+
case .object(let v):
126+
try container.encode(v)
127+
}
128+
}
129+
130+
}
131+
132+
public struct JSON: Codable, Sendable {
133+
public let value: JSONValue
134+
135+
public subscript<T>(key: String) -> T? {
39136
get {
40-
if let dictionary = value as? [String: JSON] {
41-
return dictionary[key]
42-
}
43-
return nil
137+
value[key]
44138
}
45139
}
46140

47-
/// Returns the value inside the JSON object defined by the given key.
48-
public func getValue<T>(_ key: String) -> T? {
49-
if let dictionary = value as? [String: JSON] {
50-
return dictionary[key]?.value as? T
141+
public subscript(key: String) -> JSONValue? {
142+
get {
143+
if case let .object(dictionary) = value {
144+
if let v = dictionary[key] {
145+
return v
146+
}
147+
}
148+
return nil
51149
}
52-
return nil
53150
}
54151

55-
/// Returns the value inside the JSON object.
56152
public func getValue<T>() -> T? {
57-
value as? T
153+
switch value {
154+
case .int(let v): return v as? T
155+
case .double(let v): return v as? T
156+
case .string(let v): return v as? T
157+
case .bool(let v): return v as? T
158+
case .array(let v): return v as? T
159+
case .object(let v): return v as? T
160+
case .null: return nil
161+
}
58162
}
59163

60164
// MARK: Initializers
61165

62-
public init(with value: Any?) {
166+
public init(with value: JSONValue) {
63167
self.value = value
64168
}
65169

66170
public init(from string: String) throws {
67-
var s: String!
68-
if string.isEmpty {
69-
s = "{}"
70-
} else {
71-
s = string
72-
}
171+
let s = string.isEmpty ? "{}" : string
73172
guard let data = s.data(using: .utf8) else {
74173
throw BedrockLibraryError.encodingError("Could not encode String to Data")
75174
}
@@ -78,59 +177,18 @@ public struct JSON: Codable, @unchecked Sendable { // FIXME: make Sendable
78177

79178
public init(from data: Data) throws {
80179
do {
180+
print(String(decoding: data, as: UTF8.self))
81181
self = try JSONDecoder().decode(JSON.self, from: data)
82182
} catch {
83183
throw BedrockLibraryError.decodingError("Failed to decode JSON: \(error)")
84184
}
85185
}
86186

187+
// Codable
188+
87189
public init(from decoder: Decoder) throws {
88190
let container = try decoder.singleValueContainer()
89-
if container.decodeNil() {
90-
self.value = nil
91-
} else if let intValue = try? container.decode(Int.self) {
92-
self.value = intValue
93-
} else if let doubleValue = try? container.decode(Double.self) {
94-
self.value = doubleValue
95-
} else if let stringValue = try? container.decode(String.self) {
96-
self.value = stringValue
97-
} else if let boolValue = try? container.decode(Bool.self) {
98-
self.value = boolValue
99-
} else if let arrayValue = try? container.decode([JSON].self) {
100-
self.value = arrayValue.map { JSON(with: $0.value) }
101-
} else if let dictionaryValue = try? container.decode([String: JSON].self) {
102-
self.value = dictionaryValue.mapValues { JSON(with: $0.value) }
103-
} else {
104-
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type")
105-
}
191+
value = try container.decode(JSONValue.self)
106192
}
107193

108-
// MARK: Public Methods
109-
110-
public func encode(to encoder: Encoder) throws {
111-
var container = encoder.singleValueContainer()
112-
if let jsonValue = value as? JSON {
113-
try jsonValue.encode(to: encoder)
114-
} else if let intValue = value as? Int {
115-
try container.encode(intValue)
116-
} else if let doubleValue = value as? Double {
117-
try container.encode(doubleValue)
118-
} else if let stringValue = value as? String {
119-
try container.encode(stringValue)
120-
} else if let boolValue = value as? Bool {
121-
try container.encode(boolValue)
122-
} else if let arrayValue = value as? [Any] {
123-
let jsonArray = arrayValue.map { JSON(with: $0) }
124-
try container.encode(jsonArray)
125-
} else if let dictionaryValue = value as? [String: Any] {
126-
let jsonDictionary = dictionaryValue.mapValues { JSON(with: $0) }
127-
try container.encode(jsonDictionary)
128-
} else {
129-
// try container.encode(String(describing: value ?? "nil"))
130-
throw EncodingError.invalidValue(
131-
value ?? "nil",
132-
EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type")
133-
)
134-
}
135-
}
136194
}

Sources/Converse/ConverseRequest.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,21 @@ public struct ConverseRequest {
7070
}
7171

7272
func getAdditionalModelRequestFields() throws -> Smithy.Document? {
73+
//FIXME: this is incorrect. We should check for all Claude models
7374
if model == .claudev3_7_sonnet, let maxReasoningTokens {
74-
let reasoningConfigJSON = JSON(with: [
75-
"thinking": [
76-
"type": "enabled",
77-
"budget_tokens": maxReasoningTokens,
78-
]
79-
])
75+
let reasoningConfigJSON = JSON(
76+
with: .array(
77+
[
78+
.object([
79+
"thinking": .object([
80+
"type": .string("enabled"),
81+
"budget_tokens": .int(maxReasoningTokens),
82+
])
83+
])
84+
]
85+
)
86+
)
87+
8088
return try reasoningConfigJSON.toDocument()
8189
}
8290
return nil

0 commit comments

Comments
 (0)