Skip to content

Commit 353b688

Browse files
committed
GenericObject for decoding unknown model
1 parent 21a4216 commit 353b688

File tree

10 files changed

+218
-19
lines changed

10 files changed

+218
-19
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Foundation
2+
3+
public struct KeyedGenericObject: Codable, Equatable {
4+
public let key: String
5+
public let value: GenericObject
6+
}
7+
8+
public indirect enum GenericObject: Equatable {
9+
case array([GenericObject])
10+
case genericObject([KeyedGenericObject])
11+
case bool(Bool)
12+
case double(Double)
13+
case uuid(UUID)
14+
case int(Int)
15+
case string(String)
16+
case null
17+
}
18+
19+
extension GenericObject: Codable {
20+
struct CodingKeys: CodingKey {
21+
let intValue: Int?
22+
let stringValue: String
23+
24+
init?(intValue: Int) {
25+
return nil
26+
}
27+
28+
init?(stringValue: String) {
29+
self.stringValue = stringValue
30+
self.intValue = nil
31+
}
32+
}
33+
34+
public init(from decoder: Decoder) throws { // swiftlint:disable:this cyclomatic_complexity
35+
if let keyed = try? decoder.container(keyedBy: CodingKeys.self) {
36+
var array = [KeyedGenericObject]()
37+
for key in keyed.allKeys {
38+
if let genericObject = try? keyed.decode(GenericObject.self, forKey: key) {
39+
array.append(.init(key: key.stringValue, value: genericObject))
40+
} else {
41+
throw DecodingError.dataCorruptedError(forKey: key, in: keyed, debugDescription: "Unknown type")
42+
}
43+
}
44+
self = .genericObject(array)
45+
} else if var unkeyed = try? decoder.unkeyedContainer() {
46+
var array = [GenericObject]()
47+
while !unkeyed.isAtEnd {
48+
if let genericObject = try? unkeyed.decode(GenericObject.self) {
49+
array.append(genericObject)
50+
} else {
51+
throw DecodingError.dataCorruptedError(in: unkeyed, debugDescription: "Unknown type")
52+
}
53+
}
54+
self = .array(array)
55+
} else if let singleValue = try? decoder.singleValueContainer() {
56+
if let int = try? singleValue.decode(Int.self) {
57+
self = .int(int)
58+
} else if let bool = try? singleValue.decode(Bool.self) {
59+
self = .bool(bool)
60+
} else if let double = try? singleValue.decode(Double.self) {
61+
self = .double(double)
62+
} else if let uuid = try? singleValue.decode(UUID.self) {
63+
self = .uuid(uuid)
64+
} else if let string = try? singleValue.decode(String.self) {
65+
self = .string(string)
66+
} else if singleValue.decodeNil() {
67+
self = .null
68+
} else {
69+
throw DecodingError.dataCorruptedError(in: singleValue, debugDescription: "Unknown type")
70+
}
71+
} else {
72+
self = .null
73+
}
74+
}
75+
76+
public func encode(to encoder: Encoder) throws {
77+
throw EncodingError.invalidValue("Not implemented yet", EncodingError.Context(codingPath: encoder.codingPath,
78+
debugDescription: "Not implemented yet"))
79+
}
80+
}

MonitoredAppMiddleware/Tests/MonitoredAppMiddlewareTests/MonitoredAppMiddlewareTests.swift

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,112 @@
22
import XCTest
33

