Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prepareDependencies #288

Merged
merged 8 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
7 changes: 4 additions & 3 deletions Package@swift-6.0.swift
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 All @@ -81,7 +81,7 @@ let package = Package(
]
),
],
swiftLanguageVersions: [.v6]
swiftLanguageModes: [.v6]
)

#if !os(macOS) && !os(WASI)
Expand All @@ -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