Skip to content
This repository was archived by the owner on Jan 11, 2024. It is now read-only.

Commit c222eb1

Browse files
committed
test: add tests for dynamic JSON decoding with type(s) parent coding key
1 parent 9fc73b7 commit c222eb1

13 files changed

+556
-15
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ let package = Package(
3333
dependencies: ["DynamicCodableKit"],
3434
resources: [
3535
.process("DynamicDecodingContextCodingKey/JSONs"),
36+
.process("DynamicDecodingContextContainerCodingKey/JSONs"),
3637
]
3738
),
3839
],

Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/DynamicDecodingDictionaryWrapper.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,34 @@ public struct DynamicDecodingDictionaryWrapper<
4040
}
4141
}
4242
}
43+
44+
/// A property wrapper type that strictly decodes a dictionary value of ``DynamicDecodingContextContainerCodingKey``
45+
/// coding key and their dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value and
46+
/// throws error if decoding fails.
47+
///
48+
/// `StrictDynamicDecodingDictionaryWrapper` is a type alias for
49+
/// ``DynamicDecodingDictionaryWrapper``,
50+
/// with ``DynamicDecodingCollectionConfigurationProvider`` as
51+
/// ``StrictCollectionConfiguration``
52+
public typealias StrictDynamicDecodingDictionaryWrapper<
53+
ContainerCodingKey: DynamicDecodingContextContainerCodingKey
54+
> = DynamicDecodingDictionaryWrapper<
55+
ContainerCodingKey,
56+
StrictCollectionConfiguration
57+
> where ContainerCodingKey: Hashable
58+
59+
/// A property wrapper type that decodes valid data into a dictionary value of
60+
/// ``DynamicDecodingContextContainerCodingKey`` coding key and
61+
/// their dynamic ``DynamicDecodingContextContainerCodingKey/Contained``
62+
/// value while ignoring invalid data.
63+
///
64+
/// `LossyDynamicDecodingDictionaryWrapper` is a type alias for
65+
/// ``DynamicDecodingDictionaryWrapper``,
66+
/// with ``DynamicDecodingCollectionConfigurationProvider`` as
67+
/// ``LossyCollectionConfiguration``
68+
public typealias LossyDynamicDecodingDictionaryWrapper<
69+
ContainerCodingKey: DynamicDecodingContextContainerCodingKey
70+
> = DynamicDecodingDictionaryWrapper<
71+
ContainerCodingKey,
72+
LossyCollectionConfiguration
73+
> where ContainerCodingKey: Hashable

Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/PathCodingKeyDefaultValueWrapper.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@propertyWrapper
1212
public struct PathCodingKeyDefaultValueWrapper<
1313
Value: DynamicDecodingDefaultValueProvider
14-
> where Value.Wrapped: CodingKey {
14+
>: Decodable where Value.Wrapped: CodingKey {
1515
/// The underlying ``DynamicDecodingDefaultValueProvider``
1616
/// that wraps coding key value referenced.
1717
public var wrappedValue: Value
@@ -39,6 +39,27 @@ public struct PathCodingKeyDefaultValueWrapper<
3939
}
4040
}
4141

42+
public extension KeyedDecodingContainer {
43+
/// Decodes a value of the type ``DynamicDecodingDefaultValueProvider``
44+
/// for the given ``DynamicDecodingDefaultValueProvider`` type.
45+
///
46+
/// - Parameters:
47+
/// - type: The type of value to decode.
48+
/// - key: The coding key.
49+
///
50+
/// - Returns: A value of the type ``DynamicDecodingDefaultValueProvider``
51+
/// for the given ``DynamicDecodingDefaultValueProvider`` type.
52+
func decode<Value: DynamicDecodingDefaultValueProvider>(
53+
_ type: PathCodingKeyDefaultValueWrapper<Value>.Type,
54+
forKey key: K
55+
) -> PathCodingKeyDefaultValueWrapper<Value> {
56+
guard
57+
let value = self.codingKeyFromPath(ofType: Value.Wrapped.self)
58+
else { return .init(wrappedValue: .default) }
59+
return .init(wrappedValue: .init(value))
60+
}
61+
}
62+
4263
public extension KeyedDecodingContainerProtocol {
4364
/// Decodes a value of the type ``DynamicDecodingDefaultValueProvider``
4465
/// for the given ``DynamicDecodingDefaultValueProvider`` type.

Sources/DynamicCodableKit/DynamicDecodingContextContainerCodingKey/PathCodingKeyWrapper.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ public struct PathCodingKeyWrapper<Key: CodingKey>: Decodable {
2525
}
2626
}
2727

