2022-05-10 10:26:13 -07:00
|
|
|
//
|
|
|
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the MIT License (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
|
//
|
|
|
|
|
// DecoderExtensionTests.swift
|
|
|
|
|
//
|
|
|
|
|
// Created by Nacho Soto on 4/26/22.
|
|
|
|
|
|
|
|
|
|
import Nimble
|
|
|
|
|
import XCTest
|
|
|
|
|
|
|
|
|
|
@testable import RevenueCat
|
|
|
|
|
|
|
|
|
|
// swiftlint:disable type_name identifier_name nesting
|
|
|
|
|
|
2022-05-10 12:40:40 -07:00
|
|
|
class DecoderExtensionsDefaultValueTests: TestCase {
|
2022-05-10 10:26:13 -07:00
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
|
|
|
|
enum E: String, DefaultValueProvider, Codable, Equatable {
|
|
|
|
|
case e1
|
|
|
|
|
case e2
|
|
|
|
|
|
|
|
|
|
static let defaultValue: Self = .e2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@DefaultValue<E> var e: E
|
|
|
|
|
|
|
|
|
|
init(e: E) {
|
|
|
|
|
self.e = e
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private struct Data2: Codable, Equatable {
|
|
|
|
|
enum DefaultString: DefaultValueProvider {
|
|
|
|
|
static var defaultValue: String = "default"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@DefaultValue<DefaultString> var string: String
|
|
|
|
|
|
|
|
|
|
init(string: String) {
|
|
|
|
|
self.string = string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesActualValue() throws {
|
|
|
|
|
let data = Data(e: .e1)
|
|
|
|
|
let decodedData = try data.encodeAndDecode()
|
|
|
|
|
|
|
|
|
|
expect(decodedData) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultValueIfMissing() throws {
|
|
|
|
|
expect(try Data.decodeEmptyData().e) == Data.E.defaultValue
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
func testThrowsForInvalidValue() throws {
|
2022-05-10 10:35:16 -07:00
|
|
|
let json = "{\"e\": \"e3\"}"
|
2022-05-10 10:26:13 -07:00
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
expect(try Data.decode(json))
|
|
|
|
|
.to(throwError(errorType: DecodingError.self))
|
2022-05-10 10:26:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultValueFromAnotherSource() throws {
|
|
|
|
|
expect(try Data2.decodeEmptyData().string) == Data2.DefaultString.defaultValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-10 12:40:40 -07:00
|
|
|
class DecoderExtensionsIgnoreErrorsTests: TestCase {
|
2022-05-10 10:30:17 -07:00
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
2022-06-07 16:56:04 -07:00
|
|
|
@IgnoreDecodeErrors<URL?> var url: URL?
|
2022-05-10 10:30:17 -07:00
|
|
|
|
|
|
|
|
init(url: URL) {
|
|
|
|
|
self.url = url
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesActualValue() throws {
|
|
|
|
|
let data = Data(url: URL(string: "https://revenuecat.com")!)
|
|
|
|
|
let decodedData = try data.encodeAndDecode()
|
|
|
|
|
|
|
|
|
|
expect(decodedData) == data
|
2023-07-12 06:35:54 -07:00
|
|
|
|
|
|
|
|
self.logger.verifyMessageWasNotLogged("Couldn't decode", allowNoMessages: true)
|
2022-05-10 10:30:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testIgnoresErrors() throws {
|
2023-06-09 09:47:17 -07:00
|
|
|
let json = "{\"url\": 1}"
|
2022-05-10 10:35:16 -07:00
|
|
|
let data = try Data.decode(json)
|
2022-05-10 10:30:17 -07:00
|
|
|
|
|
|
|
|
expect(data.url).to(beNil())
|
2023-07-12 06:35:54 -07:00
|
|
|
|
|
|
|
|
self.logger.verifyMessageWasLogged("Couldn't decode 'Optional<URL>' from json.",
|
|
|
|
|
level: .debug,
|
|
|
|
|
expectedCount: 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testIgnoresMissingValue() throws {
|
|
|
|
|
let data = try Data.decode("{}")
|
|
|
|
|
|
|
|
|
|
expect(data.url).to(beNil())
|
|
|
|
|
|
|
|
|
|
self.logger.verifyMessageWasNotLogged("Couldn't decode", allowNoMessages: true)
|
2022-05-10 10:30:17 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
func testDecodesDefaultValueForInvalidValue() throws {
|
|
|
|
|
struct Data: Codable, Equatable {
|
|
|
|
|
enum E: String, DefaultValueProvider, Codable, Equatable {
|
|
|
|
|
case e1
|
|
|
|
|
case e2
|
|
|
|
|
|
|
|
|
|
static let defaultValue: Self = .e2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IgnoreDecodeErrors<E> var e: E
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let json = "{\"e\": \"e3\"}"
|
|
|
|
|
|
|
|
|
|
let data = try Data.decode(json)
|
|
|
|
|
expect(data.e) == .e2
|
2023-07-12 06:35:54 -07:00
|
|
|
|
|
|
|
|
self.logger.verifyMessageWasLogged("Couldn't decode 'E' from json.",
|
|
|
|
|
level: .debug,
|
|
|
|
|
expectedCount: 1)
|
2022-06-07 16:56:04 -07:00
|
|
|
}
|
|
|
|
|
|
2022-05-10 10:30:17 -07:00
|
|
|
}
|
|
|
|
|
|
2022-05-10 12:40:40 -07:00
|
|
|
class DecoderExtensionsDefaultDecodableTests: TestCase {
|
2022-05-10 10:26:13 -07:00
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
|
|
|
|
@DefaultDecodable.True var bool1: Bool
|
|
|
|
|
@DefaultDecodable.False var bool2: Bool
|
|
|
|
|
@DefaultDecodable.EmptyString var string: String
|
|
|
|
|
@DefaultDecodable.EmptyArray var array: [String]
|
|
|
|
|
@DefaultDecodable.EmptyDictionary var dictionary: [String: Int]
|
2023-03-24 07:41:43 -07:00
|
|
|
@DefaultDecodable.Now var date: Date
|
2022-05-10 10:26:13 -07:00
|
|
|
|
|
|
|
|
init(
|
|
|
|
|
bool1: Bool,
|
|
|
|
|
bool2: Bool,
|
|
|
|
|
string: String,
|
|
|
|
|
array: [String],
|
2023-03-24 07:41:43 -07:00
|
|
|
dictionary: [String: Int],
|
|
|
|
|
date: Date
|
2022-05-10 10:26:13 -07:00
|
|
|
) {
|
|
|
|
|
self.bool1 = bool1
|
|
|
|
|
self.bool2 = bool2
|
|
|
|
|
self.string = string
|
|
|
|
|
self.array = array
|
|
|
|
|
self.dictionary = dictionary
|
2023-03-24 07:41:43 -07:00
|
|
|
self.date = date
|
2022-05-10 10:26:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesActualValues() throws {
|
2023-03-24 07:41:43 -07:00
|
|
|
let data = Data(bool1: false,
|
|
|
|
|
bool2: true,
|
|
|
|
|
string: "test",
|
|
|
|
|
array: ["a", "b"],
|
|
|
|
|
dictionary: ["a": 1],
|
|
|
|
|
date: Date(timeIntervalSince1970: 200000))
|
2022-05-10 10:26:13 -07:00
|
|
|
let decodedData = try data.encodeAndDecode()
|
|
|
|
|
|
|
|
|
|
expect(decodedData) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultTrue() throws {
|
|
|
|
|
expect(try Data.decodeEmptyData().bool1) == true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultFalse() throws {
|
|
|
|
|
expect(try Data.decodeEmptyData().bool2) == false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultString() throws {
|
|
|
|
|
expect(try Data.decodeEmptyData().string) == ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultArray() throws {
|
|
|
|
|
expect(try Data.decodeEmptyData().array) == []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesDefaultDictionary() throws {
|
|
|
|
|
expect(try Data.decodeEmptyData().dictionary) == [:]
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-24 07:41:43 -07:00
|
|
|
func testDecodesDateAsNow() throws {
|
2023-05-17 12:43:14 -04:00
|
|
|
expect(try Data.decodeEmptyData().date).to(beCloseToNow())
|
2023-03-24 07:41:43 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
func testDoesNotIgnoreErrorsIfNotArray() throws {
|
2022-06-07 09:45:51 -07:00
|
|
|
let json = "{\"array\": \"this is not an array\"}"
|
2022-05-10 10:35:16 -07:00
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
expect(try Data.decode(json))
|
|
|
|
|
.to(throwError(errorType: DecodingError.self))
|
2022-05-10 10:35:16 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
func testDoesNotIgnoreErrorsIfNotDictionary() throws {
|
2022-06-07 09:45:51 -07:00
|
|
|
let json = "{\"dictionary\": \"this is not a dictionary\"}"
|
|
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
expect(try Data.decode(json))
|
|
|
|
|
.to(throwError(errorType: DecodingError.self))
|
2022-06-07 09:45:51 -07:00
|
|
|
}
|
|
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
func testDoesNotIgnoreErrorsIfPartiallyFailsToDecodeData() throws {
|
2022-06-07 09:45:51 -07:00
|
|
|
struct Data: Codable, Equatable {
|
2022-06-07 16:56:04 -07:00
|
|
|
struct Content: Codable, Equatable {
|
|
|
|
|
let string: String
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-07 09:45:51 -07:00
|
|
|
@DefaultDecodable.EmptyDictionary var dictionary: [String: Content]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let json = """
|
|
|
|
|
{
|
|
|
|
|
"dictionary": {
|
|
|
|
|
"1\": { "invalid_key": false },
|
|
|
|
|
"2\": { "string": "value" }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"""
|
2022-05-10 10:35:16 -07:00
|
|
|
|
2022-06-07 16:56:04 -07:00
|
|
|
expect(try Data.decode(json))
|
|
|
|
|
.to(throwError(errorType: DecodingError.self))
|
2022-05-10 10:35:16 -07:00
|
|
|
}
|
|
|
|
|
|
2022-05-23 18:27:18 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class IgnoreEncodableTests: TestCase {
|
|
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
|
|
|
|
var value: Int
|
|
|
|
|
@IgnoreEncodable var ignored: Int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testValueIsNotEncoded() throws {
|
|
|
|
|
let data = Data(value: 2, ignored: 2)
|
|
|
|
|
let encoded = try XCTUnwrap(
|
|
|
|
|
String(data: try JSONEncoder.default.encode(data), encoding: .utf8)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(encoded) == "{\"value\":2}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testValueIsDecoded() throws {
|
|
|
|
|
let json = "{\"value\": 1, \"ignored\": 2}"
|
|
|
|
|
|
|
|
|
|
expect(try Data.decode(json)) == .init(value: 1, ignored: 2)
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-10 10:26:13 -07:00
|
|
|
}
|
|
|
|
|
|
2023-07-14 15:00:12 -07:00
|
|
|
class DecoderExtensionsNonEmptyStringTests: TestCase {
|
|
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
|
|
|
|
@NonEmptyStringDecodable var value: String?
|
|
|
|
|
|
|
|
|
|
init(value: String) {
|
|
|
|
|
self.value = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesActualValue() throws {
|
|
|
|
|
let data = Data(value: "string")
|
|
|
|
|
expect(try data.encodeAndDecode()) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesNil() throws {
|
|
|
|
|
let data = try Data.decode("{\"value\": null}")
|
|
|
|
|
expect(data.value).to(beNil())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testConvertsEmptyStringToNil() throws {
|
|
|
|
|
let data = try Data.decode("{\"value\": \"\"}")
|
|
|
|
|
expect(data.value).to(beNil())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testConvertsSpacesToNil() throws {
|
|
|
|
|
let data = try Data.decode("{\"value\": \" \"}")
|
|
|
|
|
expect(data.value).to(beNil())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-18 06:12:41 -07:00
|
|
|
class DecoderExtensionsNonEmptyArrayTests: TestCase {
|
|
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
2023-08-14 07:35:41 -07:00
|
|
|
@EnsureNonEmptyCollectionDecodable var value: [String]
|
2023-07-18 06:12:41 -07:00
|
|
|
|
|
|
|
|
init(value: [String]) {
|
|
|
|
|
self.value = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesOneValues() throws {
|
|
|
|
|
let data = Data(value: ["1"])
|
|
|
|
|
expect(try data.encodeAndDecode()) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesMultipleValues() throws {
|
|
|
|
|
let data = Data(value: ["1", "2"])
|
|
|
|
|
expect(try data.encodeAndDecode()) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testEncodesEmptyValues() throws {
|
|
|
|
|
expect(try Data(value: []).encodedJSON) == "{\"value\":[]}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testThrowsWhenDecodingEmptyArray() throws {
|
|
|
|
|
expect {
|
|
|
|
|
try Data.decode("{\"value\": []}")
|
2023-08-14 07:35:41 -07:00
|
|
|
}.to(throwError(EnsureNonEmptyCollectionDecodable<[String]>.Error()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DecoderExtensionsNonEmptyDictionaryTests: TestCase {
|
|
|
|
|
|
|
|
|
|
private struct Data: Codable, Equatable {
|
|
|
|
|
@EnsureNonEmptyCollectionDecodable var value: [String: Int]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesOneValues() throws {
|
|
|
|
|
let data = Data(value: ["1": 1])
|
|
|
|
|
expect(try data.encodeAndDecode()) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testDecodesMultipleValues() throws {
|
|
|
|
|
let data = Data(value: ["1": 1, "2": 2])
|
|
|
|
|
expect(try data.encodeAndDecode()) == data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testEncodesEmptyValues() throws {
|
|
|
|
|
expect(try Data(value: [:]).encodedJSON) == "{\"value\":{}}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testThrowsWhenDecodingEmptyDictionary() throws {
|
|
|
|
|
expect {
|
|
|
|
|
try Data.decode("{\"value\": {}}")
|
|
|
|
|
}.to(throwError(EnsureNonEmptyCollectionDecodable<[String: Int]>.Error()))
|
2023-07-18 06:12:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-14 15:00:12 -07:00
|
|
|
// MARK: - Extensions
|
|
|
|
|
|
2022-05-19 11:27:12 -07:00
|
|
|
extension Decodable where Self: Encodable {
|
2022-05-10 10:26:13 -07:00
|
|
|
|
|
|
|
|
func encodeAndDecode() throws -> Self {
|
|
|
|
|
return try JSONDecoder.default.decode(
|
|
|
|
|
jsonData: JSONEncoder.default.encode(self)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-16 16:12:32 -07:00
|
|
|
extension Decodable {
|
2022-05-10 10:26:13 -07:00
|
|
|
|
2022-05-10 10:35:16 -07:00
|
|
|
static func decode(_ json: String) throws -> Self {
|
2022-05-20 15:53:27 -07:00
|
|
|
return try JSONDecoder.default.decode(jsonData: json.asData)
|
2022-05-10 10:35:16 -07:00
|
|
|
}
|
|
|
|
|
|
2022-05-10 10:26:13 -07:00
|
|
|
static func decodeEmptyData() throws -> Self {
|
2022-05-20 15:53:27 -07:00
|
|
|
return try self.decode("{}")
|
2022-05-10 10:26:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|