44
final class MonitoredAppMiddlewareTests: XCTestCase {
5-
func testExample() {
6-
// This is an example of a functional test case.
7-
// Use XCTAssert and related functions to verify your tests produce the correct
8-
// results.
9-
XCTAssertEqual(MonitoredAppMiddleware().text, "Hello, World!")
5+
func testGenericObjectDecodeString() throws {
6+
let json = "{\"string\":\"my string\"}"
7+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
8+
XCTAssertEqual(result, GenericObject.genericObject([.init(key: "string", value: .string("my string"))]))
9+
}
10+
11+
func testGenericObjectDecodeInt() throws {
12+
let json = "{\"int\":42}"
13+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
14+
XCTAssertEqual(result, GenericObject.genericObject([.init(key: "int", value: .int(42))]))
15+
}
16+
17+
func testGenericObjectDecodeBoolTrue() throws {
18+
let json = "{\"bool\":true}"
19+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
20+
XCTAssertEqual(result, GenericObject.genericObject([.init(key: "bool", value: .bool(true))]))
21+
}
22+
23+
func testGenericObjectDecodeBoolFalse() throws {
24+
let json = "{\"bool\":false}"
25+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
26+
XCTAssertEqual(result, GenericObject.genericObject([.init(key: "bool", value: .bool(false))]))
27+
}
28+
29+
func testGenericObjectDecodeNull() throws {
30+
let json = "{\"something\":null}"
31+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
32+
XCTAssertEqual(result, GenericObject.genericObject([.init(key: "something", value: .null)]))
33+
}
34+
35+
func testGenericObjectDecodeDouble() throws {
36+
let json = "{\"double\":42.1}"
37+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
38+
XCTAssertEqual(result, GenericObject.genericObject([.init(key: "double", value: .double(42.1))]))
39+
}
40+
41+
func testGenericObjectDecodeArrayOfInts() throws {
42+
let json = "{\"ints\":[1, 2, 3, 5, 8, 13, 21, 34, 55]}"
43+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
44+
XCTAssertEqual(result, GenericObject.genericObject([
45+
.init(
46+
key: "ints",
47+
value: .array([.int(1), .int(2), .int(3), .int(5), .int(8), .int(13), .int(21), .int(34), .int(55)]
48+
))
49+
]))
50+
}
51+
52+
func testGenericObjectDecodeArrayOfStrings() throws {
53+
let json = "{\"ints\":[\"1\", \"2\", \"3\", \"5\", \"8\", \"13\", \"21\", \"34\", \"55\"]}"
54+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
55+
XCTAssertEqual(result, GenericObject.genericObject([
56+
.init(
57+
key: "ints",
58+
value: .array([
59+
.string("1"),
60+
.string("2"),
61+
.string("3"),
62+
.string("5"),
63+
.string("8"),
64+
.string("13"),
65+
.string("21"),
66+
.string("34"),
67+
.string("55")
68+
]
69+
))
70+
]))
71+
}
72+
73+
func testGenericObjectDecodeNestedOfInts() throws {
74+
let json = "{\"parent\":{\"ints\":[1, 2, 3, 5, 8, 13, 21, 34, 55]}}"
75+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
76+
XCTAssertEqual(result, GenericObject.genericObject([
77+
.init(
78+
key: "parent",
79+
value: .genericObject([.init(
80+
key: "ints",
81+
value: .array([.int(1), .int(2), .int(3), .int(5), .int(8), .int(13), .int(21), .int(34), .int(55)])
82+
)])
83+
)
84+
]))
85+
}
86+
87+
func testGenericObjectDecodeArrayOfObjects() throws {
88+
let json = "{\"objects\":[{\"someKey\": \"1\"},{\"someKey\": \"2\"}]}"
89+
let result = try JSONDecoder().decode(GenericObject.self, from: json.data(using: .utf8)!)
90+
XCTAssertEqual(result, GenericObject.genericObject([
91+
.init(
92+
key: "objects",
93+
value: .array([
94+
.genericObject([.init(key: "someKey", value: .string("1"))]),
95+
.genericObject([.init(key: "someKey", value: .string("2"))])
96+
]
97+
))
98+
]))
1099
}
11100

12101
static var allTests = [
13-
("testExample", testExample)
102+
("testGenericObjectDecodeString", testGenericObjectDecodeString),
103+
("testGenericObjectDecodeInt", testGenericObjectDecodeInt),
104+
("testGenericObjectDecodeBoolTrue", testGenericObjectDecodeBoolTrue),
105+
("testGenericObjectDecodeBoolFalse", testGenericObjectDecodeBoolFalse),
106+
("testGenericObjectDecodeNull", testGenericObjectDecodeNull),
107+
("testGenericObjectDecodeDouble", testGenericObjectDecodeDouble),
108+
("testGenericObjectDecodeArrayOfInts", testGenericObjectDecodeArrayOfInts),
109+
("testGenericObjectDecodeArrayOfStrings", testGenericObjectDecodeArrayOfStrings),
110+
("testGenericObjectDecodeNestedOfInts", testGenericObjectDecodeNestedOfInts),
111+
("testGenericObjectDecodeArrayOfObjects", testGenericObjectDecodeArrayOfObjects)
14112
]
15113
}

SwiftRexMonitor.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
/* End PBXAggregateTarget section */
2222

2323
/* Begin PBXBuildFile section */
24+
D62B0F712455FB4800247D7F /* ActionHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62B0F702455FB4800247D7F /* ActionHistoryEntry.swift */; };
2425
D6414F5D241EFC84004F4A9F /* MultipeerConnectivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6414F5C241EFC84004F4A9F /* MultipeerConnectivity.framework */; };
2526
D64C382F241D6705008D470F /* TypeErase.generated.abstract.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64C382D241D6704008D470F /* TypeErase.generated.abstract.swift */; };
2627
D64C3831241D677D008D470F /* EnumCodable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64C3830241D677C008D470F /* EnumCodable.generated.swift */; };
@@ -53,6 +54,7 @@
5354
/* End PBXBuildFile section */
5455

