Skip to content

Commit

Permalink
New @NonEmptyStringDecodable (#2819)
Browse files Browse the repository at this point in the history
This allows decorating a `String` property in a `Decodable` type so it
ensures it gets converted to `nil` if it's empty.
It will be used for `PaywallData`.
  • Loading branch information
NachoSoto authored Jul 14, 2023
1 parent 455d3da commit 8483c29
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 0 deletions.
4 changes: 4 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@
4FA4C9752A16D49E007D2803 /* MockOfflineEntitlementsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57488BE929CB83540000EE7E /* MockOfflineEntitlementsManager.swift */; };
4FA696BD2A0020A000D228B1 /* MainThreadMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */; };
4FB3FE132A38CB1F004789C6 /* SignatureVerificationIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB3FE122A38CB1F004789C6 /* SignatureVerificationIntegrationTests.swift */; };
4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */; };
4FC083292A4A35FB00A97089 /* Integer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */; };
4FC0832B2A4A361700A97089 /* IntegerExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */; };
4FCBA84F2A15391B004134BD /* SnapshotTesting+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A9127D27DDD0058FA6E /* SnapshotTesting+Extensions.swift */; };
Expand Down Expand Up @@ -971,6 +972,7 @@
4FA696A329FC43C600D228B1 /* ReceiptParserTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ReceiptParserTests-Info.plist"; sourceTree = "<group>"; };
4FA696BC2A0020A000D228B1 /* MainThreadMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadMonitor.swift; sourceTree = "<group>"; };
4FB3FE122A38CB1F004789C6 /* SignatureVerificationIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignatureVerificationIntegrationTests.swift; sourceTree = "<group>"; };
4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonEmptyStringDecodable.swift; sourceTree = "<group>"; };
4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Integer+Extensions.swift"; sourceTree = "<group>"; };
4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerExtensionsTests.swift; sourceTree = "<group>"; };
4FCBA8522A1539D0004134BD /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2434,6 +2436,7 @@
5766AA55283D4C5400FA6091 /* IgnoreHashable.swift */,
57EAE52C274468900060EB74 /* RawDataContainer.swift */,
4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */,
4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */,
);
path = Codable;
sourceTree = "<group>";
Expand Down Expand Up @@ -3252,6 +3255,7 @@
B378156D285A9772000A7B93 /* OfferingsAPI.swift in Sources */,
B34605C0279A6E380031CA74 /* CustomerInfoCallback.swift in Sources */,
B33CEAA0268CDCC9008A3144 /* ISOPeriodFormatter.swift in Sources */,
4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */,
2DDF41A324F6F331005BC22D /* PurchasesReceiptParser.swift in Sources */,
2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */,
35D832F4262E606500E60AC5 /* HTTPResponse.swift in Sources */,
Expand Down
59 changes: 59 additions & 0 deletions Sources/Misc/Codable/NonEmptyStringDecodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// 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
//
// NonEmptyStringDecodable.swift
//
// Created by Nacho Soto on 7/14/23.

import Foundation

/// A property wrapper that ensures decoded strings aren't empty
/// - Example:
/// ```
/// struct Data {
/// @NonEmptyStringDecodable var value: String? // becomes `nil` if value is empty or has only whitespaces
/// }
/// ```
@propertyWrapper
struct NonEmptyStringDecodable {

var wrappedValue: String?

}

extension NonEmptyStringDecodable: Equatable, Hashable {}

extension NonEmptyStringDecodable: Decodable {

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = try container.decode(String?.self)?.notEmptyOrWhitespaces
}

}

extension NonEmptyStringDecodable: Encodable {

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.wrappedValue)
}

}

extension KeyedDecodingContainer {

func decode(
_ type: NonEmptyStringDecodable.Type,
forKey key: Key
) throws -> NonEmptyStringDecodable {
return try self.decodeIfPresent(type, forKey: key) ?? .init()
}

}
34 changes: 34 additions & 0 deletions Tests/UnitTests/FoundationExtensions/DecoderExtensionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,40 @@ class IgnoreEncodableTests: TestCase {

}

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())
}

}

// MARK: - Extensions

extension Decodable where Self: Encodable {

func encodeAndDecode() throws -> Self {
Expand Down

0 comments on commit 8483c29

Please sign in to comment.