From 3d1ac40fa70c87105233c5f98edefe8e07eaf77d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 7 May 2023 21:37:46 +0200 Subject: [PATCH] Implemented the LossyCodableList property wrapper. --- .../Property Wrappers/LossyCodableList.swift | 89 +++++++++++++++++++ .../LossyCodableList+DecodableTests.swift | 75 ++++++++++++++++ .../LossyCodableList+EncodableTests.swift | 80 +++++++++++++++++ .../Core/Helpers/Extensions/Data+Result.swift | 21 +++++ Tests/Core/Helpers/Extensions/Data+Seed.swift | 21 +++++ .../Helpers/Extensions/String+Result.swift | 19 ++++ .../Core/Helpers/Extensions/String+Seed.swift | 19 ++++ Tests/Core/Helpers/Models/TestCodable.swift | 30 +++++++ .../Core/Helpers/Models/TestCodableList.swift | 17 ++++ 9 files changed, 371 insertions(+) create mode 100644 Sources/Core/Property Wrappers/LossyCodableList.swift create mode 100644 Tests/Core/Cases/Property Wrappers/LossyCodableList+DecodableTests.swift create mode 100644 Tests/Core/Cases/Property Wrappers/LossyCodableList+EncodableTests.swift create mode 100644 Tests/Core/Helpers/Extensions/Data+Result.swift create mode 100644 Tests/Core/Helpers/Extensions/Data+Seed.swift create mode 100644 Tests/Core/Helpers/Extensions/String+Result.swift create mode 100644 Tests/Core/Helpers/Extensions/String+Seed.swift create mode 100644 Tests/Core/Helpers/Models/TestCodable.swift create mode 100644 Tests/Core/Helpers/Models/TestCodableList.swift diff --git a/Sources/Core/Property Wrappers/LossyCodableList.swift b/Sources/Core/Property Wrappers/LossyCodableList.swift new file mode 100644 index 0000000..f052718 --- /dev/null +++ b/Sources/Core/Property Wrappers/LossyCodableList.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +@propertyWrapper +/// This property wrapper provides a generic type that acts as a thin wrapper around an array of `Elements` instances to allow a lossy decoding and or encoding process. +public struct LossyCodableList { + + // MARK: Properties + + private var elements: [Element] + + /// Provides read/write access to the array of `Element` instances. + public var wrappedValue: [Element] { + get { elements } + set { elements = newValue } + } + + // MARK: Initialisers + + /// Initialises this property wrapper. + public init() { + self.elements = [] + } + +} + +// MARK: - Decodable + +extension LossyCodableList: Decodable where Element: Decodable { + + // MARK: Initialisers + + /// Initialises the struct with a lossy decoder. + /// - Parameter decoder: The decoder to use for the lossy decoder process. + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let wrappers = try container.decode([ElementWrapper].self) + + self.elements = wrappers.compactMap(\.element) + } + +} + +// MARK: - Encodable + +extension LossyCodableList: Encodable where Element: Encodable { + + // MARK: Functions + + /// Encodes an array of `Element` instances loosely. + /// - Parameter encoder: The encoder to use for the lossy encoding process. + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + elements.forEach { element in + try? container.encode(element) + } + } + +} + +// MARK: - Structs + +private extension LossyCodableList where Element: Decodable { + struct ElementWrapper: Decodable { + + // MARK: Properties + + var element: Element? + + // MARK: Initialisers + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + self.element = try? container.decode(Element.self) + } + + } +} diff --git a/Tests/Core/Cases/Property Wrappers/LossyCodableList+DecodableTests.swift b/Tests/Core/Cases/Property Wrappers/LossyCodableList+DecodableTests.swift new file mode 100644 index 0000000..55198a6 --- /dev/null +++ b/Tests/Core/Cases/Property Wrappers/LossyCodableList+DecodableTests.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Core +import Foundation +import XCTest + +final class LossyCodableList_DecodableTests: XCTestCase { + + // MARK: Properties + + private let decoder = JSONDecoder() + + private var dataToDecode: Data! + private var decodedList: TestCodableList! + + // MARK: Tests + + func test_decode_whenAllDataIsComplete() throws { + // GIVEN + dataToDecode = .Seed.itemsWithAllKeysHavingIntValues + + // WHEN + decodedList = try decoder.decode(TestCodableList.self, from: dataToDecode) + + // THEN + XCTAssertNotNil(decodedList) + XCTAssertTrue(decodedList.items.isNotEmpty) + XCTAssertEqual(decodedList.items, [ + .init(key: "One", value: 1), + .init(key: "Two", value: 2), + .init(key: "Three", value: 3), + .init(key: "Four", value: 4) + ]) + } + + func test_decode_whenSomeDataHasNil() throws { + // GIVEN + dataToDecode = .Seed.itemsWithSomeKeysAndValuesAreNil + + // WHEN + decodedList = try decoder.decode(TestCodableList.self, from: dataToDecode) + + // THEN + XCTAssertNotNil(decodedList) + XCTAssertTrue(decodedList.items.isNotEmpty) + XCTAssertEqual(decodedList.items, [ + .init(key: "One", value: 1), + .init(key: "Three", value: 3) + ]) + } + + func test_decode_whenAllDataHasNil() throws { + // GIVEN + dataToDecode = .Seed.itemsWithAllKeysAndValuesAreNil + + // WHEN + decodedList = try decoder.decode(TestCodableList.self, from: dataToDecode) + + // THEN + XCTAssertNotNil(decodedList) + XCTAssertTrue(decodedList.items.isEmpty) + XCTAssertEqual(decodedList.items, []) + } + +} diff --git a/Tests/Core/Cases/Property Wrappers/LossyCodableList+EncodableTests.swift b/Tests/Core/Cases/Property Wrappers/LossyCodableList+EncodableTests.swift new file mode 100644 index 0000000..468a3cd --- /dev/null +++ b/Tests/Core/Cases/Property Wrappers/LossyCodableList+EncodableTests.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +import XCTest + +@testable import Core + +final class LossyCodableList_EncodableTests: XCTestCase { + + // MARK: Properties + + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + private var dataToDecode: Data! + private var encodedData: Data! + private var list: TestCodableList! + + // MARK: Setup + + override func setUpWithError() throws { + // This setting is used to guarantee that the properties of the model are being generated by sorted keys order. + encoder.outputFormatting = .sortedKeys + } + + // MARK: Tests + + func test_encode_whenAllKeysHaveIntValues() throws { + // GIVEN + dataToDecode = .Seed.itemsWithAllKeysHavingIntValues + list = try decoder.decode(TestCodableList.self, from: dataToDecode) + + // WHEN + encodedData = try encoder.encode(list) + + // THEN + XCTAssertNotNil(encodedData) + XCTAssertTrue(encodedData.isNotEmpty) + XCTAssertEqual(encodedData, .Result.allItemsNotFilteredOut) + } + + func test_encode_whenSomeKeysAndValuesAreNil() throws { + // GIVEN + dataToDecode = .Seed.itemsWithSomeKeysAndValuesAreNil + list = try decoder.decode(TestCodableList.self, from: dataToDecode) + + // WHEN + encodedData = try encoder.encode(list) + + // THEN + XCTAssertNotNil(encodedData) + XCTAssertTrue(encodedData.isNotEmpty) + XCTAssertEqual(encodedData, .Result.someItemsFilteredOut) + } + + func test_encode_whenAllKeysAndValuesAreNil() throws { + // GIVEN + dataToDecode = .Seed.itemsWithAllKeysAndValuesAreNil + list = try decoder.decode(TestCodableList.self, from: dataToDecode) + + // WHEN + encodedData = try encoder.encode(list) + + // THEN + XCTAssertNotNil(encodedData) + XCTAssertTrue(encodedData.isNotEmpty) + XCTAssertEqual(encodedData, .Result.allItemsFilteredOut) + } + +} diff --git a/Tests/Core/Helpers/Extensions/Data+Result.swift b/Tests/Core/Helpers/Extensions/Data+Result.swift new file mode 100644 index 0000000..0d9bcbd --- /dev/null +++ b/Tests/Core/Helpers/Extensions/Data+Result.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension Data { + enum Result { + static let allItemsNotFilteredOut = String.Result.allItemsNotFilteredOut.data(using: .utf8) + static let someItemsFilteredOut = String.Result.someItemsFilteredOut.data(using: .utf8) + static let allItemsFilteredOut = String.Result.allItemsFilteredOut.data(using: .utf8) + } +} diff --git a/Tests/Core/Helpers/Extensions/Data+Seed.swift b/Tests/Core/Helpers/Extensions/Data+Seed.swift new file mode 100644 index 0000000..455c97b --- /dev/null +++ b/Tests/Core/Helpers/Extensions/Data+Seed.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension Data { + enum Seed { + static let itemsWithAllKeysHavingIntValues = String.Seed.itemsWithAllKeysHavingIntValues.data(using: .utf8) + static let itemsWithSomeKeysAndValuesAreNil = String.Seed.itemsWithSomeKeysAndValuesAreNil.data(using: .utf8) + static let itemsWithAllKeysAndValuesAreNil = String.Seed.itemsWithAllKeysAndValuesAreNil.data(using: .utf8) + } +} diff --git a/Tests/Core/Helpers/Extensions/String+Result.swift b/Tests/Core/Helpers/Extensions/String+Result.swift new file mode 100644 index 0000000..46265aa --- /dev/null +++ b/Tests/Core/Helpers/Extensions/String+Result.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +extension String { + enum Result { + static let allItemsNotFilteredOut = "{\"items\":[{\"key\":\"One\",\"value\":1},{\"key\":\"Two\",\"value\":2},{\"key\":\"Three\",\"value\":3},{\"key\":\"Four\",\"value\":4}]}" + static let someItemsFilteredOut = "{\"items\":[{\"key\":\"One\",\"value\":1},{\"key\":\"Three\",\"value\":3}]}" + static let allItemsFilteredOut = "{\"items\":[]}" + } +} diff --git a/Tests/Core/Helpers/Extensions/String+Seed.swift b/Tests/Core/Helpers/Extensions/String+Seed.swift new file mode 100644 index 0000000..4a8f8bf --- /dev/null +++ b/Tests/Core/Helpers/Extensions/String+Seed.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +extension String { + enum Seed { + static let itemsWithAllKeysHavingIntValues = "{\"items\":[{\"key\":\"One\",\"value\":1},{\"key\":\"Two\",\"value\":2},{\"key\":\"Three\",\"value\":3},{\"key\":\"Four\",\"value\":4}]}" + static let itemsWithSomeKeysAndValuesAreNil = "{\"items\":[{\"key\":\"One\",\"value\":1},{\"key\":\"Two\",\"value\":null},{\"key\":\"Three\",\"value\":3},{\"key\":null,\"value\":4}]}" + static let itemsWithAllKeysAndValuesAreNil = "{\"items\":[{\"key\":\"One\",\"value\":null},{\"key\":null,\"value\":2},{\"key\":\"Three\",\"value\":null},{\"key\":null,\"value\":4}]}" + } +} diff --git a/Tests/Core/Helpers/Models/TestCodable.swift b/Tests/Core/Helpers/Models/TestCodable.swift new file mode 100644 index 0000000..b2ab6d9 --- /dev/null +++ b/Tests/Core/Helpers/Models/TestCodable.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +struct TestCodable: Codable, Equatable { + let key: String + let value: Int +} + +// MARK: - Initialisers + +extension TestCodable { + init?( + key: String? = nil, + value: Int? = nil + ) { + guard let key, let value else { return nil } + + self.key = key + self.value = value + } +} diff --git a/Tests/Core/Helpers/Models/TestCodableList.swift b/Tests/Core/Helpers/Models/TestCodableList.swift new file mode 100644 index 0000000..e015e75 --- /dev/null +++ b/Tests/Core/Helpers/Models/TestCodableList.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftLibs open source project +// +// Copyright (c) 2023 Röck+Cöde VoF. and the SwiftLibs project authors +// Licensed under the EUPL 1.2 or later. +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftLibs project authors +// +//===----------------------------------------------------------------------===// + +import Core + +struct TestCodableList: Codable { + @LossyCodableList var items: [TestCodable] +}