28+
public extension KeyedDecodingContainer {
29+
/// Decodes a value of the type ``PathCodingKeyWrapper``
30+
/// for the given `PathKey` coding key.
31+
///
32+
/// - Parameters:
33+
/// - type: The type of value to decode.
34+
/// - key: The coding key.
35+
///
36+
/// - Returns: A value of the type ``PathCodingKeyWrapper``
37+
/// for the given `PathKey` coding key.
38+
func decode<PathKey: CodingKey>(
39+
_ type: PathCodingKeyWrapper<PathKey>.Type,
40+
forKey key: K
41+
) throws -> PathCodingKeyWrapper<PathKey> {
42+
return try .init(
43+
wrappedValue: self.codingKeyFromPath(ofType: PathKey.self)
44+
)
45+
}
46+
}
47+
2848
public extension KeyedDecodingContainerProtocol {
2949
/// Decodes a value of the type ``PathCodingKeyWrapper``
3050
/// for the given `PathKey` coding key.

Tests/DynamicCodableKitTests/DynamicDecodable.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,42 @@ final class DynamicDecodableTests: XCTestCase {
88
let value: Decodable = try 5.castAs(type: Decodable.self, codingPath: [])
99
XCTAssertEqual(value as? Int, 5)
1010
}
11+
1112
func testDefaultDownCastingFailure() throws {
1213
XCTAssertThrowsError(try 5.castAs(type: String.self, codingPath: []))
1314
}
15+
1416
func testDefaultOptionalDownCasting() throws {
1517
let value: Decodable? = 5.castAs(type: Decodable?.self, codingPath: [])
1618
XCTAssertEqual(value as? Int, 5)
1719
}
20+
1821
func testDefaultOptionalDownCastingFailure() throws {
1922
let value: String? = 5.castAs(type: String?.self, codingPath: [])
2023
XCTAssertNil(value)
2124
}
25+
2226
func testDefaultCollectionDownCasting() throws {
2327
let value: [Decodable] = try [5, 6, 7].castAs(type: [Decodable].self, codingPath: [])
2428
XCTAssertEqual(value as! Array<Int>, [5, 6, 7])
2529
let set: Set<AnyHashable> = try ([5, 6, 7] as Set).castAs(type: Set<AnyHashable>.self, codingPath: [])
2630
XCTAssertEqual(set, [5, 6, 7] as Set)
2731
}
32+
2833
func testDefaultCollectionCastingForSingleValue() throws {
2934
let value: [Decodable] = try 5.castAs(type: [Decodable].self, codingPath: [])
3035
XCTAssertEqual(value as! Array<Int>, [5])
3136
let set: Set<AnyHashable> = try 5.castAs(type: Set<AnyHashable>.self, codingPath: [])
3237
XCTAssertEqual(set, [5] as Set)
3338
}
39+
3440
func testDefaultCollectionDownCastingFailure() throws {
3541
XCTAssertThrowsError(try [5, 6, 7].castAs(type: [String].self, codingPath: []))
3642
}
43+
3744
func testCastingToExistential() throws {
3845
let textPost = TextPost(
3946
id: UUID(),
40-
type: .text,
4147
author: UUID(),
4248
likes: 78,
4349
createdAt: "2021-07-23T07:36:43Z",
@@ -46,10 +52,10 @@ final class DynamicDecodableTests: XCTestCase {
4652
let post = try textPost.castAs(type: Post.self, codingPath: [])
4753
XCTAssertEqual(post.type, .text)
4854
}
55+
4956
func testCastingToBoxType() throws {
5057
let textPost = TextPost(
5158
id: UUID(),
52-
type: .text,
5359
author: UUID(),
5460
likes: 78,
5561
createdAt: "2021-07-23T07:36:43Z",
@@ -58,10 +64,10 @@ final class DynamicDecodableTests: XCTestCase {
5864
let post = try textPost.castAs(type: AnyPost<Post>.self, codingPath: [])
5965
XCTAssertEqual(post.type, .text)
6066
}
67+
6168
func testOptionalCastingToExistential() throws {
6269
let textPost = TextPost(
6370
id: UUID(),
64-
type: .text,
6571
author: UUID(),
6672
likes: 78,
6773
createdAt: "2021-07-23T07:36:43Z",
@@ -70,10 +76,10 @@ final class DynamicDecodableTests: XCTestCase {
7076
let post = textPost.castAs(type: Post?.self, codingPath: [])
7177
XCTAssertEqual(post?.type, .text)
7278
}
79+
7380
func testOptionalCastingToBoxType() throws {
7481
let textPost = TextPost(
7582
id: UUID(),
76-
type: .text,
7783
author: UUID(),
7884
likes: 78,
7985
createdAt: "2021-07-23T07:36:43Z",
@@ -82,11 +88,11 @@ final class DynamicDecodableTests: XCTestCase {
8288
let post = textPost.castAs(type: AnyPost<Post>?.self, codingPath: [])
8389
XCTAssertEqual(post?.type, .text)
8490
}
91+
8592
func testArrayCastingToExistentialArray() throws {
8693
let textPosts = Array(
8794
repeating: TextPost(
8895
id: UUID(),
89-
type: .text,
9096
author: UUID(),
9197
likes: 78,
9298
createdAt: "2021-07-23T07:36:43Z",
@@ -97,11 +103,11 @@ final class DynamicDecodableTests: XCTestCase {
97103
let posts = try textPosts.castAs(type: [Post].self, codingPath: [])
98104
posts.forEach { XCTAssertEqual($0.type, .text) }
99105
}
106+
100107
func testArrayCastingToBoxTypeArray() throws {
101108
let textPosts = Array(
102109
repeating: TextPost(
103110
id: UUID(),
104-
type: .text,
105111
author: UUID(),
106112
likes: 78,
107113
createdAt: "2021-07-23T07:36:43Z",
@@ -112,27 +118,25 @@ final class DynamicDecodableTests: XCTestCase {
112118
let posts = try textPosts.castAs(type: [AnyPost<Post>].self, codingPath: [])
113119
posts.forEach { XCTAssertEqual($0.type, .text) }
114120
}
121+
115122
func testSetCastingToBoxTypeSet() throws {
116123
let textPosts: Set<TextPost> = [
117124
TextPost(
118125
id: UUID(),
119-
type: .text,
120126
author: UUID(),
121127
likes: 78,
122128
createdAt: "2021-07-23T07:36:43Z",
123129
text: "Lorem Ipsium"
124130
),
125131
TextPost(
126132
id: UUID(),
127-
type: .text,
128133
author: UUID(),
129134
likes: 88,
130135
createdAt: "2021-06-23T07:36:43Z",
131136
text: "Lorem Ipsium"
132137
),
133138
TextPost(
134139
id: UUID(),
135-
type: .text,
136140
author: UUID(),
137141
likes: 887,
138142
createdAt: "2021-06-28T07:36:43Z",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import XCTest
2+
@testable import DynamicCodableKit
3+
4+
final class DynamicDecodingCollectionDictionaryWrapperTests: XCTestCase {
5+
func testDecoding() throws {
6+
let url = Bundle.module.url(forResource: "container-decode", withExtension: "json")!
7+
let data = try Data(contentsOf: url)
8+
let decoder = JSONDecoder()
9+
let postPage = try decoder.decode(ThrowingKeyedPostPage.self, from: data)
10+
XCTAssertEqual(postPage.content.count, 4)
11+
XCTAssertEqual(Set(postPage.content.map(\.value.type)), Set([.text, .picture, .audio, .video]))
12+
postPage.content.forEach { XCTAssertEqual($1.type, $0) }
13+
}
14+
15+
func testInvalidDataDecodingWithThrowConfig() throws {
16+
let url = Bundle.module.url(forResource: "container-decode-with-invalid-data", withExtension: "json")!
17+
let data = try Data(contentsOf: url)
18+
let decoder = JSONDecoder()
19+
XCTAssertThrowsError(try decoder.decode(ThrowingKeyedPostPage.self, from: data))
20+
}
21+
22+
func testInvalidDataDecodingWithLossyConfig() throws {
23+
let url = Bundle.module.url(forResource: "container-decode-with-invalid-data", withExtension: "json")!
24+
let data = try Data(contentsOf: url)
25+
let decoder = JSONDecoder()
26+
let postPage = try decoder.decode(LossyKeyedPostPage.self, from: data)
27+
XCTAssertEqual(postPage.content.count, 3)
28+
XCTAssertEqual(Set(postPage.content.map(\.value.type)), Set([.text, .picture, .video]))
29+
postPage.content.forEach { XCTAssertEqual($1.type, $0) }
30+
}
31+
}
32+
33+
struct ThrowingKeyedPostPage: Decodable {
34+
let next: URL
35+
@StrictDynamicDecodingDictionaryWrapper<PostType> var content: [PostType: Post]
36+
}
37+
38+
struct LossyKeyedPostPage: Decodable {
39+
let next: URL
40+
@LossyDynamicDecodingDictionaryWrapper<PostType> var content: [PostType: Post]
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import XCTest
2+
@testable import DynamicCodableKit
3+
4+
final class DynamicDecodingDictionaryWrapperTests: XCTestCase {
5+
func testDecoding() throws {
6+
let url = Bundle.module.url(forResource: "container-collection-decode", withExtension: "json")!
7+
let data = try Data(contentsOf: url)
8+
let decoder = JSONDecoder()
9+
let postPage = try decoder.decode(ThrowingKeyedPostPageCollection.self, from: data)
10+
XCTAssertEqual(postPage.content.count, 4)
11+
postPage.content.forEach { type, posts in
12+
XCTAssertEqual(posts.count, 3)
13+
posts.forEach { XCTAssertEqual($0.type, type) }
14+
}
15+
}
16+
17+
func testInvalidDataDecodingWithThrowConfig() throws {
18+
let url = Bundle.module.url(forResource: "container-collection-decode-with-invalid-data", withExtension: "json")!
19+
let data = try Data(contentsOf: url)
20+
let decoder = JSONDecoder()
21+
XCTAssertThrowsError(try decoder.decode(ThrowingKeyedPostPageCollection.self, from: data))
22+
}
23+
24+
func testInvalidDataDecodingWithLossyConfig() throws {
25+
let url = Bundle.module.url(forResource: "container-collection-decode-with-invalid-data", withExtension: "json")!
26+
let data = try Data(contentsOf: url)
27+
let decoder = JSONDecoder()
28+
let postPage = try decoder.decode(LossyKeyedPostPageCollection.self, from: data)
29+
XCTAssertEqual(postPage.content.count, 4)
30+
postPage.content.forEach { type, posts in
31+
switch type {
32+
case .audio, .video:
33+
XCTAssertEqual(posts.count, 2)
34+
default:
35+
XCTAssertEqual(posts.count, 3)
36+
}
37+
posts.forEach { XCTAssertEqual($0.type, type) }
38+
}
39+
}
40+
}
41+
42+
struct ThrowingKeyedPostPageCollection: Decodable {
43+
let next: URL
44+
@StrictDynamicDecodingArrayDictionaryWrapper<PostType> var content: [PostType: [Post]]
45+
}
46+
47+
struct LossyKeyedPostPageCollection: Decodable {
48+
let next: URL
49+
@LossyDynamicDecodingArrayDictionaryWrapper<PostType> var content: [PostType: [Post]]
50+
}

0 commit comments

Comments
 (0)