From 9a2aab8fc4e5add90313d21b85b8ab86e92e5a2d Mon Sep 17 00:00:00 2001 From: Nayanda Haberty Date: Mon, 8 Apr 2024 01:42:30 +0700 Subject: [PATCH] Simplified the requirements for adding EnvironmentValues & Make GlobalEnvironment settable (#10) * Make GlobalEnvironment settable * Simplified the requirements for adding EnvironmentValues * Fix Unit test * Update README --- README.md | 72 ++++---- .../SwiftEnvironment/GlobalEnvironment.swift | 6 +- Sources/SwiftEnvironment/Macros.swift | 2 +- .../EnvironmentValueMacro.swift | 157 +++++------------- .../Error/EnvironmentValueMacroError.swift | 24 --- .../Utilities/String+Extensions.swift | 2 + .../EnvironmentValueMacroTests.swift | 142 ++++++---------- .../IntegrationTests.swift | 28 +--- 8 files changed, 140 insertions(+), 293 deletions(-) diff --git a/README.md b/README.md index e9e569c..97cfdf1 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,9 @@ protocol MyProtocol { } // add to EnvironmentValues -@EnvironmentValue("myValue") +@EnvironmentValue extension EnvironmentValues { - struct MyProtocolKey: EnvironmentKey { - static let defaultValue = MyProtocolStub() - } + static let myValue: MyProtocol = MyProtocolStub() } ``` @@ -79,18 +77,45 @@ Different than SwiftUI Environment, GlobalEnvironment can be injected and access GlobalResolver.environment(\.myValue, MyImplementation()) ``` +### GlobalEnvironment + +`GlobalEnvironment` complements SwiftUI Environment. It allows `EnvironmentValues` to be accessed globally outside of SwiftUI injection scope. To use it, add EnvironmentValues just like how we add it for SwiftUI, and inject it into `GlobalResolver`: + +```swift +@GlobalEnvironment(\.myValue) var myValue +``` + +To provide gobal environment use GlobalResolver static methods: + +```swift +GlobalResolver + .environment(\.myValue, SomeDependency()) +``` + +You can connect multiple KeyPaths to one KeyPaths by a single call: + +```swift +GlobalResolver + .environment(\.this, \.that, use: \.myValue) +``` + +To resolve dependency manually from GlobalResolver, do this: + +```swift +let myValue = GlobalResolver.resolve(\.myValue) +``` + + ### EnvironmentValue macro -The `EnvironmentValue` macro is used to remove boilerplate code when adding a new variable to EnvironmentValue. To use it, simply add `@EnvironmentValue("")` to the extension of `EnvironmentValues` with the structure of your `EnvironmentKey`. +The `EnvironmentValue` macro is used to remove boilerplate code when adding a new variable to EnvironmentValue. To use it, simply add `@EnvironmentValue` to the extension of `EnvironmentValues` with static variable of your default environmentValue. ```swift import SwiftEnvironment -@EnvironmentValue("myValue") +@EnvironmentValue extension EnvironmentValues { - struct MyEnvironmentKey: EnvironmentKey { - static let defaultValue = MyDependency() - } + static let myValue: Dependency = MyDependency() } ``` @@ -178,35 +203,6 @@ struct MyStruct { You can provide multiple types as variadic parameters. -### GlobalEnvironment - -`GlobalEnvironment` complements SwiftUI Environment. It allows `EnvironmentValues` to be accessed globally outside of SwiftUI injection scope. To use it, add EnvironmentValues just like how we add it for SwiftUI, and inject it into `GlobalResolver`: - -```swift -GlobalResolver - .environment(\.myValue, SomeDependency()) -``` - -Then, you can access it globally using `GlobalEnvironment` property wrapper: - -```swift -@GlobalEnvironment(\.myValue) var myValue -``` - -You can connect multiple KeyPaths to one KeyPaths by a single call: - -```swift -GlobalResolver - .environment(\.this, \.that, use: \.myValue) -``` - -To resolve dependency manually from GlobalResolver, do this: - -```swift -let myValue = GlobalResolver.resolve(\.myValue) -``` - - ### GlobalResolver environment Injected values to `GlobalResolver.environment` are injected using `autoclosure`, so the value will be created lazily. This value will be stored as long as the app is alive. You can inject an explicit closure too if needed: diff --git a/Sources/SwiftEnvironment/GlobalEnvironment.swift b/Sources/SwiftEnvironment/GlobalEnvironment.swift index a281bc6..c897a49 100644 --- a/Sources/SwiftEnvironment/GlobalEnvironment.swift +++ b/Sources/SwiftEnvironment/GlobalEnvironment.swift @@ -13,7 +13,11 @@ public final class GlobalEnvironment { private let resolver: EnvironmentValuesResolver private let keyPath: KeyPath - public private(set) lazy var wrappedValue: Value = resolver.resolve(keyPath) + private lazy var _wrappedValue: Value = resolver.resolve(keyPath) + public var wrappedValue: Value { + get { _wrappedValue } + set { _wrappedValue = newValue } + } public init(_ keyPath: KeyPath) { self.keyPath = keyPath diff --git a/Sources/SwiftEnvironment/Macros.swift b/Sources/SwiftEnvironment/Macros.swift index e720d68..47d0746 100644 --- a/Sources/SwiftEnvironment/Macros.swift +++ b/Sources/SwiftEnvironment/Macros.swift @@ -8,7 +8,7 @@ import Foundation @attached(member, names: arbitrary) -public macro EnvironmentValue(_ keys: String...) = #externalMacro( +public macro EnvironmentValue() = #externalMacro( module: "SwiftEnvironmentMacro", type: "EnvironmentValueMacro" ) diff --git a/Sources/SwiftEnvironmentMacro/EnvironmentValueMacro.swift b/Sources/SwiftEnvironmentMacro/EnvironmentValueMacro.swift index 721b641..43a6fa1 100644 --- a/Sources/SwiftEnvironmentMacro/EnvironmentValueMacro.swift +++ b/Sources/SwiftEnvironmentMacro/EnvironmentValueMacro.swift @@ -20,35 +20,52 @@ public struct EnvironmentValueMacro: MemberMacro { extensionDeclaration.extendedType.as(IdentifierTypeSyntax.self)?.name.text == "EnvironmentValues" else { throw EnvironmentValueMacroError.attachedToInvalidType } - let keyPaths = try node.environmentValueKeyPaths - let environmentKeys = try extensionDeclaration.environmentKeys - return try combine(keyPaths, with: environmentKeys).map { keyPath, environmentKey in - let keyName = environmentKey.name.text - return """ - var \(raw: keyPath): \(raw: try environmentKey.environmentValueType) { - get { self[\(raw: keyName).self] } - set { self[\(raw: keyName).self] = newValue } - } - """ - } + return extensionDeclaration.environmentDeclaration + .map { "\(raw: $0)" } } } -// MARK: Private functions - -private func combine( - _ keyPaths: [String], - with environmentKeys: [StructDeclSyntax]) -> [(keyPath: String, environmentKey: StructDeclSyntax)] { - keyPaths.enumerated().reduce(into: []) { partialResult, pair in - partialResult.append((pair.element, environmentKeys[pair.offset])) +struct EnvironmentDeclaration: CustomStringConvertible { + let baseName: String + let type: String + + var derivedName: String { + "\(baseName.firstCapitalized)SwiftEnvironmentKey" + } + + var description: String { + """ + struct \(derivedName): EnvironmentKey { + static let defaultValue: \(type) = EnvironmentValues.\(baseName) } + + var \(baseName): \(type) { + get { + self[\(derivedName).self] + } + set { + self[\(derivedName).self] = newValue + } + } + """ } +} // MARK: Private extensions -private extension Array where Element: Hashable { - var hasDuplication: Bool { - Set(self).count > self.count +private extension ExtensionDeclSyntax { + var environmentDeclaration: [EnvironmentDeclaration] { + memberBlock.members + .compactMap { $0.decl.as(VariableDeclSyntax.self) } + .filter { $0.isStatic } + .compactMap { variable in + guard let name = variable.name, + let type = variable.typeAnnotation ?? variable.initializer else { + return nil + } + return EnvironmentDeclaration(baseName: name, type: type) + + } } } @@ -78,101 +95,3 @@ private extension VariableDeclSyntax { .trimmedDescription } } - -private extension TypeAliasDeclSyntax { - - var realType: String? { - initializer.value - .as(IdentifierTypeSyntax.self)? - .name.text - } -} - -private extension StructDeclSyntax { - - var typeAliasValueRealType: String? { - memberBlock.members - .compactMap { $0.decl.as(TypeAliasDeclSyntax.self) } - .first { $0.name.text == "Value" }? - .realType - } - - var environmentValueType: String { - get throws { - let defaultValue = memberBlock.members - .compactMap { $0.decl.as(VariableDeclSyntax.self) } - .first { $0.isStatic && $0.name == "defaultValue" } - - guard let defaultValue else { - throw EnvironmentValueMacroError.undeterminedEnvironmentValueType - } - if let typeAnnotation = defaultValue.typeAnnotation, - typeAnnotation != "Value" { - return typeAnnotation - } - if let typeAliasValueRealType { - return typeAliasValueRealType - } - if let initializer = defaultValue.initializer, - initializer != "Value" { - return initializer - } - throw EnvironmentValueMacroError.undeterminedEnvironmentValueType - } - } -} - -private extension ExtensionDeclSyntax { - var environmentKeys: [StructDeclSyntax] { - get throws { - let result = memberBlock.members - .compactMap { $0.as(MemberBlockItemSyntax.self)?.decl.as(StructDeclSyntax.self) } - .filter { $0.isEnvironmentKey } - - guard !result.isEmpty else { - throw EnvironmentValueMacroError.noEnvironmentKeyProvided - } - let names = result.map { $0.name.text } - - guard !names.hasDuplication else { - throw EnvironmentValueMacroError.duplicatedEnvironmentKeys - } - return result - } - } -} - -private extension StructDeclSyntax { - var isEnvironmentKey: Bool { - inheritanceClause?.inheritedTypes - .contains { - $0.type.as(IdentifierTypeSyntax.self)?.name.text == "EnvironmentKey" - } - ?? false - } -} - -private extension AttributeSyntax { - - var environmentValueKeyPaths: [String] { - get throws { - guard let labeledSyntaxes = arguments?.as(LabeledExprListSyntax.self) else { - throw EnvironmentValueMacroError.noArgumentPassed - } - let result = try labeledSyntaxes.map { - guard let stringLiteral = $0.expression.as(StringLiteralExprSyntax.self), - let stringSegment = stringLiteral.segments.first?.as(StringSegmentSyntax.self)else { - throw EnvironmentValueMacroError.wrongArgumentValue - } - return stringSegment.content.text - } - guard !result.isEmpty else { - throw EnvironmentValueMacroError.noArgumentPassed - } - guard !result.hasDuplication else { - throw EnvironmentValueMacroError.duplicatedKeyPaths - } - return result - } - } -} diff --git a/Sources/SwiftEnvironmentMacro/Error/EnvironmentValueMacroError.swift b/Sources/SwiftEnvironmentMacro/Error/EnvironmentValueMacroError.swift index b370e7f..22ba521 100644 --- a/Sources/SwiftEnvironmentMacro/Error/EnvironmentValueMacroError.swift +++ b/Sources/SwiftEnvironmentMacro/Error/EnvironmentValueMacroError.swift @@ -8,36 +8,12 @@ import Foundation public enum EnvironmentValueMacroError: CustomStringConvertible, Error { - case noArgumentPassed - case noKeyPathProvided - case wrongArgumentValue case attachedToInvalidType - case noEnvironmentKeyProvided - case duplicatedKeyPaths - case duplicatedEnvironmentKeys - case undeterminedEnvironmentValueType - case missingEnvironmentKeyValuePair public var description: String { switch self { - case .noArgumentPassed: - return "@EnvironmentValue can only be applied with arguments passed" - case .noKeyPathProvided: - return "@EnvironmentValue can only be applied with provided name of KeyPath" - case .wrongArgumentValue: - return "@EnvironmentValue argument value is wrong" case .attachedToInvalidType: return "@EnvironmentValue can only be attached to extension of EnvironmentValues" - case .noEnvironmentKeyProvided: - return "@EnvironmentValue failed to extract EnvironmentKey" - case .duplicatedKeyPaths: - return "@EnvironmentValue provided KeyPaths is duplicated" - case .undeterminedEnvironmentValueType: - return "@EnvironmentValue cannot determine EnvironmentValue type" - case .duplicatedEnvironmentKeys: - return "@EnvironmentValue provided EnvironmentKeys is duplicated" - case .missingEnvironmentKeyValuePair: - return "@EnvironmentValue provided KeyPaths number is not the same as provided EnvironmentKeys" } } } diff --git a/Sources/SwiftEnvironmentMacro/Utilities/String+Extensions.swift b/Sources/SwiftEnvironmentMacro/Utilities/String+Extensions.swift index fecea99..7d1dad3 100644 --- a/Sources/SwiftEnvironmentMacro/Utilities/String+Extensions.swift +++ b/Sources/SwiftEnvironmentMacro/Utilities/String+Extensions.swift @@ -18,4 +18,6 @@ extension String { return false } } + + var firstCapitalized: String { return prefix(1).capitalized + dropFirst() } } diff --git a/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift b/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift index 8369547..1a21321 100644 --- a/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift +++ b/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift @@ -18,13 +18,6 @@ final class EnvironmentValueMacroTests: XCTestCase { ) } - func test_givenOneArgumentWithTypeAliases_whenExpanded_shouldAddProperty() { - assertMacroExpansion( - oneArgTypeAlias, expandedSource: oneArgTypeAliasExpansion, - macros: ["EnvironmentValue": EnvironmentValueMacro.self] - ) - } - func test_givenOneArgumentWithNoTypeAnnotation_whenExpanded_shouldAddProperty() { assertMacroExpansion( oneArgNoType, expandedSource: oneArgNoTypeExpansion, @@ -55,27 +48,27 @@ final class EnvironmentValueMacroTests: XCTestCase { } private let oneImplicitArg: String = """ -@EnvironmentValue("dummy") +@EnvironmentValue extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue = Some.Dependency() - } + static let dummy = Some.Dependency() } """ private let oneImplicitArgExpansion: String = """ extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue = Some.Dependency() + static let dummy = Some.Dependency() + + struct DummySwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: Some.Dependency = EnvironmentValues.dummy } var dummy: Some.Dependency { get { - self [DummyEnvironmentKey.self] + self[DummySwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKey.self] = newValue + self[DummySwiftEnvironmentKey.self] = newValue } } } @@ -84,108 +77,79 @@ extension EnvironmentValues { private let oneGenericArg: String = """ @EnvironmentValue("dummy") extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue: Dependency = DummyDependency() - } + static let dummy: Dependency = DummyDependency() } """ private let oneGenericArgExpansion: String = """ extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue: Dependency = DummyDependency() + static let dummy: Dependency = DummyDependency() + + struct DummySwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: Dependency = EnvironmentValues.dummy } var dummy: Dependency { get { - self [DummyEnvironmentKey.self] + self[DummySwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKey.self] = newValue + self[DummySwiftEnvironmentKey.self] = newValue } } } """ private let oneArg: String = """ -@EnvironmentValue("dummy") +@EnvironmentValue extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } + static let dummy: DummyDependency = DummyDependency() } """ private let oneArgExpansion: String = """ extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } + static let dummy: DummyDependency = DummyDependency() - var dummy: DummyDependency { - get { - self [DummyEnvironmentKey.self] - } - set { - self [DummyEnvironmentKey.self] = newValue - } - } -} -""" - -private let oneArgTypeAlias: String = """ -@EnvironmentValue("dummy") -extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - typealias Value = DummyDependency - static let defaultValue: Value = DummyDependency() - } -} -""" - -private let oneArgTypeAliasExpansion: String = """ - -extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - typealias Value = DummyDependency - static let defaultValue: Value = DummyDependency() + struct DummySwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: DummyDependency = EnvironmentValues.dummy } var dummy: DummyDependency { get { - self [DummyEnvironmentKey.self] + self[DummySwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKey.self] = newValue + self[DummySwiftEnvironmentKey.self] = newValue } } } """ private let oneArgNoType: String = """ -@EnvironmentValue("dummy") +@EnvironmentValue extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue = DummyDependency() - } + static let dummy = DummyDependency() } """ private let oneArgNoTypeExpansion: String = """ extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue = DummyDependency() + static let dummy = DummyDependency() + + struct DummySwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: DummyDependency = EnvironmentValues.dummy } var dummy: DummyDependency { get { - self [DummyEnvironmentKey.self] + self[DummySwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKey.self] = newValue + self[DummySwiftEnvironmentKey.self] = newValue } } } @@ -194,55 +158,55 @@ extension EnvironmentValues { private let multiArgs: String = """ @EnvironmentValue("one", "two", "three") extension EnvironmentValues { - struct DummyEnvironmentKeyOne: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } - struct DummyEnvironmentKeyTwo: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } - struct DummyEnvironmentKeyThree: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } + static let one = DummyDependency() + static let two = DummyDependency() + static let three = DummyDependency() } """ private let multiArgsExpansion: String = """ extension EnvironmentValues { - struct DummyEnvironmentKeyOne: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } - struct DummyEnvironmentKeyTwo: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() - } - struct DummyEnvironmentKeyThree: EnvironmentKey { - static let defaultValue: DummyDependency = DummyDependency() + static let one = DummyDependency() + static let two = DummyDependency() + static let three = DummyDependency() + + struct OneSwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: DummyDependency = EnvironmentValues.one } var one: DummyDependency { get { - self [DummyEnvironmentKeyOne.self] + self[OneSwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKeyOne.self] = newValue + self[OneSwiftEnvironmentKey.self] = newValue } } + struct TwoSwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: DummyDependency = EnvironmentValues.two + } + var two: DummyDependency { get { - self [DummyEnvironmentKeyTwo.self] + self[TwoSwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKeyTwo.self] = newValue + self[TwoSwiftEnvironmentKey.self] = newValue } } + struct ThreeSwiftEnvironmentKey: EnvironmentKey { + static let defaultValue: DummyDependency = EnvironmentValues.three + } + var three: DummyDependency { get { - self [DummyEnvironmentKeyThree.self] + self[ThreeSwiftEnvironmentKey.self] } set { - self [DummyEnvironmentKeyThree.self] = newValue + self[ThreeSwiftEnvironmentKey.self] = newValue } } } diff --git a/Tests/SwiftEnvironmentTests/IntegrationTests.swift b/Tests/SwiftEnvironmentTests/IntegrationTests.swift index 7592aa0..b745758 100644 --- a/Tests/SwiftEnvironmentTests/IntegrationTests.swift +++ b/Tests/SwiftEnvironmentTests/IntegrationTests.swift @@ -98,27 +98,13 @@ final class IntegrationTests: XCTestCase { } -typealias DummyEnvironmentKey = EnvironmentValues.DummyEnvironmentKey +typealias DummyEnvironmentKey = EnvironmentValues.DummySwiftEnvironmentKey -@EnvironmentValue("dummy", "secondDummy", "thirdDummy", "fourthDummy", "fifthDummy") +@EnvironmentValue extension EnvironmentValues { - struct DummyEnvironmentKey: EnvironmentKey { - static let defaultValue = DummyDependencyStub() - } - - struct SecondDummyEnvironmentKey: EnvironmentKey { - static let defaultValue = DummyDependencyStub() - } - - struct ThirdDummyEnvironmentKey: EnvironmentKey { - static let defaultValue = DummyDependencyStub() - } - - struct FourthDummyEnvironmentKey: EnvironmentKey { - static let defaultValue = DummyDependencyStub() - } - - struct FiftDummyEnvironmentKey: EnvironmentKey { - static let defaultValue: String = "dummy" - } + static let dummy = DummyDependencyStub() + static let secondDummy = DummyDependencyStub() + static let thirdDummy = DummyDependencyStub() + static let fourthDummy = DummyDependencyStub() + static let fifthDummy: String = "dummy" }