5556
/* Begin PBXFileReference section */
57+
D62B0F702455FB4800247D7F /* ActionHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionHistoryEntry.swift; sourceTree = "<group>"; };
5658
D6414F5C241EFC84004F4A9F /* MultipeerConnectivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MultipeerConnectivity.framework; path = System/Library/Frameworks/MultipeerConnectivity.framework; sourceTree = SDKROOT; };
5759
D64C382C241D6704008D470F /* AutoMockable.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoMockable.generated.swift; sourceTree = "<group>"; };
5860
D64C382D241D6704008D470F /* TypeErase.generated.abstract.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypeErase.generated.abstract.swift; sourceTree = "<group>"; };
@@ -175,6 +177,7 @@
175177
D69ED04C23EB8B7F002C1FDB /* Model */ = {
176178
isa = PBXGroup;
177179
children = (
180+
D62B0F702455FB4800247D7F /* ActionHistoryEntry.swift */,
178181
D6C7C53B241C115F000F89EF /* AppAction.swift */,
179182
D6C7C539241C1106000F89EF /* AppState.swift */,
180183
D6B6A625241DC51000AF74CA /* MonitoredPeer.swift */,
@@ -359,6 +362,7 @@
359362
isa = PBXSourcesBuildPhase;
360363
buildActionMask = 2147483647;
361364
files = (
365+
D62B0F712455FB4800247D7F /* ActionHistoryEntry.swift in Sources */,
362366
D69ED04723EB8B1E002C1FDB /* Collection.swift in Sources */,
363367
D6C7C53A241C1106000F89EF /* AppState.swift in Sources */,
364368
D64C383B241D7FFB008D470F /* Operators.swift in Sources */,

SwiftRexMonitor/CodeGen/EnumCodable.generated.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ extension MonitorAction: Codable {
8181
let subContainer = try container.nestedContainer(keyedBy: CodingKeys.GotActionKeys.self, forKey: .associatedValues)
8282
let associatedValues0 = try subContainer.decode(String.self, forKey: .action)
8383
let associatedValues1 = try subContainer.decode(Date.self, forKey: .remoteDate)
84-
let associatedValues2 = try subContainer.decode(String?.self, forKey: .state)
84+
let associatedValues2 = try subContainer.decode(GenericObject?.self, forKey: .state)
8585
let associatedValues3 = try subContainer.decode(ActionSource.self, forKey: .actionSource)
8686
let associatedValues4 = try subContainer.decode(Peer.self, forKey: .peer)
8787
self = .gotAction(action: associatedValues0, remoteDate: associatedValues1, state: associatedValues2, actionSource: associatedValues3, peer: associatedValues4)

SwiftRexMonitor/CodeGen/Prism.generated.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ extension MonitorAction {
102102
self.evaluateData != nil
103103
}
104104

105-
public var gotAction: (action: String, remoteDate: Date, state: String?, actionSource: ActionSource, peer: Peer)? {
105+
public var gotAction: (action: String, remoteDate: Date, state: GenericObject?, actionSource: ActionSource, peer: Peer)? {
106106
get {
107107
guard case let .gotAction(action, remoteDate, state, actionSource, peer) = self else { return nil }
108108
return (action, remoteDate, state, actionSource, peer)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// ActionHistoryEntry.swift
3+
// SwiftRexMonitor
4+
//
5+
// Created by Luiz Rodrigo Martins Barbosa on 26.04.20.
6+
// Copyright © 2020 DeveloperCity. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import MonitoredAppMiddleware
11+
import SwiftRex
12+
13+
public struct ActionHistoryEntry: Codable, Equatable, Identifiable {
14+
public let id: UUID
15+
public let remoteDate: Date
16+
public let action: String
17+
public let state: GenericObject?
18+
public let actionSource: ActionSource
19+
}
Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import Foundation
22
import MonitoredAppMiddleware
33
import MultipeerMiddleware
4-
import SwiftRex
54

65
public struct MonitoredPeer: Codable, Equatable {
76
public var peer: Peer
87
public var isConnected: Bool
98
public var metadata: PeerMetadata?
109
public var history: [ActionHistoryEntry]
1110
}
12-
13-
public struct ActionHistoryEntry: Codable, Equatable {
14-
public let remoteDate: Date
15-
public let action: String
16-
public let state: String?
17-
public let actionSource: ActionSource
18-
}

SwiftRexMonitor/Redux/Monitor/MonitorAction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ public enum MonitorAction {
1010
case peerListNeedsRefresh
1111
case peerListHasChanged([Peer])
1212
case evaluateData(Data, from: Peer)
13-
case gotAction(action: String, remoteDate: Date, state: String?, actionSource: ActionSource, peer: Peer)
13+
case gotAction(action: String, remoteDate: Date, state: GenericObject?, actionSource: ActionSource, peer: Peer)
1414
case gotGreetings(PeerMetadata, peer: Peer)
1515
}

SwiftRexMonitor/Redux/Monitor/MonitorMiddleware.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,13 @@ public final class MonitorMiddleware: Middleware {
106106
.gotAction(
107107
action: message.action,
108108
remoteDate: message.remoteDate,
109-
state: message.state.flatMap { String(data: $0, encoding: .utf8) },
109+
state: message.state.map {
110+
do {
111+
return try decoder().decode(GenericObject.self, from: $0)
112+
} catch {
113+
return .string("Decoding error: \(error.localizedDescription)")
114+
}
115+
},
110116
actionSource: message.actionSource,
111117
peer: peer
112118
)

SwiftRexMonitor/Redux/Monitor/MonitorReducer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ let monitorReducer = Reducer<MonitorAction, [MonitoredPeer]> { action, state in
3636
guard $0.peer.peerInstance.displayName == peer.peerInstance.displayName else { return $0 }
3737

3838
var peerInState = $0
39-
peerInState.history.append(.init(remoteDate: remoteDate, action: action, state: newState, actionSource: actionSource))
39+
peerInState.history.append(.init(id: UUID(), remoteDate: remoteDate, action: action, state: newState, actionSource: actionSource))
4040
return peerInState
4141
}
4242
}

0 commit comments

Comments
 (0)