Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
159 commits
Select commit Hold shift + click to select a range
1eb73d9
Rename test case to better reflect the intent of the tests
crisfeim Apr 3, 2025
4bfac7a
Does not delete cache upon LocalFeedLoader creation
crisfeim Apr 3, 2025
11afe3b
Save command requests cache deletioin
crisfeim Apr 3, 2025
6c585fc
Extract system under test creation to a factory method to protect tes…
crisfeim Apr 3, 2025
a98769c
Move memory leak tracking extension from the Feed API module scope to…
crisfeim Apr 3, 2025
841817f
Add memory leak tracking
crisfeim Apr 3, 2025
1848e88
Save command does not request cache insertion on deletion error
crisfeim Apr 3, 2025
a317343
Save command requests new cache insertion on succesful deletion
crisfeim Apr 3, 2025
b9b79db
Save command requests cache insertion with timestamp on succesful del…
crisfeim Apr 3, 2025
dc8b1aa
Remove redundant test code
crisfeim Apr 3, 2025
e3ab1ae
Unify store helper received messages to guarantee order and simplify …
crisfeim Apr 3, 2025
d985e6b
Save commands fails on cache deletion error
crisfeim Apr 3, 2025
bc909e6
Save command fails on cache insertion error
crisfeim Apr 3, 2025
0c8d987
Save command succeds on succesful cache insertion
crisfeim Apr 3, 2025
0a6a783
Extract duplicated test code into a shared helper method
crisfeim Apr 3, 2025
7a63831
Extract FeedStore protocol into a FeedStoreSpy helper
crisfeim Apr 3, 2025
527ac03
Guarantee that the `LocalFeedLoader`does not delvier deletion error a…
crisfeim Apr 3, 2025
dd954de
Guarantee that the `LocalFeedLoader`does not deliver insertion error …
crisfeim Apr 3, 2025
bec426a
Invert `if` logic to make code paths easier to follow
crisfeim Apr 3, 2025
7c197d3
Extract cache insertion into a helper function to make logic inside c…
crisfeim Apr 3, 2025
0d56095
Move `LocalFeedLoader` and `FeedStore` collaborator to its own file i…
crisfeim Apr 3, 2025
e015d61
Move `FeedStore` to its own file
crisfeim Apr 3, 2025
af967ce
Add `SaveResult` typealias to protect code from potential breaking ch…
crisfeim Apr 3, 2025
936c613
Add `LocalFeedItem` data transfer representation to decouple storage …
crisfeim Apr 3, 2025
8acc527
Simplify test setup and assertions with a factory helper method
crisfeim Apr 3, 2025
21c3245
Add `RemoteFeedItem` data transfer representation to decouple the ite…
crisfeim Apr 3, 2025
cf6df4e
Move `RemoteFeedItem` to its own file
crisfeim Apr 3, 2025
0436d6f
Move `LocalFeedItem` to its own file
crisfeim Apr 3, 2025
6c56288
Remove references of "items" in favor of "images" which is a domain t…
crisfeim Apr 3, 2025
13f3ef5
`LocalFeedLoader` does not message store upon creation (before loadin…
crisfeim Apr 3, 2025
43ac6f4
Extract `FeedCacheSpy` into a shared scope to remove duplication
crisfeim Apr 3, 2025
660e801
Load command requests cache retrieval
crisfeim Apr 3, 2025
ff82695
Load command fails on retrieval error
crisfeim Apr 3, 2025
eb710bf
Replace load command completion to return a result type rather than a…
crisfeim Apr 3, 2025
f8510e4
Load command delivers no images on empty cache
crisfeim Apr 3, 2025
c10236e
Extract duplicated test code into a shared helper method
crisfeim Apr 3, 2025
fb65fc0
Load commnd delivers cached images on less than seven days old cache
crisfeim Apr 3, 2025
871880d
Load command delivers no images on seven days old cache
crisfeim Apr 3, 2025
88e9a17
Extract local members into properties
crisfeim Apr 3, 2025
7b70f80
Load command delivers no images on more than seven days old cache
crisfeim Apr 3, 2025
7a60ea9
Deletes cache on retrieval error
crisfeim Apr 4, 2025
03e5410
Load command does not delete cache when cache is already empty
crisfeim Apr 4, 2025
6170177
Load command does not delete less than seven days old cache
crisfeim Apr 4, 2025
d01a9bc
Load command deletes seven days old cache upon retrieval
crisfeim Apr 4, 2025
6bfe842
Load command deletes more than seven days old cache upon retrieval
crisfeim Apr 4, 2025
a301ea7
Load command does not delive a load result after the instance has bee…
crisfeim Apr 4, 2025
6312531
LocalFeedLoader does not message the store upon creation before vali…
crisfeim Apr 4, 2025
4e19f75
Extract cache deletion side-effect on retrieval error from the load m…
crisfeim Apr 4, 2025
c161ac3
Validate cache command does not delete cache when cache is already empty
crisfeim Apr 4, 2025
4bb8113
Validate cache command does not delete less than seven days old cache
crisfeim Apr 4, 2025
d48c2b3
Extract duplicated test helpers into a shared scope
crisfeim Apr 4, 2025
2757344
Extract cache deletion side-effects on expired cache from the load me…
crisfeim Apr 4, 2025
637a603
Validate cache command does not delete cache after instance has been…
crisfeim Apr 4, 2025
f4b56b0
Group cases to remove duplication
crisfeim Apr 4, 2025
7709baf
Segment functionnalities into extensions
crisfeim Apr 4, 2025
c520a63
Make LocalFeedLoader conform to FeedLoader protocol
crisfeim Apr 4, 2025
08aea38
Move typealiases to pertinent segments
crisfeim Apr 4, 2025
1eb6bc6
Extract cache validation policy into the new `FeedCachePolicy` type
crisfeim Apr 4, 2025
dabbcd6
Make the feed cache policy a pure type with no side effects (determin…
crisfeim Apr 4, 2025
c244c0b
Make feed cache policy static since it doesn't keep any state
crisfeim Apr 4, 2025
453e1d1
Move FeedCachePolicy to its own file
crisfeim Apr 4, 2025
d1f479c
Hide cache expiration details from tests with a new DSL method to pro…
crisfeim Apr 4, 2025
d7b6cf2
Move feed cache max age (7) days to a computer var to clarify intent …
crisfeim Apr 4, 2025
1d282a4
Separate date extension helpers into two distinct context to clarify …
crisfeim Apr 4, 2025
b034f0e
Retrieving from empty cache delivers empty resu:t
crisfeim Apr 4, 2025
339d31a
Retrieving from empty cache twice delivers empty result (no side-effe…
crisfeim Apr 4, 2025
cc4e793
Retieving after inserting to empty cache delivers inserted values
crisfeim Apr 4, 2025
a8bae42
Move Codable conformance from the framework agnostic LocalFeedImage t…
crisfeim Apr 4, 2025
bb31318
Extract system under testt (sut) creation into a factory method
crisfeim Apr 4, 2025
25a471e
Add memory leak tracking
crisfeim Apr 4, 2025
d253d48
Extract hard-coded store URL from the CodableFeedStore production typ…
crisfeim Apr 4, 2025
b6a022e
Add teardown store cleaning as an extra security layer (besides setUp…
crisfeim Apr 4, 2025
a6da4d5
Extract duplicated store URL creation into a helper factory method
crisfeim Apr 4, 2025
e64aa09
Replace production storeURL with a test specific storeURL to avoid sh…
crisfeim Apr 4, 2025
48d46bd
Add helper method to provide documentation context and clarify test s…
crisfeim Apr 4, 2025
ba6b078
Retrieving from non empty cache twice delivers same found result (no …
crisfeim Apr 4, 2025
13f5f3e
Add missing description for expectation
crisfeim Apr 4, 2025
ac44e11
Extract the duplicated retrieve test code into a reusable helper method
crisfeim Apr 4, 2025
2b0cf26
Extract duplicated non-side effects-on-retrieve test code into a reus…
crisfeim Apr 4, 2025
26ee57e
Extract duplicate insert test code into a reusable helper method
crisfeim Apr 4, 2025
138ea63
Improve test name to follow convention
crisfeim Apr 4, 2025
3649db8
Retrieve delivers error on retrieval error (invalid cached data in th…
crisfeim Apr 4, 2025
af91572
Make the storeURL explicit within the test to facilitate debugging if…
crisfeim Apr 4, 2025
ca4121d
Retrieving from invalid cache twice delivers same error (no side-effe…
crisfeim Apr 4, 2025
352a683
Inserting to a non-empty cache overrides the previously inserted cach…
crisfeim Apr 4, 2025
6ea1cd6
Insert delivers error on insertion error (invalid store url)
crisfeim Apr 4, 2025
1d71c01
Delete comand has no side effects on empty cache
crisfeim Apr 4, 2025
84e18d3
Delete command empties previously inserted cache
crisfeim Apr 4, 2025
48bdcc6
Delete command delviers error on deletion error
crisfeim Apr 4, 2025
04c308f
Abstract delete cache test logic into a shared reusable helper
crisfeim Apr 4, 2025
6d36c8f
Move system mask caches directory instantiation into a factory method…
crisfeim Apr 4, 2025
d85553c
Add spacing in tests to reflect Given-When-Then structure
crisfeim Apr 4, 2025
d8a581e
Make the CodableFeedStore conform to FeedStore
crisfeim Apr 4, 2025
bd54559
Replace concrete CodableFeedStore dependency in test with the FeedSto…
crisfeim Apr 4, 2025
1df730c
Move CodableFeedStore to its own file in production
crisfeim Apr 4, 2025
6edb1ca
Proved that the `CodableFeedStore` side-effects run serially
crisfeim Apr 7, 2025
f09d5a1
Dispatch the `CodableFeedStore` operations in a serial background que…
crisfeim Apr 7, 2025
278dd4b
Make the `CodableFeedStore` queue concurrent to allow multiple `retri…
crisfeim Apr 7, 2025
f08fc5a
Add comments to document that completion handlers in any `FeedStore` …
crisfeim Apr 7, 2025
2238e1a
Add comment to document that the completion handler in any `HTTPClien…
crisfeim Apr 7, 2025
15a0438
Break down `CodableFeedStore`tests to guarantee there's only one asse…
crisfeim Apr 7, 2025
4ab6737
Create `FeedStoreSpecs`that must be implemented by any `FeedStore`tes…
crisfeim Apr 7, 2025
3ae4780
Extract reusable `FeedStoreSpecs`helper methods into a shared scope s…
crisfeim Apr 7, 2025
1f29743
Move failable protocol methods to their own extension to have a bette…
crisfeim Apr 8, 2025
2a76201
Extract common specs into asserts so we can reuse implementation when…
crisfeim Apr 8, 2025
8c0ac1a
Add empty core data feed store specs
crisfeim Apr 8, 2025
870e879
Retrieve command delivers empty on empty cache
crisfeim Apr 8, 2025
56fa809
Retrieve command has no side effects on empty cache
crisfeim Apr 8, 2025
dbd4870
Add CoreDataFeedStoer data model
crisfeim Apr 9, 2025
d065ca9
Add `ManagedCache` and `ManagedFeedImage` model representations
crisfeim Apr 9, 2025
345dbf7
Load persistent container upon `CoreDataFeedStoer` initialization
crisfeim Apr 9, 2025
e9ce2b0
Add private background context to perform store operations
crisfeim Apr 9, 2025
49e62b3
Mke `storeURL` an explicit dependency so we can inject test-specific …
crisfeim Apr 9, 2025
c0a87d6
`CoreDataFeedStore.retrieve()` delivers found values on non-empty cache
crisfeim Apr 9, 2025
677ec7d
Extract model translations into helper methods within the managed models
crisfeim Apr 9, 2025
94a41b3
Extract `ManagedCache` fetch request logic into a helper method withi…
crisfeim Apr 9, 2025
0fa69a8
`CorerDataFeedStore.retrieve()` has no side-effects on non-empty cache
crisfeim Apr 9, 2025
d3a9500
Add missing inert methods in spec and implement testing in CodableStore
crisfeim Apr 9, 2025
a01b3f2
`CoreDataStore.insert()` delivers no error on empty cache
crisfeim Apr 9, 2025
a93d785
`CoreDataFeedStore.insert()` delivers no error on no empty cache
crisfeim Apr 9, 2025
0da5da4
`CoreDataFeedStore.insert()` overrides previously inserted cache values
crisfeim Apr 9, 2025
4da9ab9
Add missing delete specs to FeedStoreSpecs
crisfeim Apr 9, 2025
2eba6fa
`CodableFeedStore.delete()` does not deliver errors on empty cache
crisfeim Apr 9, 2025
7609007
`CodableFeedStore.delete()` delivers no error on non empty cache
crisfeim Apr 9, 2025
9614c9b
`CoreDataStore.delete()` delivers no error on empty cache
crisfeim Apr 9, 2025
3ccd197
`CoreDataStore.delete()` delivers no error on non empty cache
crisfeim Apr 9, 2025
e708ae7
Rename `CoreDataStore` to `CoreDataFeedStore` thus adding pertinent d…
crisfeim Apr 9, 2025
b66e606
`CoreDataFeedStore.delete()` has no side effects on empty cache
crisfeim Apr 9, 2025
cf574bc
`CoreDataFeedStore.delete()` empties previously inserted cache
crisfeim Apr 9, 2025
a3cec19
Proved that `CoreDataFeedStore` side-effects run serially
crisfeim Apr 9, 2025
c6fe8a1
Extract duplicate code into reusable helper method
crisfeim Apr 9, 2025
64ac31f
Move `CoreDataFeedStore` and `CodableFeedStore` files to the new infr…
crisfeim Apr 9, 2025
84ed203
Extract reusable CoreData heleprs into a separate file
crisfeim Apr 9, 2025
945032d
Extract CoreData managed classes into separate files
crisfeim Apr 9, 2025
6095f8b
Separate CoreData managed model classes data from helpers with extens…
crisfeim Apr 9, 2025
cd5df45
Add cache integration test test target
crisfeim Apr 9, 2025
f3e49d7
Randomize test exectuion order on cache integration tests
crisfeim Apr 9, 2025
67ce5bc
Gather coverage on cache integration tests for the EssentialFeed target
crisfeim Apr 9, 2025
b8e181e
Include memory leak tracking helper in the cache integration test tar…
crisfeim Apr 9, 2025
b41456b
`LocalFeedLoader` in integration with the `CoreDataFeedStore` deliver…
crisfeim Apr 9, 2025
1caa8e6
Include cache test helpers in the cache integration tests target
crisfeim Apr 9, 2025
f33c3ca
Clean up & undo all cache side-effects on `setUp` and `tearDown` to a…
crisfeim Apr 9, 2025
5b7eca9
`LocalFeedLoader` in integration with `CoreDataFeedStore`delivers ite…
crisfeim Apr 9, 2025
225e177
Extract duplicate cache load expectations into a shared helper method
crisfeim Apr 9, 2025
ce79053
`LocalFeedLoader` in integration with the `CoreDataFeedStore` overrid…
crisfeim Apr 9, 2025
0eefecc
Extract duplicated save operation into a shared helper method
crisfeim Apr 9, 2025
11c5c47
Delete the `CodableFeedStore` in favor of the `CoreDataFeedStore`(we …
crisfeim Apr 9, 2025
c11b385
Include `EssentialFeedCacheIntegrationTests`test target in the CI sch…
crisfeim Apr 9, 2025
9b6a4af
Remove redunant `internal` access control declarations
crisfeim Apr 9, 2025
e12c8ac
Replace custom `LoadFeedResult` enum with standard swift result type
crisfeim Apr 9, 2025
b384d3e
Nest `LocalFeedResult` into the `FeedLoader` protocol as `FeedLoader.…
crisfeim Apr 9, 2025
840aea8
Replace custom `HTTPClientResult` enum with a typealias over the stan…
crisfeim Apr 9, 2025
45a738e
Replace custom `RetrieveCacheFeedResult`enum with a nest typealais ov…
crisfeim Apr 9, 2025
b0fe572
Refactor `CacheFeed`type from `enum` to `tuple` since we can represen…
crisfeim Apr 9, 2025
3391786
Add typealiases for `FeedStore.DeletionResult` and `FeedStore.Inserti…
crisfeim Apr 9, 2025
97189d0
Replace occurencies of `Error?`for representing operation success/fai…
crisfeim Apr 9, 2025
40c4569
Simplify `CoreDataFeedStore` completion code with the new `Result` APIs
crisfeim Apr 9, 2025
04d9704
Simplify `get` method of `URLSessionHTTPClient` by leveraging nativ…
crisfeim Apr 9, 2025
2b975bb
Update CI's xcode version to 16.2
crisfeim Apr 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4

- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_16.0.app
run: sudo xcode-select -switch /Applications/Xcode_16.2.app

- name: Xcode version
run: /usr/bin/xcodebuild -version
Expand Down
38 changes: 38 additions & 0 deletions EssentialFeed/CI.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"configurations" : [
{
"id" : "FB6DC599-8749-4C8A-B598-7DA402070AD5",
"name" : "Test Scheme Action",
"options" : {

}
}
],
"defaultOptions" : {
"testExecutionOrdering" : "random"
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:EssentialFeed.xcodeproj",
"identifier" : "080EDEF921B6DA7E00813479",
"name" : "EssentialFeedTests"
}
},
{
"target" : {
"containerPath" : "container:EssentialFeed.xcodeproj",
"identifier" : "40B002442CF9E9DB0058D3E0",
"name" : "EssentialFeedAPIEndToEndTests"
}
},
{
"target" : {
"containerPath" : "container:EssentialFeed.xcodeproj",
"identifier" : "40412A492DA67465004677C4",
"name" : "EssentialFeedCacheIntegrationTests"
}
}
],
"version" : 1
}
268 changes: 260 additions & 8 deletions EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:CI.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:EssentialFeedCacheIntegrationTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "40412A492DA67465004677C4"
BuildableName = "EssentialFeedCacheIntegrationTests.xctest"
BlueprintName = "EssentialFeedCacheIntegrationTests"
ReferencedContainer = "container:EssentialFeed.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
151 changes: 151 additions & 0 deletions EssentialFeed/EssentialFeed/Feed Cache/CoreDataStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// CoreDataStore.swift
// EssentialFeed
//
// Created by Cristian Felipe Patiño Rojas on 8/4/25.
//

import CoreData

public final class CoreDataFeedStore: FeedStore {

let container: NSPersistentContainer
private let context: NSManagedObjectContext
public init(storeURL: URL, bundle: Bundle = .main) throws {
container = try NSPersistentContainer.load(
modelName: "FeedStore",
url: storeURL,
in: bundle
)
context = container.newBackgroundContext()
}

public func retrieve(completion: @escaping RetrievalCompletion) {
context.perform { [context] in

do {
if let cache = try ManagedCache.find(in: context) {
completion(.found(feed: cache.localFeed, timestamp: cache.timestamp))
} else {
completion(.empty)
}
} catch {
completion(.failure(error))
}
}
}

public func insert(_ feed: [LocalFeedImage], timestamp: Date, completion: @escaping InsertionCompletion) {
context.perform { [context] in
do {
let managedCache = try ManagedCache.newUniqueInstance(in: context)
managedCache.timestamp = timestamp
managedCache.feed = ManagedFeedImage.images(
from: feed,
in: context
)
try context.save()
completion(nil)
} catch {
completion(error)
}
}

}


public func deleteCachedFeed(completion: @escaping DeletionCompletion) {
completion(nil)
}
}

private extension NSPersistentContainer {
enum LoadingError: Error {
case modelNotFound
case failedToPersistentStores(Error)
}

static func load(
modelName name: String,
url: URL,
in bundle: Bundle
) throws -> NSPersistentContainer {
guard let model = NSManagedObjectModel.with(name: name, in: bundle) else { throw LoadingError.modelNotFound }
let container = NSPersistentContainer(name: name, managedObjectModel: model)

container.persistentStoreDescriptions = [
NSPersistentStoreDescription(url: url)
]

var loadError: Error?
container.loadPersistentStores {
loadError = $1
}

try loadError.map {
throw LoadingError.failedToPersistentStores($0)
}

return container
}
}

private extension NSManagedObjectModel {
static func with(name: String, in bundle: Bundle) -> NSManagedObjectModel? {
return bundle.url(forResource: name, withExtension: "momd")
.flatMap {
NSManagedObjectModel(contentsOf: $0)
}
}
}

@objc(ManagedCache)
private class ManagedCache: NSManagedObject {
@NSManaged var timestamp: Date
@NSManaged var feed: NSOrderedSet

var localFeed: [LocalFeedImage] {
return feed.compactMap { ($0 as? ManagedFeedImage)?.local }
}

static func find(in context: NSManagedObjectContext) throws -> ManagedCache? {
let request = NSFetchRequest<ManagedCache>(entityName: entity().name!)
request.returnsObjectsAsFaults = false
return try context.fetch(request).first
}

static func newUniqueInstance(in context: NSManagedObjectContext) throws -> ManagedCache {
try find(in: context).map(context.delete)
return ManagedCache(context: context)
}
}

@objc(ManagedFeedImage)
private class ManagedFeedImage: NSManagedObject {
@NSManaged var id: UUID
@NSManaged var imageDescription: String?
@NSManaged var location: String?
@NSManaged var url: URL
@NSManaged var cache: ManagedCache

static func images(from localFeed: [LocalFeedImage], in context: NSManagedObjectContext) -> NSOrderedSet {
let managedFeedImages = localFeed.map {
let managedFeedImage = ManagedFeedImage(context: context)
managedFeedImage.id = $0.id
managedFeedImage.imageDescription = $0.description
managedFeedImage.location = $0.location
managedFeedImage.url = $0.url
return managedFeedImage
}
return NSOrderedSet(array: managedFeedImages)
}

var local: LocalFeedImage {
LocalFeedImage(
id: id,
description: imageDescription,
location: location,
url: url
)
}
}
20 changes: 20 additions & 0 deletions EssentialFeed/EssentialFeed/Feed Cache/FeedCachePolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// FeedCachePolicy.swift
// EssentialFeed
//
// Created by Cristian Felipe Patiño Rojas on 4/4/25.
//

import Foundation

final class FeedCachePolicy {
private static let calendar = Calendar(identifier: .gregorian)
private static var maxCacheAgeInDays: Int { return 7 }
private init() {}
static func validate(_ timestamp: Date, against date: Date) -> Bool {
guard let maxCacheAge = calendar.date(byAdding: .day, value: maxCacheAgeInDays, to: timestamp) else {
return false
}
return date < maxCacheAge
}
}
36 changes: 36 additions & 0 deletions EssentialFeed/EssentialFeed/Feed Cache/FeedStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// FeedStore.swift
// EssentialFeed
//
// Created by Cristian Felipe Patiño Rojas on 3/4/25.
//

import Foundation


public typealias CacheFeed = (feed: [LocalFeedImage], timestamp: Date)

public protocol FeedStore {
typealias DeletionResult = Result<Void, Error>
typealias DeletionCompletion = (DeletionResult) -> Void

typealias InsertionResult = Result<Void, Error>
typealias InsertionCompletion = (InsertionResult) -> Void

typealias RetrievalResult = Result<CacheFeed?, Error>
typealias RetrievalCompletion = (RetrievalResult) -> Void

/// The completion handler can be invoked in any thread.
/// Clients are responsible to dispatch to appropiate threads, if needed.
func deleteCachedFeed(completion: @escaping DeletionCompletion)

/// The completion handler can be invoked in any thread.
/// Clients are responsible to dispatch to appropiate threads, if needed.
func insert(_ feed: [LocalFeedImage], timestamp: Date, completion: @escaping InsertionCompletion)

/// The completion handler can be invoked in any thread.
/// Clients are responsible to dispatch to appropiate threads, if needed.
func retrieve(completion: @escaping RetrievalCompletion)
}


Loading