Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
19 kB · 571 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572import ComposableArchitecture2import ComposableAuthoringimport ComposableAuthoringWorkspaceimport ComposableWorkspaceimport Dependenciesimport Foundationimport Sharing
public struct DemoSession: Equatable, Sendable { public var displayName: String
public init(displayName: String) { self.displayName = displayName }}
public struct DemoGrant: Equatable, Sendable { public var author: String
public init(author: String) { self.author = author }}
public enum DemoContext: Equatable, Sendable { case festival}
public struct DemoDocument: Codable, Equatable, Sendable { public var performanceTime: String public var stageName: String
public init(performanceTime: String, stageName: String) { self.performanceTime = performanceTime self.stageName = stageName }}
public struct DemoChangeSet: Codable, Equatable, Sendable { public var document: DemoDocument
public init(document: DemoDocument) { self.document = document }}
public struct DemoAuthenticationClient: Sendable { public var signIn: @Sendable () async throws -> DemoSession public var signOut: @Sendable (DemoSession) async throws -> Void
public init( signIn: @escaping @Sendable () async throws -> DemoSession, signOut: @escaping @Sendable (DemoSession) async throws -> Void ) { self.signIn = signIn self.signOut = signOut }}
extension DemoAuthenticationClient: TestDependencyKey { public static var testValue: Self { Self( signIn: { throw DemoSystemError.unimplemented }, signOut: { _ in throw DemoSystemError.unimplemented } ) }}
enum DemoSessionChanged: FeatureEventKey { typealias Value = DemoSession?}
public struct DemoProposalClient: Sendable { public var merge: @Sendable () async throws -> Void
public init(merge: @escaping @Sendable () async throws -> Void) { self.merge = merge }}
extension DemoProposalClient: TestDependencyKey { public static var testValue: Self { Self(merge: { throw DemoSystemError.unimplemented }) }}
@Featurepublic struct DemoAuthentication { @Dependency(DemoAuthenticationClient.self) var client
public init() {}
public struct State { @StoreTaskID public var request @Shared public var session: DemoSession?
public init(session: Shared<DemoSession?>) { self._session = session }
public var phase: Phase { if request.isRunning { return session == nil ? .signingIn : .signingOut(session!) } return session.map(Phase.signedIn) ?? .signedOut } }
public enum Phase: Equatable, Sendable { case signedIn(DemoSession) case signingIn case signingOut(DemoSession) case signedOut }
public enum Action { case signInButtonTapped case signOutButtonTapped }
public var body: some Feature { Update { state, action in switch action { case .signInButtonTapped: guard state.session == nil, !state.request.isRunning else { return } let client = client store.addTask(id: state.request) { let session = try await client.signIn() try Task.checkCancellation() var didSignIn = false let transition = try store.modify { guard $0.session == nil else { return } $0.$session.withLock { $0 = session } didSignIn = true } await transition?.value guard didSignIn else { return } let sessionChange = try store.post( key: DemoSessionChanged.self, value: session ) await sessionChange?.value }
case .signOutButtonTapped: guard let session = state.session, !state.request.isRunning else { return } let client = client store.addTask(id: state.request) { try await client.signOut(session) try Task.checkCancellation() var didSignOut = false let transition = try store.modify { guard $0.session == session else { return } $0.$session.withLock { $0 = nil } didSignOut = true } await transition?.value guard didSignOut else { return } let sessionChange = try store.post( key: DemoSessionChanged.self, value: nil ) await sessionChange?.value } } } }}
@Featurepublic struct DemoEventEditor { public init() {}
public struct State { @Shared public var document: DemoDocument public var recordedProposal: WorkspaceProposal? @Trigger<WorkspaceProposal> public var proposalRecorded
public init(document: Shared<DemoDocument>) { self._document = document } }
public enum Action { case movePerformanceButtonTapped case renameStageButtonTapped }
public var body: some Feature { Update { state, action in switch action { case .movePerformanceButtonTapped: state.$document.withLock { $0.performanceTime = "11:00 AM" } case .renameStageButtonTapped: state.$document.withLock { $0.stageName = $0.stageName == "Meadow" ? "Meadow Stage" : "Forest Stage" } } } .onTrigger(store.proposalRecorded) { proposal, state in state.recordedProposal = proposal } }}
@Featurepublic struct WorkspaceDemo { public typealias Editing = AuthoringWorkspace< DemoEventEditor, DemoContext, DemoSession, DemoGrant, WorkspaceDemoSystem, DemoChangeSet >
@Dependency(DemoProposalClient.self) var proposalClient
public init() {}
public struct State { public var authentication: DemoAuthentication.State public var editing: Editing.State
public init( session: Shared<DemoSession?>, grant: Shared<DemoGrant?>, document: Shared<DemoDocument> ) { self.authentication = DemoAuthentication.State(session: session) self.editing = Editing.State( context: .festival, grant: grant ) { Workspace<DemoEventEditor, WorkspaceDemoSystem, DemoChangeSet>.State( workspaceID: WorkspaceID(rawValue: "demo-festival") ) { DemoEventEditor.State(document: document) } } } }
public enum Action { case applicationStarted case authentication(DemoAuthentication.Action) case editing(Editing.Action) case mergeProposalButtonTapped }
public var body: some Feature { Scope(\.authentication, action: \.authentication) { DemoAuthentication() } Scope(\.editing, action: \.editing) { AuthoringWorkspace(proposalRecordedTrigger: \.proposalRecorded) { DemoEventEditor() } } Update { state, action in switch action { case .applicationStarted: store.addTask { let restoration = try store.send( .editing(.authoring(.restoreAuthorization)) ) await restoration?.value }
case .authentication, .editing: break
case .mergeProposalButtonTapped: guard state.editing.content.content.recordedProposal != nil else { return } let proposalClient = proposalClient store.addTask { try await proposalClient.merge() let selection = try store.send( .editing( .authoring( .content(.contextRefreshRequested) ) ) ) await selection?.value } } } .onEvent(DemoSessionChanged.self) { session, _ in store.addTask { let invalidation = try store.send( .editing(.authoring(.authorizationInvalidated)) ) await invalidation?.value if session != nil { let refresh = try store.send( .editing(.authoring(.refreshAdmission)) ) await refresh?.value } } } }}
public actor WorkspaceDemoSystem { private let document: SharedReader<DemoDocument> private let grant: SharedReader<DemoGrant?> private let session: SharedReader<DemoSession?>
private var prepared = false private var proposal: WorkspaceProposal? private var proposalDocument: DemoDocument? private var referenceDocument = DemoDocument( performanceTime: "10:00 AM", stageName: "Meadow" ) private var baseline = WorkspaceBranchReference( branch: WorkspaceBranch(rawValue: "main"), revision: WorkspaceRevision(rawValue: "main-1") ) private var revision = 1
public init( session: SharedReader<DemoSession?>, grant: SharedReader<DemoGrant?>, document: SharedReader<DemoDocument> ) { self.session = session self.grant = grant self.document = document }
public nonisolated var authenticationClient: DemoAuthenticationClient { DemoAuthenticationClient( signIn: { DemoSession(displayName: "Festival Editor") }, signOut: { _ in } ) }
public nonisolated var authoringClient: AuthoringClient<DemoContext, DemoSession, DemoGrant> { AuthoringClient( admission: { _ in guard let session = self.session.wrappedValue else { return .unavailable } return .eligible(session) }, enter: { _, eligibility in guard self.session.wrappedValue == eligibility else { throw DemoSystemError.signedOut } return DemoGrant(author: eligibility.displayName) }, exit: { _, _ in }, restore: { _ in guard let session = self.session.wrappedValue else { return nil } return self.grant.wrappedValue.map { _ in DemoGrant(author: session.displayName) } } ) }
public nonisolated var proposalClient: DemoProposalClient { DemoProposalClient(merge: { try await self.mergeProposal() }) }
public nonisolated var workspaceClient: WorkspaceClient<WorkspaceDemoSystem, DemoChangeSet> { WorkspaceClient( prepareSource: { workspaceID in let baseline = try await self.prepareSource(workspaceID: workspaceID) return WorkspaceSession(source: self, baseline: baseline) }, captureChanges: { source, workspaceID in return try await source.captureChanges( workspaceID: workspaceID ) }, publishProposal: { source, workspaceID, attempt in return try await source.publishProposal( workspaceID: workspaceID, attempt: attempt ) }, publishChanges: { source, workspaceID, attempt in return try await source.publishChanges( workspaceID: workspaceID, attempt: attempt ) }, reconcile: { current, workspaceID in return try await current.source.reconcile( workspaceID: workspaceID, current: current ) }, discardChangesAndUpdate: { current, workspaceID in return try await current.source.discardChangesAndUpdate( workspaceID: workspaceID, current: current ) }, selectBranch: { source, workspaceID, branch in return try await source.selectBranch( workspaceID: workspaceID, branch: branch ) } ) }
private func authorize(_ workspaceID: WorkspaceID) throws { guard workspaceID == WorkspaceID(rawValue: "demo-festival") else { throw DemoSystemError.wrongWorkspace } guard session.wrappedValue != nil, grant.wrappedValue != nil else { throw WorkspaceAuthorizationError.invalidated } }
private func captureChanges( workspaceID: WorkspaceID ) throws -> WorkspaceChangeCapture<DemoChangeSet> { try authorize(workspaceID) return document.wrappedValue == referenceDocument ? .noChanges : .changes(DemoChangeSet(document: document.wrappedValue)) }
private func prepareSource( workspaceID: WorkspaceID ) throws -> WorkspaceBranchReference { try authorize(workspaceID) prepared = true return baseline }
private func publishProposal( workspaceID: WorkspaceID, attempt: WorkspaceProposalAttempt<DemoChangeSet> ) throws -> WorkspaceProposalPublication { try authorize(workspaceID) guard prepared else { throw DemoSystemError.sourceNotPrepared }
revision += 1 let revisionNumber = revision let proposalRevision = WorkspaceRevision(rawValue: "proposal-\(revisionNumber)") let publication = WorkspaceProposal( id: WorkspaceProposalID(rawValue: "demo-proposal-\(revisionNumber)"), revision: proposalRevision, target: attempt.target ) proposal = publication proposalDocument = attempt.changes.document return WorkspaceProposalPublication(proposal: publication) }
private func publishChanges( workspaceID: WorkspaceID, attempt: WorkspacePublicationAttempt<DemoChangeSet> ) throws -> WorkspacePublishedChanges<WorkspaceDemoSystem> { try authorize(workspaceID) guard prepared else { throw DemoSystemError.sourceNotPrepared } guard baseline == attempt.target else { throw DemoSystemError.contextDoesNotMatchReference } revision += 1 referenceDocument = attempt.changes.document let published = WorkspaceBranchReference( branch: attempt.target.branch, revision: WorkspaceRevision(rawValue: "published-\(revision)") ) baseline = published return WorkspacePublishedChanges( published: published, session: WorkspaceSession(source: self, baseline: baseline) ) }
private func mergeProposal() throws { guard session.wrappedValue != nil, grant.wrappedValue != nil else { throw WorkspaceAuthorizationError.invalidated } guard proposal != nil, proposalDocument != nil else { throw DemoSystemError.proposalMissing } revision += 1 referenceDocument = proposalDocument! baseline = WorkspaceBranchReference( branch: baseline.branch, revision: WorkspaceRevision(rawValue: "main-\(revision)") ) self.proposal = nil self.proposalDocument = nil }
private func reconcile( workspaceID: WorkspaceID, current: WorkspaceSession<WorkspaceDemoSystem> ) throws -> WorkspaceReconciliationResult<WorkspaceDemoSystem> { try authorize(workspaceID) guard baseline.branch == current.baseline.branch else { throw DemoSystemError.contextDoesNotMatchReference } return .current(WorkspaceSession(source: self, baseline: baseline)) }
private func discardChangesAndUpdate( workspaceID: WorkspaceID, current: WorkspaceSession<WorkspaceDemoSystem> ) throws -> WorkspaceSession<WorkspaceDemoSystem> { try authorize(workspaceID) guard baseline.branch == current.baseline.branch else { throw DemoSystemError.contextDoesNotMatchReference } return WorkspaceSession(source: self, baseline: baseline) }
private func selectBranch( workspaceID: WorkspaceID, branch: WorkspaceBranch ) throws -> WorkspaceBranchReference { try authorize(workspaceID) baseline = WorkspaceBranchReference( branch: branch, revision: WorkspaceRevision(rawValue: "\(branch.rawValue)-\(revision)") ) return baseline }}
@MainActorpublic func makeWorkspaceDemoStore() -> StoreOf<WorkspaceDemo> { let session = Shared<DemoSession?>(value: nil) let grant = Shared<DemoGrant?>(value: nil) let document = Shared( value: DemoDocument( performanceTime: "10:00 AM", stageName: "Meadow" ) ) let system = WorkspaceDemoSystem( session: SharedReader(session), grant: SharedReader(grant), document: SharedReader(document) ) return Store( initialState: WorkspaceDemo.State( session: session, grant: grant, document: document ) ) { WorkspaceDemo() .dependency(system.authenticationClient) .dependency(system.authoringClient) .dependency(system.proposalClient) .dependency(system.workspaceClient) }}
private enum DemoSystemError: Error { case proposalMissing case contextDoesNotMatchReference case signedOut case sourceNotPrepared case unimplemented case wrongWorkspace}