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

Commit

Permalink
test: add tests for dynamic JSON decoding with type(s) parent coding key
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Jun 4, 2022
1 parent 9fc73b7 commit c222eb1
Show file tree
Hide file tree
Showing 13 changed files with 556 additions and 15 deletions.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ let package = Package(
dependencies: ["DynamicCodableKit"],
resources: [
.process("DynamicDecodingContextCodingKey/JSONs"),
.process("DynamicDecodingContextContainerCodingKey/JSONs"),
]
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,34 @@ public struct DynamicDecodingDictionaryWrapper<
}
}
}

/// A property wrapper type that strictly decodes a dictionary value of ``DynamicDecodingContextContainerCodingKey``
/// coding key and their dynamic ``DynamicDecodingContextContainerCodingKey/Contained`` value and
/// throws error if decoding fails.
///
/// `StrictDynamicDecodingDictionaryWrapper` is a type alias for
/// ``DynamicDecodingDictionaryWrapper``,
/// with ``DynamicDecodingCollectionConfigurationProvider`` as
/// ``StrictCollectionConfiguration``
public typealias StrictDynamicDecodingDictionaryWrapper<
ContainerCodingKey: DynamicDecodingContextContainerCodingKey
> = DynamicDecodingDictionaryWrapper<
ContainerCodingKey,
StrictCollectionConfiguration
> where ContainerCodingKey: Hashable

/// A property wrapper type that decodes valid data into a dictionary value of
/// ``DynamicDecodingContextContainerCodingKey`` coding key and
/// their dynamic ``DynamicDecodingContextContainerCodingKey/Contained``
/// value while ignoring invalid data.
///
/// `LossyDynamicDecodingDictionaryWrapper` is a type alias for
/// ``DynamicDecodingDictionaryWrapper``,
/// with ``DynamicDecodingCollectionConfigurationProvider`` as
/// ``LossyCollectionConfiguration``
public typealias LossyDynamicDecodingDictionaryWrapper<
ContainerCodingKey: DynamicDecodingContextContainerCodingKey
> = DynamicDecodingDictionaryWrapper<
ContainerCodingKey,
LossyCollectionConfiguration
> where ContainerCodingKey: Hashable
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@propertyWrapper
public struct PathCodingKeyDefaultValueWrapper<
Value: DynamicDecodingDefaultValueProvider
> where Value.Wrapped: CodingKey {
>: Decodable where Value.Wrapped: CodingKey {
/// The underlying ``DynamicDecodingDefaultValueProvider``
/// that wraps coding key value referenced.
public var wrappedValue: Value
Expand Down Expand Up @@ -39,6 +39,27 @@ public struct PathCodingKeyDefaultValueWrapper<
}
}

public extension KeyedDecodingContainer {
/// Decodes a value of the type ``DynamicDecodingDefaultValueProvider``
/// for the given ``DynamicDecodingDefaultValueProvider`` type.
///
/// - Parameters:
/// - type: The type of value to decode.
/// - key: The coding key.
///
/// - Returns: A value of the type ``DynamicDecodingDefaultValueProvider``
/// for the given ``DynamicDecodingDefaultValueProvider`` type.
func decode<Value: DynamicDecodingDefaultValueProvider>(
_ type: PathCodingKeyDefaultValueWrapper<Value>.Type,
forKey key: K
) -> PathCodingKeyDefaultValueWrapper<Value> {
guard
let value = self.codingKeyFromPath(ofType: Value.Wrapped.self)
else { return .init(wrappedValue: .default) }
return .init(wrappedValue: .init(value))
}
}

public extension KeyedDecodingContainerProtocol {
/// Decodes a value of the type ``DynamicDecodingDefaultValueProvider``
/// for the given ``DynamicDecodingDefaultValueProvider`` type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ public struct PathCodingKeyWrapper<Key: CodingKey>: Decodable {
}
}

