Skip to content

Commit f073691

Browse files
committed
@EnsureNonEmptyArrayDecodable
This allows us to define properties in a `Decodable` type and ensure that they're not empty. Example ```swift struct Data: Codable, Equatable { @EnsureNonEmptyArrayDecodable var value: [String] } ```
1 parent 0ebe629 commit f073691

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

RevenueCat.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
4F83F6BA2A5DB807003F90A5 /* CurrentTestCaseTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575A17AA2773A59300AA6F22 /* CurrentTestCaseTracker.swift */; };
252252
4F83F6BB2A5DB80B003F90A5 /* OSVersionEquivalent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DE80AD28075D77008D6C6F /* OSVersionEquivalent.swift */; };
253253
4F8452682A5756CC00084550 /* HTTPRequestBody+Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8452672A5756CC00084550 /* HTTPRequestBody+Signing.swift */; };
254+
4F8929192A65EF3000A91EA2 /* EnsureNonEmptyArrayDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8929182A65EF3000A91EA2 /* EnsureNonEmptyArrayDecodable.swift */; };
254255
4F8A58172A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */; };
255256
4F8A58182A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */; };
256257
4F90AFCB2A3915340047E63F /* TestMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F90AFCA2A3915340047E63F /* TestMessage.swift */; };
@@ -964,6 +965,7 @@
964965
4F7DBFBC2A1E986C00A2F511 /* StoreKit2TransactionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2TransactionFetcher.swift; sourceTree = "<group>"; };
965966
4F8038322A1EA7C300D21039 /* TransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionPoster.swift; sourceTree = "<group>"; };
966967
4F8452672A5756CC00084550 /* HTTPRequestBody+Signing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPRequestBody+Signing.swift"; sourceTree = "<group>"; };
968+
4F8929182A65EF3000A91EA2 /* EnsureNonEmptyArrayDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnsureNonEmptyArrayDecodable.swift; sourceTree = "<group>"; };
967969
4F8A58162A16EE3500EF97AD /* MockOfflineCustomerInfoCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOfflineCustomerInfoCreator.swift; sourceTree = "<group>"; };
968970
4F90AFCA2A3915340047E63F /* TestMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMessage.swift; sourceTree = "<group>"; };
969971
4F98E9D22A465A4400DB6EAB /* TestStoreProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestStoreProduct.swift; sourceTree = "<group>"; };
@@ -2437,6 +2439,7 @@
24372439
57EAE52C274468900060EB74 /* RawDataContainer.swift */,
24382440
4F0BBA802A1D0524000E75AB /* DefaultDecodable.swift */,
24392441
4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */,
2442+
4F8929182A65EF3000A91EA2 /* EnsureNonEmptyArrayDecodable.swift */,
24402443
);
24412444
path = Codable;
24422445
sourceTree = "<group>";
@@ -3331,6 +3334,7 @@
33313334
2D9C7BB326D838FC006838BE /* UIApplication+RCExtensions.swift in Sources */,
33323335
F56E2E7727622B5E009FED5B /* TransactionsManager.swift in Sources */,
33333336
B34605CC279A6E380031CA74 /* LogInOperation.swift in Sources */,
3337+
4F8929192A65EF3000A91EA2 /* EnsureNonEmptyArrayDecodable.swift in Sources */,
33343338
35F82BB626A9B8040051DF03 /* AttributionDataMigrator.swift in Sources */,
33353339
A55D08302722368600D919E0 /* SK2BeginRefundRequestHelper.swift in Sources */,
33363340
35D832CD262A5B7500E60AC5 /* ETagManager.swift in Sources */,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// Copyright RevenueCat Inc. All Rights Reserved.
3+
//
4+
// Licensed under the MIT License (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://opensource.org/licenses/MIT
9+
//
10+
// EnsureNonEmptyArrayDecodable.swift
11+
//
12+
// Created by Nacho Soto on 7/17/23.
13+
14+
import Foundation
15+
16+
/// A property wrapper that ensures decoded arrays aren't empty.
17+
/// - Example:
18+
/// ```
19+
/// struct Data {
20+
/// @EnsureNonEmptyArrayDecodable var values: [String] // fails to decode if array is empty
21+
/// }
22+
/// ```
23+
@propertyWrapper
24+
struct EnsureNonEmptyArrayDecodable<Value: Codable> {
25+
26+
struct Error: Swift.Error {}
27+
28+
var wrappedValue: [Value]
29+
30+
}
31+
32+
extension EnsureNonEmptyArrayDecodable: Equatable where Value: Equatable {}
33+
extension EnsureNonEmptyArrayDecodable: Hashable where Value: Hashable {}
34+
35+
extension EnsureNonEmptyArrayDecodable: Decodable {
36+
37+
init(from decoder: Decoder) throws {
38+
let container = try decoder.singleValueContainer()
39+
let array = try container.decode([Value].self)
40+
41+
if array.isEmpty {
42+
throw Error()
43+
} else {
44+
self.wrappedValue = array
45+
}
46+
}
47+
48+
}
49+
50+
extension EnsureNonEmptyArrayDecodable: Encodable {
51+
52+
func encode(to encoder: Encoder) throws {
53+
var container = encoder.singleValueContainer()
54+
try container.encode(self.wrappedValue)
55+
}
56+
57+
}
58+
59+
extension KeyedDecodingContainer {
60+
61+
func decode<T>(
62+
_ type: EnsureNonEmptyArrayDecodable<T>.Type,
63+
forKey key: Key
64+
) throws -> EnsureNonEmptyArrayDecodable<T> {
65+
return try self.decodeIfPresent(type, forKey: key)
66+
.orThrow(EnsureNonEmptyArrayDecodable<T>.Error())
67+
}
68+
69+
}

Tests/UnitTests/FoundationExtensions/DecoderExtensionTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,38 @@ class DecoderExtensionsNonEmptyStringTests: TestCase {
290290

291291
}
292292

293+
class DecoderExtensionsNonEmptyArrayTests: TestCase {
294+
295+
private struct Data: Codable, Equatable {
296+
@EnsureNonEmptyArrayDecodable var value: [String]
297+
298+
init(value: [String]) {
299+
self.value = value
300+
}
301+
}
302+
303+
func testDecodesOneValues() throws {
304+
let data = Data(value: ["1"])
305+
expect(try data.encodeAndDecode()) == data
306+
}
307+
308+
func testDecodesMultipleValues() throws {
309+
let data = Data(value: ["1", "2"])
310+
expect(try data.encodeAndDecode()) == data
311+
}
312+
313+
func testEncodesEmptyValues() throws {
314+
expect(try Data(value: []).encodedJSON) == "{\"value\":[]}"
315+
}
316+
317+
func testThrowsWhenDecodingEmptyArray() throws {
318+
expect {
319+
try Data.decode("{\"value\": []}")
320+
}.to(throwError(EnsureNonEmptyArrayDecodable<String>.Error()))
321+
}
322+
323+
}
324+
293325
// MARK: - Extensions
294326

295327
extension Decodable where Self: Encodable {

0 commit comments

Comments
 (0)