Files
swift-composable-architectu…/Sources/ComposableArchitectureMacros/PresentsMacro.swift
Stephen Celis e121b91fb4 Shared State (#2858)
* Shared State

* fix compiler errors in 5.7

* fixes and tests

* fix

* fix

* wip

* longer sleeps

* fix test

* fix tests

* Clean up some typos in SharingState.md (#2860)

* public inits

* wip

* wip

* add test for shared state and onChange

* Case study for sandboxing shared state

* tweaks to case study

* wip

* add tests to sandboxing case study

* rename files

* wip

* simplify sync up tests

* wip

* fix

* wip

* fix $shared.publisher non-determinism and write test that currently fails but ideally would not.

* more docs and a test for autoclosure

* more docs

* wip

* update todo with persistence

* wip

* wip

* support default `nil` optionals in `@Shared`

* Introduce unavailable overload for better diagnostics

* Fix autocomplete from `@Shared(.`

* Revert "Fix autocomplete from `@Shared(.`"

This reverts commit fd1798f9f5.

* Fix defaults

* wip

* Give persistence keys a synchronous update interface (#2880)

* Don't use async sequence for persistence

* wip

* wip

* wip

* Update Sources/ComposableArchitecture/SharedState/PersistenceKey.swift

Co-authored-by: Hal Lee <hal@lee.me>

---------

Co-authored-by: Hal Lee <hal@lee.me>

* Revert sharing in Todos for now

* wip

* wip

* Pass initial shared value to strategy / register app storage (#2904)

* Pass initial shared value to strategy / register app storage

This PR modifies the `PersistenceKey` protocol so that its `load` and
`subscribe` endpoints are handed the initial value and must return a
non-optional value. By feeding this value in, we can ensure that
`.appStorage` declared in a feature is registered outside the feature.

* wip

* wip

* wip

* wip

* wip

* wip

* swift-format

* Update Sources/ComposableArchitecture/Documentation.docc/Articles/SharingState.md

Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>

* non-exhaustive fix

* wip

* remove decodable for now

* Fix app storage registration with `nil` values

* fix

* wip

* tutorial

* fixes

* lots of tutorial fixes

* more tutorial fixes

* more tutorial fixes

* more tutorial fixes

* tutorial fixes

* wip

* re-arrange test

* Remove store shared preview quarantine

* Add support for \.defaultInMemoryStorage (#2965)

* fix

* Added Privacy Manifest file (#2930)

* Added Privacy Manifest file

* Update Package@swift-5.9.swift

---------

Co-authored-by: Stephen Celis <stephen.celis@gmail.com>

* remove warning overloads

* write default instead of register

this is more consistent with SwiftUI's property wrapper, and causes less
strangeness when multiple repeat properties have different defaults

* move privacy manifest

* wip

* wip

* EphemeralFileStorage -> InMemoryFileStorage

* documented gotcha

* add test

* Shared state beta task snaps (#2976)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>

* wip

* Use notification center instead of KVO for user defaults observation. (#2978)

* Use notification center instead of KVO for user defaults observation.

* wip

* wip

* Introduce @SharedReader (#2979)

* wip

* wip

* wip

* wip

* wip

* wip

* Introduce @SharedReader.

* wip

* wip

* sendable

* wip

* wip

---------

Co-authored-by: Stephen Celis <stephen@stephencelis.com>

* Make Shared.Subscription.cancel public (#2983)

* added failing test

* Add convenience initializers

* Save to file storage when app is about to be terminated (#2992)

* Save file storage on termination too.

* wip

* Shared change tracking enhancements (#2989)

* wip

* wip

* wip

* wip

* Add unavailable conformance

* wip

* wip

* wip

* Don't need to check for previews.

* Add default providing persistence key (#2980)

* wip - default providing key

* warn on default access

* Revert "warn on default access"

This reverts commit 38706450bef44a50f94afeaddbc4628e383b52b3.

* wip

* wip

* wip - tests

* wip - reader key

* fix docs

* A few changes.

* A few more changes.

* docs

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>

* Fix optional shared defaults.

* undo last commit but keep test

* wip

* Better shared state change tracking and `TestStore` interactions (#2995)

* A fix for shared assertion.

* fixes

* wip

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>

* docs

* docs

* wip

* Throttle for 1 second.

* fix typo in examples: `let` -> `var` (#2999)

Co-authored-by: Andreas Tielmann <atielmann@deloitte.de>

* wip

* wip

* fix observing projection

* Ping `Shared.publisher` in `willSet` so prev/next values observable

* wip

* wip

* remove sandbox demo

* wip

* wip

* recover inlining

* wip

* wip

* fix docc

* wip

* wip

* re-entrant test

* wip

* Don’t assume CastableLookup (#3011)

* Don’t assume CastableLookup

If the observed value in `UserDefaults` is a `RawRepresentable` type, simply casting to `Value` might fail because the raw representing type could be different from `Value`.  Instead of querying `UserDefaults` directly here, we should call `loadValue()` which will do the right thing for raw representable types (esp. try to call `Value.init(rawValue:)` with the value obtained from `UserDefaults`).

* Add test

---------

Co-authored-by: Stephen Celis <stephen@stephencelis.com>

* Make `FileStorage` more opaque (#3010)

* Make `FileStorage` more opaque

Previously, we had a public protocol and conformances, but we don't
expect anyone to conform outside the library, so instead let's hide
things in a more opaque struct.

The downside of this PR is that it's a breaking API change pretty late
in the game:

```diff
-$0.defaultFileStorage = InMemoryFileStorage()
+$0.defaultFileStorage = .inMemory
```

We could add public deprecated functions that emulate the old
initializers if we think it's worth it...

* wip

* wip

* wip

* wip

* wip

* wip

* fix for 5.7.1

* sync ups clean up

* more docs

* more tests

* shared testing tiups

* wip

* wip

* Fix typo. (#3014)

* more shared state docs

* relax version of swift-dependencies.

* dep change

* fix == and hash on Shared and more tests

* revert == changes

* add test

* Remove hashable conformance from shared

* remove conditional encodability from shared

* wip

* remove syncups tutorial

* wip

* fix

* fix file storage deletion resubscription

* wip

* wip

* wip

* wip

* wip

* fix

* eager throttle

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>
Co-authored-by: NF <4764329+NFulkerson@users.noreply.github.com>
Co-authored-by: Hal Lee <hal@lee.me>
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
Co-authored-by: Pyry Jahkola <pyry.jahkola@iki.fi>
Co-authored-by: Daniel Lyons <72824209+DandyLyons@users.noreply.github.com>
Co-authored-by: Hilton Campbell <github@crosswaterbridge.com>
Co-authored-by: Luke Redpath <lredpath@community.com>
Co-authored-by: andtie <andreas.tielmann@gmail.com>
Co-authored-by: Andreas Tielmann <atielmann@deloitte.de>
Co-authored-by: Alex Kovács <alex@kobachi.jp>
Co-authored-by: Zev Eisenberg <zev@zeveisenberg.com>
2024-04-28 17:53:12 -07:00

229 lines
6.7 KiB
Swift

import SwiftDiagnostics
import SwiftOperators
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacroExpansion
import SwiftSyntaxMacros
public enum PresentsMacro {
}
extension PresentsMacro: AccessorMacro {
public static func expansion<D: DeclSyntaxProtocol, C: MacroExpansionContext>(
of node: AttributeSyntax,
providingAccessorsOf declaration: D,
in context: C
) throws -> [AccessorDeclSyntax] {
guard
let property = declaration.as(VariableDeclSyntax.self),
property.isValidForPresentation,
let identifier = property.identifier?.trimmed
else {
return []
}
let initAccessor: AccessorDeclSyntax =
"""
@storageRestrictions(initializes: _\(identifier))
init(initialValue) {
_\(identifier) = PresentationState(wrappedValue: initialValue)
}
"""
let getAccessor: AccessorDeclSyntax =
"""
get {
_$observationRegistrar.access(self, keyPath: \\.\(identifier))
return _\(identifier).wrappedValue
}
"""
let setAccessor: AccessorDeclSyntax =
"""
set {
_$observationRegistrar.mutate(self, keyPath: \\.\(identifier), &_\(identifier).wrappedValue, newValue, _$isIdentityEqual)
}
"""
// TODO: _modify accessor?
return [initAccessor, getAccessor, setAccessor]
}
}
extension PresentsMacro: PeerMacro {
public static func expansion<D: DeclSyntaxProtocol, C: MacroExpansionContext>(
of node: AttributeSyntax,
providingPeersOf declaration: D,
in context: C
) throws -> [DeclSyntax] {
guard
let property = declaration.as(VariableDeclSyntax.self),
property.isValidForPresentation
else {
return []
}
let wrapped = DeclSyntax(
property.privateWrapped(addingAttribute: ObservableStateMacro.ignoredAttribute)
)
let projected = DeclSyntax(property.projected)
return [
projected,
wrapped,
]
}
}
extension VariableDeclSyntax {
fileprivate func privateWrapped(
addingAttribute attribute: AttributeSyntax
) -> VariableDeclSyntax {
var attributes = self.attributes
for index in attributes.indices.reversed() {
let attribute = attributes[index]
switch attribute {
case let .attribute(attribute):
if attribute.attributeName.tokens(viewMode: .all).map(\.tokenKind) == [
.identifier("Presents")
] {
attributes.remove(at: index)
}
default:
break
}
}
let newAttributes = attributes + [.attribute(attribute)]
return VariableDeclSyntax(
leadingTrivia: leadingTrivia,
attributes: newAttributes,
modifiers: modifiers.privatePrefixed("_"),
bindingSpecifier: TokenSyntax(
bindingSpecifier.tokenKind, trailingTrivia: .space,
presence: .present
),
bindings: bindings.privateWrapped,
trailingTrivia: trailingTrivia
)
}
fileprivate var projected: VariableDeclSyntax {
VariableDeclSyntax(
leadingTrivia: leadingTrivia,
modifiers: modifiers,
bindingSpecifier: TokenSyntax(
bindingSpecifier.tokenKind, trailingTrivia: .space,
presence: .present
),
bindings: bindings.projected,
trailingTrivia: trailingTrivia
)
}
fileprivate var isValidForPresentation: Bool {
!isComputed && isInstance && !isImmutable && identifier != nil
}
}
extension PatternBindingListSyntax {
fileprivate var privateWrapped: PatternBindingListSyntax {
var bindings = self
for index in bindings.indices {
var binding = bindings[index]
if let optionalType = binding.typeAnnotation?.type.as(OptionalTypeSyntax.self) {
binding.typeAnnotation = nil
binding.initializer = InitializerClauseSyntax(
value: FunctionCallExprSyntax(
calledExpression: optionalType.wrappedType.presentationWrapped,
leftParen: .leftParenToken(),
arguments: [
LabeledExprSyntax(
label: "wrappedValue",
expression: binding.initializer?.value ?? ExprSyntax(NilLiteralExprSyntax())
)
],
rightParen: .rightParenToken()
)
)
}
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
bindings[index] = PatternBindingSyntax(
leadingTrivia: binding.leadingTrivia,
pattern: IdentifierPatternSyntax(
leadingTrivia: identifier.leadingTrivia,
identifier: identifier.identifier.privatePrefixed("_"),
trailingTrivia: identifier.trailingTrivia
),
typeAnnotation: binding.typeAnnotation,
initializer: binding.initializer,
accessorBlock: binding.accessorBlock,
trailingComma: binding.trailingComma,
trailingTrivia: binding.trailingTrivia
)
}
}
return bindings
}
fileprivate var projected: PatternBindingListSyntax {
var bindings = self
for index in bindings.indices {
var binding = bindings[index]
if let optionalType = binding.typeAnnotation?.type.as(OptionalTypeSyntax.self) {
binding.typeAnnotation?.type = TypeSyntax(
IdentifierTypeSyntax(
name: .identifier(optionalType.wrappedType.presentationWrapped.trimmedDescription)
)
)
}
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
bindings[index] = PatternBindingSyntax(
leadingTrivia: binding.leadingTrivia,
pattern: IdentifierPatternSyntax(
leadingTrivia: identifier.leadingTrivia,
identifier: identifier.identifier.privatePrefixed("$"),
trailingTrivia: identifier.trailingTrivia
),
typeAnnotation: binding.typeAnnotation,
accessorBlock: AccessorBlockSyntax(
accessors: .accessors([
"""
get {
_$observationRegistrar.access(self, keyPath: \\.\(identifier))
return _\(identifier.identifier).projectedValue
}
""",
"""
set {
_$observationRegistrar.mutate(self, keyPath: \\.\(identifier), &_\(identifier).projectedValue, newValue, _$isIdentityEqual)
}
""",
])
)
)
}
}
return bindings
}
}
extension TypeSyntax {
fileprivate var presentationWrapped: GenericSpecializationExprSyntax {
GenericSpecializationExprSyntax(
expression: MemberAccessExprSyntax(
base: DeclReferenceExprSyntax(baseName: "ComposableArchitecture"),
name: "PresentationState"
),
genericArgumentClause: GenericArgumentClauseSyntax(
arguments: [
GenericArgumentSyntax(
argument: self
)
]
)
)
}
}