public extension KeyedDecodingContainer {
/// Decodes a value of the type ``PathCodingKeyWrapper``
/// for the given `PathKey` coding key.
///
/// - Parameters:
/// - type: The type of value to decode.
/// - key: The coding key.
///
/// - Returns: A value of the type ``PathCodingKeyWrapper``
/// for the given `PathKey` coding key.
func decode<PathKey: CodingKey>(
_ type: PathCodingKeyWrapper<PathKey>.Type,
forKey key: K
) throws -> PathCodingKeyWrapper<PathKey> {
return try .init(
wrappedValue: self.codingKeyFromPath(ofType: PathKey.self)
)
}
}

public extension KeyedDecodingContainerProtocol {
/// Decodes a value of the type ``PathCodingKeyWrapper``
/// for the given `PathKey` coding key.
Expand Down
22 changes: 13 additions & 9 deletions Tests/DynamicCodableKitTests/DynamicDecodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,42 @@ final class DynamicDecodableTests: XCTestCase {
let value: Decodable = try 5.castAs(type: Decodable.self, codingPath: [])
XCTAssertEqual(value as? Int, 5)
}

func testDefaultDownCastingFailure() throws {
XCTAssertThrowsError(try 5.castAs(type: String.self, codingPath: []))
}

func testDefaultOptionalDownCasting() throws {
let value: Decodable? = 5.castAs(type: Decodable?.self, codingPath: [])
XCTAssertEqual(value as? Int, 5)
}

func testDefaultOptionalDownCastingFailure() throws {
let value: String? = 5.castAs(type: String?.self, codingPath: [])
XCTAssertNil(value)
}

func testDefaultCollectionDownCasting() throws {
let value: [Decodable] = try [5, 6, 7].castAs(type: [Decodable].self, codingPath: [])
XCTAssertEqual(value as! Array<Int>, [5, 6, 7])
let set: Set<AnyHashable> = try ([5, 6, 7] as Set).castAs(type: Set<AnyHashable>.self, codingPath: [])
XCTAssertEqual(set, [5, 6, 7] as Set)
}

func testDefaultCollectionCastingForSingleValue() throws {
let value: [Decodable] = try 5.castAs(type: [Decodable].self, codingPath: [])
XCTAssertEqual(value as! Array<Int>, [5])
let set: Set<AnyHashable> = try 5.castAs(type: Set<AnyHashable>.self, codingPath: [])
XCTAssertEqual(set, [5] as Set)
}

func testDefaultCollectionDownCastingFailure() throws {
XCTAssertThrowsError(try [5, 6, 7].castAs(type: [String].self, codingPath: []))
}

func testCastingToExistential() throws {
let textPost = TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
Expand All @@ -46,10 +52,10 @@ final class DynamicDecodableTests: XCTestCase {
let post = try textPost.castAs(type: Post.self, codingPath: [])
XCTAssertEqual(post.type, .text)
}

func testCastingToBoxType() throws {
let textPost = TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
Expand All @@ -58,10 +64,10 @@ final class DynamicDecodableTests: XCTestCase {
let post = try textPost.castAs(type: AnyPost<Post>.self, codingPath: [])
XCTAssertEqual(post.type, .text)
}

func testOptionalCastingToExistential() throws {
let textPost = TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
Expand All @@ -70,10 +76,10 @@ final class DynamicDecodableTests: XCTestCase {
let post = textPost.castAs(type: Post?.self, codingPath: [])
XCTAssertEqual(post?.type, .text)
}

func testOptionalCastingToBoxType() throws {
let textPost = TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
Expand All @@ -82,11 +88,11 @@ final class DynamicDecodableTests: XCTestCase {
let post = textPost.castAs(type: AnyPost<Post>?.self, codingPath: [])
XCTAssertEqual(post?.type, .text)
}

func testArrayCastingToExistentialArray() throws {
let textPosts = Array(
repeating: TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
Expand All @@ -97,11 +103,11 @@ final class DynamicDecodableTests: XCTestCase {
let posts = try textPosts.castAs(type: [Post].self, codingPath: [])
posts.forEach { XCTAssertEqual($0.type, .text) }
}

func testArrayCastingToBoxTypeArray() throws {
let textPosts = Array(
repeating: TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
Expand All @@ -112,27 +118,25 @@ final class DynamicDecodableTests: XCTestCase {
let posts = try textPosts.castAs(type: [AnyPost<Post>].self, codingPath: [])
posts.forEach { XCTAssertEqual($0.type, .text) }
}

func testSetCastingToBoxTypeSet() throws {
let textPosts: Set<TextPost> = [
TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 78,
createdAt: "2021-07-23T07:36:43Z",
text: "Lorem Ipsium"
),
TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 88,
createdAt: "2021-06-23T07:36:43Z",
text: "Lorem Ipsium"
),
TextPost(
id: UUID(),
type: .text,
author: UUID(),
likes: 887,
createdAt: "2021-06-28T07:36:43Z",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import XCTest
@testable import DynamicCodableKit

final class DynamicDecodingCollectionDictionaryWrapperTests: XCTestCase {
func testDecoding() throws {
let url = Bundle.module.url(forResource: "container-decode", withExtension: "json")!
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let postPage = try decoder.decode(ThrowingKeyedPostPage.self, from: data)
XCTAssertEqual(postPage.content.count, 4)
XCTAssertEqual(Set(postPage.content.map(\.value.type)), Set([.text, .picture, .audio, .video]))
postPage.content.forEach { XCTAssertEqual($1.type, $0) }
}

func testInvalidDataDecodingWithThrowConfig() throws {
let url = Bundle.module.url(forResource: "container-decode-with-invalid-data", withExtension: "json")!
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode(ThrowingKeyedPostPage.self, from: data))
}

func testInvalidDataDecodingWithLossyConfig() throws {
let url = Bundle.module.url(forResource: "container-decode-with-invalid-data", withExtension: "json")!
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let postPage = try decoder.decode(LossyKeyedPostPage.self, from: data)
XCTAssertEqual(postPage.content.count, 3)
XCTAssertEqual(Set(postPage.content.map(\.value.type)), Set([.text, .picture, .video]))
postPage.content.forEach { XCTAssertEqual($1.type, $0) }
}
}

struct ThrowingKeyedPostPage: Decodable {
let next: URL
@StrictDynamicDecodingDictionaryWrapper<PostType> var content: [PostType: Post]
}

struct LossyKeyedPostPage: Decodable {
let next: URL
@LossyDynamicDecodingDictionaryWrapper<PostType> var content: [PostType: Post]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import XCTest
@testable import DynamicCodableKit

final class DynamicDecodingDictionaryWrapperTests: XCTestCase {
func testDecoding() throws {
let url = Bundle.module.url(forResource: "container-collection-decode", withExtension: "json")!
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let postPage = try decoder.decode(ThrowingKeyedPostPageCollection.self, from: data)
XCTAssertEqual(postPage.content.count, 4)
postPage.content.forEach { type, posts in
XCTAssertEqual(posts.count, 3)
posts.forEach { XCTAssertEqual($0.type, type) }
}
}

func testInvalidDataDecodingWithThrowConfig() throws {
let url = Bundle.module.url(forResource: "container-collection-decode-with-invalid-data", withExtension: "json")!
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode(ThrowingKeyedPostPageCollection.self, from: data))
}

func testInvalidDataDecodingWithLossyConfig() throws {
let url = Bundle.module.url(forResource: "container-collection-decode-with-invalid-data", withExtension: "json")!
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let postPage = try decoder.decode(LossyKeyedPostPageCollection.self, from: data)
XCTAssertEqual(postPage.content.count, 4)
postPage.content.forEach { type, posts in
switch type {
case .audio, .video:
XCTAssertEqual(posts.count, 2)
default:
XCTAssertEqual(posts.count, 3)
}
posts.forEach { XCTAssertEqual($0.type, type) }
}
}
}

struct ThrowingKeyedPostPageCollection: Decodable {
let next: URL
@StrictDynamicDecodingArrayDictionaryWrapper<PostType> var content: [PostType: [Post]]
}

struct LossyKeyedPostPageCollection: Decodable {
let next: URL
@LossyDynamicDecodingArrayDictionaryWrapper<PostType> var content: [PostType: [Post]]
}
Loading

0 comments on commit c222eb1

Please sign in to comment.