Skip to content

Commit

Permalink
Add prepareDependencies (#288)
Browse files Browse the repository at this point in the history
* Add `prepareDependencies`

Right now, dependencies can only be overridden using scoped operations
like `withDependencies` or attached to object livecycles
(`withDependencies(from:)`). This introduces a new top-level way of
preparing global dependencies so that they are accessible in the
top-level scope of your application.

* wip

* wip

* wip

* Add failing test for preparing dependency twice in the same prepareDependencies{}

* another failing test

* wip

* Allow preparing dependency multiple times in same scope (#298)

* Attempted fix of preparing dependency in multiple lines.

* fixes

* fix

* tests

* wip

* fixes

* clean up

* clean up

* clean up

* wip

* wip

* fix

* wip

* wip

* wip

---------

Co-authored-by: Brandon Williams <[email protected]>
Co-authored-by: Brandon Williams <[email protected]>
  • Loading branch information
3 people authored Nov 7, 2024
1 parent e25189b commit 653531e
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 11 deletions.
6 changes: 3 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "ac879199bc109c96e02f389573ce5b101fa5c8a274b809fc57dba0d4736f5b6f",
"originHash" : "46c7c52f0c1617cc1d5bc47663541c21a6e6734ddf2ad859bf5bf5bc207fa8f4",
"pins" : [
{
"identity" : "combine-schedulers",
Expand Down Expand Up @@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "27d767d643fa2cf083d0a73d74fa84cacb53e85c",
"version" : "1.4.1"
"revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb",
"version" : "1.4.2"
}
}
],
Expand Down
5 changes: 3 additions & 2 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ let package = Package(
name: "DependenciesTests",
dependencies: [
"Dependencies",
"DependenciesMacros",
"DependenciesTestSupport",
.product(name: "IssueReportingTestSupport", package: "xctest-dynamic-overlay"),
]
],
exclude: ["Dependencies.xctestplan"]
),
.target(
name: "DependenciesMacros",
Expand Down Expand Up @@ -102,6 +102,7 @@ let package = Package(
.testTarget(
name: "DependenciesMacrosPluginTests",
dependencies: [
"Dependencies",
"DependenciesMacros",
"DependenciesMacrosPlugin",
.product(name: "MacroTesting", package: "swift-macro-testing"),
Expand Down
89 changes: 83 additions & 6 deletions Sources/Dependencies/DependencyValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,12 @@ import IssueReporting
/// Read the article <doc:RegisteringDependencies> for more information.
public struct DependencyValues: Sendable {
@TaskLocal public static var _current = Self()
@TaskLocal static var isSetting = false
@TaskLocal static var currentDependency = CurrentDependency()
@TaskLocal static var isSetting = false
@TaskLocal static var preparationID: UUID?
static var isPreparing: Bool {
preparationID != nil
}

@_spi(Internals)
public var cachedValues = CachedValues()
Expand Down Expand Up @@ -263,7 +267,75 @@ public struct DependencyValues: Sendable {
return dependency
}
set {
self.storage[ObjectIdentifier(key)] = newValue
if DependencyValues.isPreparing {
let cacheKey = CachedValues.CacheKey(id: TypeIdentifier(key), context: context)
guard !cachedValues.cached.keys.contains(cacheKey) else {
if cachedValues.cached[cacheKey]?.preparationID != DependencyValues.preparationID {
reportIssue(
{
var dependencyDescription = ""
if let fileID = DependencyValues.currentDependency.fileID,
let line = DependencyValues.currentDependency.line
{
dependencyDescription.append(
"""
Location:
\(fileID):\(line)
"""
)
}
dependencyDescription.append(
Key.self == Key.Value.self
? """
Dependency:
\(typeName(Key.Value.self))
"""
: """
Key:
\(typeName(Key.self))
Value:
\(typeName(Key.Value.self))
"""
)
var argument: String {
"\(function)" == "subscript(key:)"
? "\(typeName(Key.self)).self"
: "\\.\(function)"
}
return """
@Dependency(\(argument)) has already been accessed or prepared.
\(dependencyDescription)
A global dependency can only be prepared a single time and cannot be accessed \
beforehand. Prepare dependencies as early as possible in the lifecycle of your \
application.
To temporarily override a dependency in your application, use 'withDependencies' \
to do so in a well-defined scope.
"""
}(),
fileID: DependencyValues.currentDependency.fileID ?? fileID,
filePath: DependencyValues.currentDependency.filePath ?? filePath,
line: DependencyValues.currentDependency.line ?? line,
column: DependencyValues.currentDependency.column ?? column
)
} else {
cachedValues.cached[cacheKey] = CachedValues.CachedValue(
base: newValue,
preparationID: DependencyValues.preparationID
)
}
return
}
cachedValues.cached[cacheKey] = CachedValues.CachedValue(
base: newValue,
preparationID: DependencyValues.preparationID
)
} else {
self.storage[ObjectIdentifier(key)] = newValue
}
}
}

Expand Down Expand Up @@ -382,8 +454,13 @@ public final class CachedValues: @unchecked Sendable {
}
}

public struct CachedValue {
let base: any Sendable
let preparationID: UUID?
}

private let lock = NSRecursiveLock()
public var cached = [CacheKey: any Sendable]()
public var cached = [CacheKey: CachedValue]()

func value<Key: TestDependencyKey>(
for key: Key.Type,
Expand All @@ -399,7 +476,7 @@ public final class CachedValues: @unchecked Sendable {

return withIssueContext(fileID: fileID, filePath: filePath, line: line, column: column) {
let cacheKey = CacheKey(id: TypeIdentifier(key), context: context)
guard let base = cached[cacheKey], let value = base as? Key.Value
guard let base = cached[cacheKey]?.base, let value = base as? Key.Value
else {
let value: Key.Value?
switch context {
Expand Down Expand Up @@ -488,12 +565,12 @@ public final class CachedValues: @unchecked Sendable {
#endif
let value = Key.testValue
if !DependencyValues.isSetting {
cached[cacheKey] = value
cached[cacheKey] = CachedValue(base: value, preparationID: DependencyValues.preparationID)
}
return value
}

cached[cacheKey] = value
cached[cacheKey] = CachedValue(base: value, preparationID: DependencyValues.preparationID)
return value
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ final class FeatureModel {
}
```

> Note: Using the `@ObservationIgnored` macro is necessary when using `@Observable` because
> `@Dependency` is a property wrapper.
That small change makes this feature much friendlier to Xcode previews and testing.

For previews, you can use the `.dependencies` preview trait to override the
Expand Down
17 changes: 17 additions & 0 deletions Sources/Dependencies/WithDependencies.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import Foundation

/// Prepares global dependencies for the lifetime of your application.
///
/// > Important: A dependency key can be prepared at most a single time, and _must_ be prepared
/// > before it has been accessed. Call `prepareDependencies` as early as possible in your
/// > application.
///
/// - Parameter updateValues: A closure for updating the current dependency values for the
/// lifetime of your application.
public func prepareDependencies(
_ updateValues: (inout DependencyValues) throws -> Void
) rethrows {
var dependencies = DependencyValues._current
try DependencyValues.$preparationID.withValue(UUID()) {
try updateValues(&dependencies)
}
}

/// Updates the current dependencies for the duration of a synchronous operation.
///
/// Any mutations made to ``DependencyValues`` inside `updateValuesForOperation` will be visible to
Expand Down
24 changes: 24 additions & 0 deletions Tests/DependenciesTests/Dependencies.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "AAA44865-978E-4BB3-B5D5-4875FE9FCB65",
"name" : "Test Scheme Action",
"options" : {

}
}
],
"defaultOptions" : {
"codeCoverage" : false
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:",
"identifier" : "DependenciesTests",
"name" : "DependenciesTests"
}
}
],
"version" : 1
}
Loading

0 comments on commit 653531e

Please sign in to comment.