diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75a4a8b..898d2ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,9 @@ name: CI on: pull_request jobs: - pod-lint-10_15: + pod-lint: name: Pod lint - runs-on: macos-10.15 + runs-on: macos-13 steps: - name: Checkout @@ -23,13 +23,13 @@ jobs: run: bundle exec pod lib lint --verbose --fail-fast --swift-version=5.5 shell: bash - test-no-iOS-host-10_15: - name: Run swift tests that don't require iOS host on MacOS 10.15 - runs-on: macos-10.15 + test-no-iOS-host-13: + name: Run swift tests that don't require iOS host on MacOS 13 + runs-on: macos-13 strategy: matrix: - # latest 11 and all available versions 12 - xcodeVersions: ['11.7', '12.0', '12.1', '12.2', '12.3', '12.4'] + # all available versions of XCode14 + xcodeVersions: ['14.1', '14.2', '14.3'] steps: - name: Checkout @@ -44,64 +44,9 @@ jobs: run: swift test -c debug -Xswiftc -enable-testing shell: bash - test-no-iOS-host-11: - name: Run swift tests that don't require iOS host on MacOS 11 - runs-on: macos-11 - strategy: - matrix: - # latest 12 and all available versions 13 - xcodeVersions: ['12.5', '13.0', '13.1'] - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up XCode Version - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ matrix.xcodeVersions }} - - - name: Run tests that don't require iOS host - run: swift test -c debug -Xswiftc -enable-testing - shell: bash - - test-keychain-iOS-host-10_15-XCode11: - name: Run swift tests that require keychain entitlement on XCode 11 - runs-on: macos-10.15 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up XCode Version - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '11.7' - - - name: Run tests that require iOS host for keychain entitlement - run: xcodebuild -project SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj -scheme SecureStorageTestsHostApp -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 11,OS=13.7" test - shell: bash - - test-keychain-iOS-host-11-XCode12: - name: Run swift tests that require keychain entitlement on XCode 12 - runs-on: macos-11 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up XCode Version - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '12.5' - - - name: Run tests that require iOS host for keychain entitlement - run: xcodebuild -project SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj -scheme SecureStorageTestsHostApp -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 12,OS=14.5" test - shell: bash - - test-keychain-iOS-host-11-XCode13: - name: Run swift tests that require keychain entitlement on XCode 13 - runs-on: macos-11 + test-keychain-iOS-host-13-XCode14: + name: Run swift tests that require keychain entitlement on XCode 14 + runs-on: macos-13 steps: - name: Checkout @@ -113,5 +58,5 @@ jobs: xcode-version: latest-stable - name: Run tests that require iOS host for keychain entitlement - run: xcodebuild -project SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj -scheme SecureStorageTestsHostApp -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 13,OS=15.0" test + run: xcodebuild -project SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj -scheme SecureStorageTestsHostApp -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" test shell: bash \ No newline at end of file diff --git a/GKStorageKit.podspec b/GKStorageKit.podspec index 7b5ff8f..ad3d931 100644 --- a/GKStorageKit.podspec +++ b/GKStorageKit.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'GKStorageKit' - s.version = '2.0.0' + s.version = '2.2.0' s.summary = 'GKStorageKit framework.' s.description = <<-DESC * GKStorageKit framework @@ -10,8 +10,8 @@ Pod::Spec.new do |s| s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } s.author = { 'Gligor Kotushevski' => 'gligorkot@gmail.com' } s.social_media_url = 'https://twitter.com/gligor_nz' - s.platform = :ios, '9.0' - s.ios.deployment_target = '9.0' + s.platform = :ios, '11.0' + s.ios.deployment_target = '11.0' s.source = { :git => 'https://github.com/gligorkot/GKStorageKit.git', :tag => s.version.to_s } s.source_files = 'Sources/GKStorageKit/**/*' diff --git a/Gemfile.lock b/Gemfile.lock index 34c39e1..56f3335 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,50 +1,50 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.5) + CFPropertyList (3.0.6) rexml - activesupport (5.2.6) + activesupport (7.0.4.3) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) - claide (1.0.3) - cocoapods (1.10.2) - addressable (~> 2.6) + claide (1.1.0) + cocoapods (1.12.1) + addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.10.2) + cocoapods-core (= 1.12.1) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-downloader (>= 1.6.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) - molinillo (~> 0.6.6) + molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (~> 1.4) - xcodeproj (>= 1.19.0, < 2.0) - cocoapods-core (1.10.2) - activesupport (> 5.0, < 6) - addressable (~> 2.6) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.21.0, < 2.0) + cocoapods-core (1.12.1) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) netrc (~> 0.11) - public_suffix + public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.5.1) + cocoapods-downloader (1.6.3) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -53,32 +53,31 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.1.9) + concurrent-ruby (1.2.2) escape (0.0.4) - ethon (0.15.0) + ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.15.4) + ffi (1.15.5) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) - i18n (1.8.11) + i18n (1.13.0) concurrent-ruby (~> 1.0) - json (2.6.1) - minitest (5.14.4) - molinillo (0.6.6) + json (2.6.3) + minitest (5.18.0) + molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - public_suffix (4.0.6) + public_suffix (4.0.7) rexml (3.2.5) - ruby-macho (1.4.0) - thread_safe (0.3.6) + ruby-macho (2.5.1) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.9) - thread_safe (~> 0.1) - xcodeproj (1.21.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -93,4 +92,4 @@ DEPENDENCIES cocoapods (~> 1.10) BUNDLED WITH - 2.0.2 + 2.2.3 diff --git a/Package.swift b/Package.swift index f6f6756..a3041bd 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "GKStorageKit", platforms: [ - .iOS(.v9), + .iOS(.v11), .macOS(.v10_11) ], products: [ diff --git a/Package@swift-5.2.swift b/Package@swift-5.2.swift index adb93aa..6243862 100644 --- a/Package@swift-5.2.swift +++ b/Package@swift-5.2.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "GKStorageKit", platforms: [ - .iOS(.v9), + .iOS(.v11), .macOS(.v10_11) ], products: [ diff --git a/Package@swift-5.3.swift b/Package@swift-5.3.swift index a47b3c0..c62feba 100644 --- a/Package@swift-5.3.swift +++ b/Package@swift-5.3.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "GKStorageKit", platforms: [ - .iOS(.v9), + .iOS(.v11), .macOS(.v10_11) ], products: [ diff --git a/Package@swift-5.4.swift b/Package@swift-5.4.swift index e92ce15..97c32bd 100644 --- a/Package@swift-5.4.swift +++ b/Package@swift-5.4.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "GKStorageKit", platforms: [ - .iOS(.v9), + .iOS(.v11), .macOS(.v10_11) ], products: [ diff --git a/SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj/project.pbxproj b/SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj/project.pbxproj index 383f4c5..c85a4be 100644 --- a/SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj/project.pbxproj +++ b/SecureStorageTestsHostApp/SecureStorageTestsHostApp.xcodeproj/project.pbxproj @@ -423,7 +423,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -451,7 +451,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -473,7 +473,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -497,7 +497,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/SecureStorageTestsHostApp/SecureStorageTestsHostApp/Info.plist b/SecureStorageTestsHostApp/SecureStorageTestsHostApp/Info.plist index 1dac20b..6c40a6c 100644 --- a/SecureStorageTestsHostApp/SecureStorageTestsHostApp/Info.plist +++ b/SecureStorageTestsHostApp/SecureStorageTestsHostApp/Info.plist @@ -2,21 +2,21 @@ - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 diff --git a/Sources/GKStorageKit/Storage/ObjectStorageService.swift b/Sources/GKStorageKit/Storage/ObjectStorageService.swift index 9025c30..7b18c2f 100644 --- a/Sources/GKStorageKit/Storage/ObjectStorageService.swift +++ b/Sources/GKStorageKit/Storage/ObjectStorageService.swift @@ -42,6 +42,38 @@ final class ObjectStorageService: ObjectStorageInterface, StorageKitDecorator { onSuccess(nil) } } + + func storePerishableObject(_ value: T, forKey key: String, expireAfter timeInterval: TimeInterval, onSuccess: () -> ()) { + let storableTimestampItem = PerishableItem(expireOn: Date().timeIntervalSince1970 + timeInterval, item: value) + storeObject(storableTimestampItem, forKey: key, onSuccess: onSuccess) + } + + func getPerishableObject(forKey key: String, onSuccess: (T) -> (), expired: () -> ()) { + getObject(forKey: key) { (storable: PerishableItem?) in + if let storable = storable, storable.expireOn > Date().timeIntervalSince1970 { + // if cache exists and timestamp is not older than timeInterval, return item + onSuccess(storable.item) + } else { + expired() + } + } + } + + func storePerishableCollection(_ collection: Array, forKey key: String, expireAfter timeInterval: TimeInterval, onSuccess: () -> ()) { + let storableTimestampCollection = PerishableCollection(expireOn: Date().timeIntervalSince1970 + timeInterval, collection: collection) + storeObject(storableTimestampCollection, forKey: key, onSuccess: onSuccess) + } + + func getPerishableCollection(forKey key: String, onSuccess: ([T]?) -> (), expired: () -> ()) { + getObject(forKey: key) { (storable: PerishableCollection?) in + if let storable = storable, storable.expireOn > Date().timeIntervalSince1970 { + // if cache exists and timestamp is not older than timeInterval, return collection + onSuccess(storable.collection) + } else { + expired() + } + } + } func removeValue(forKey key: String, onSuccess: () -> ()) { objectStorage.removeObject(forKey: key) diff --git a/Sources/GKStorageKit/Storage/StorableTimestampItem.swift b/Sources/GKStorageKit/Storage/StorableTimestampItem.swift new file mode 100644 index 0000000..fe733dc --- /dev/null +++ b/Sources/GKStorageKit/Storage/StorableTimestampItem.swift @@ -0,0 +1,19 @@ +// +// PerishableItem.swift +// GKStorageKit +// +// Created by Gligor Kotushevski on 4/07/19. +// Copyright © 2019 Gligor Kotushevski. All rights reserved. +// + +import Foundation + +struct PerishableItem: Codable { + let expireOn: TimeInterval + let item: T +} + +struct PerishableCollection: Codable { + let expireOn: TimeInterval + let collection: Array +} diff --git a/Sources/GKStorageKit/StorageKit.swift b/Sources/GKStorageKit/StorageKit.swift index 3a59756..c046639 100644 --- a/Sources/GKStorageKit/StorageKit.swift +++ b/Sources/GKStorageKit/StorageKit.swift @@ -23,6 +23,10 @@ public protocol ObjectStorageInterface { func getCollection(forKey key: String, onSuccess: ([T]?) -> ()) func storeObject(_ value: T, forKey key: String, onSuccess: () -> ()) func getObject(forKey key: String, onSuccess: (T?) -> ()) + func storePerishableObject(_ value: T, forKey key: String, expireAfter timeInterval: TimeInterval, onSuccess: () -> ()) + func getPerishableObject(forKey key: String, onSuccess: (T) -> (), expired: () -> ()) + func storePerishableCollection(_ collection: Array, forKey key: String, expireAfter timeInterval: TimeInterval, onSuccess: () -> ()) + func getPerishableCollection(forKey key: String, onSuccess: ([T]?) -> (), expired: () -> ()) func removeValue(forKey key: String, onSuccess: () -> ()) func cleanStorage(onSuccess: () -> ()) } diff --git a/Tests/GKStorageKitTests/Storage/ObjectStorageServiceTests.swift b/Tests/GKStorageKitTests/Storage/ObjectStorageServiceTests.swift index 9e446a2..592f71a 100644 --- a/Tests/GKStorageKitTests/Storage/ObjectStorageServiceTests.swift +++ b/Tests/GKStorageKitTests/Storage/ObjectStorageServiceTests.swift @@ -201,4 +201,93 @@ class ObjectStorageServiceTests: XCTestCase { } } + func test_storageServiceSuccessfullyStorePerishableObjectAndRetrieveIt() { + let ex = expectation(description: "test_storageServiceSuccessfullyStorePerishableObjectAndRetrieveIt") + let key = "key" + let storedValue = CodableValueClass(id: 123, firstName: "firstName", lastName: "lastName") + var extractedValue: CodableValueClass? + + objectStorageService.storePerishableObject(storedValue, forKey: key, expireAfter: 1, onSuccess: { + self.objectStorageService.getPerishableObject(forKey: key, onSuccess: { (value: CodableValueClass?) in + extractedValue = value + ex.fulfill() + }, expired: { + XCTFail() + }) + }) + + waitForExpectations(timeout: defaultTimeout) { (_) in + XCTAssertNotNil(extractedValue) + XCTAssertEqual(extractedValue?.id, storedValue.id) + XCTAssertEqual(extractedValue?.firstName, storedValue.firstName) + XCTAssertEqual(extractedValue?.lastName, storedValue.lastName) + } + } + + func test_storageServiceStorePerishableObjectAndItExpires() { + let ex = expectation(description: "test_storageServiceStorePerishableObjectAndItExpires") + let key = "key" + let storedValue = CodableValueClass(id: 123, firstName: "firstName", lastName: "lastName") + var expired = false + + objectStorageService.storePerishableObject(storedValue, forKey: key, expireAfter: -1, onSuccess: { + self.objectStorageService.getPerishableObject(forKey: key, onSuccess: { (value: CodableValueClass?) in + XCTFail() + }, expired: { + expired = true + ex.fulfill() + }) + }) + + waitForExpectations(timeout: defaultTimeout) { (_) in + XCTAssert(expired) + } + } + + func test_storageServiceSuccessfullyStorePerishableCollectionAndRetrieveIt() { + let ex = expectation(description: "test_storageServiceSuccessfullyStorePerishableCollectionAndRetrieveIt") + let key = "key" + let storable = CodableValueClass(id: 123, firstName: "firstName", lastName: "lastName") + let storedValue = [storable] + var extractedValue: [Codable]? + + objectStorageService.storePerishableCollection(storedValue, forKey: key, expireAfter: 1, onSuccess: { + self.objectStorageService.getPerishableCollection(forKey: key, onSuccess: { (value: [CodableValueClass]?) in + extractedValue = value + ex.fulfill() + }, expired: { + XCTFail() + }) + }) + + waitForExpectations(timeout: defaultTimeout) { (_) in + XCTAssertEqual(extractedValue?.count, storedValue.count) + if let firstCaller = extractedValue?.first as? CodableValueClass { + XCTAssertEqual(firstCaller.id, storedValue.first!.id) + } else { + XCTFail() + } + } + } + + func test_storageServiceStorePerishableCollectionAndItExpires() { + let ex = expectation(description: "test_storageServiceStorePerishableCollectionAndItExpires") + let key = "key" + let storable = CodableValueClass(id: 123, firstName: "firstName", lastName: "lastName") + let storedValue = [storable] + var expired = false + + objectStorageService.storePerishableCollection(storedValue, forKey: key, expireAfter: -1, onSuccess: { + self.objectStorageService.getPerishableCollection(forKey: key, onSuccess: { (value: [CodableValueClass]?) in + XCTFail() + }, expired: { + expired = true + ex.fulfill() + }) + }) + + waitForExpectations(timeout: defaultTimeout) { (_) in + XCTAssert(expired) + } + } }