Add application read models and thin TUI command adapter
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
use super::{
|
||||
CommandResult, DeliveryDestinationSummary, FirstIssueAttempt, FirstIssueStatus,
|
||||
FirstIssueSummary, OperationError, OperationExecutor, QrStatus, ReissueSummary,
|
||||
RestartPreviewSummary, RestorePreviewSummary, SensitiveCommand, UiCommand,
|
||||
};
|
||||
use crate::app::{Application, FirstIssueDeliveryOutcome, RestorePreview};
|
||||
use crate::delivery::files::PreparedDestination;
|
||||
use crate::delivery::{QrCodeOutput, QrError};
|
||||
use crate::domain::PublicKey;
|
||||
|
||||
enum DeliveryMode {
|
||||
FirstIssue,
|
||||
Reissue,
|
||||
}
|
||||
|
||||
struct PreparedDestinationContext {
|
||||
identity: PublicKey,
|
||||
include_qr: bool,
|
||||
mode: DeliveryMode,
|
||||
attempt: FirstIssueAttempt,
|
||||
generation: u64,
|
||||
destination: PreparedDestination,
|
||||
}
|
||||
|
||||
struct RestorePreviewContext {
|
||||
_archive_name: String,
|
||||
generation: u64,
|
||||
preview: RestorePreview,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ApplicationExecutor {
|
||||
prepared_destination: Option<PreparedDestinationContext>,
|
||||
restore_preview: Option<RestorePreviewContext>,
|
||||
}
|
||||
|
||||
impl OperationExecutor for ApplicationExecutor {
|
||||
fn execute(
|
||||
&mut self,
|
||||
app: &mut Application,
|
||||
command: UiCommand,
|
||||
) -> Result<CommandResult, OperationError> {
|
||||
self.execute_at(app, command, 0)
|
||||
}
|
||||
|
||||
fn execute_at(
|
||||
&mut self,
|
||||
app: &mut Application,
|
||||
command: UiCommand,
|
||||
generation: u64,
|
||||
) -> Result<CommandResult, OperationError> {
|
||||
let delivery_continuation = matches!(command, UiCommand::DeliverFirstIssue { .. });
|
||||
let restore_continuation = matches!(
|
||||
command,
|
||||
UiCommand::ConfirmRestore { .. } | UiCommand::CancelRestorePreview
|
||||
);
|
||||
if !delivery_continuation && !matches!(&command, UiCommand::CancelPreparedDelivery) {
|
||||
self.prepared_destination = None;
|
||||
}
|
||||
if !restore_continuation && !matches!(&command, UiCommand::PreviewRestore { .. }) {
|
||||
self.restore_preview = None;
|
||||
}
|
||||
let result = match command {
|
||||
UiCommand::CreateClient {
|
||||
alias,
|
||||
manual_addresses,
|
||||
created_utc,
|
||||
} => app
|
||||
.prepare_new_client(&alias, manual_addresses, created_utc.as_deref())
|
||||
.map(CommandResult::ClientCreated),
|
||||
UiCommand::RenameClient { identity, alias } => app
|
||||
.rename_client(identity, &alias)
|
||||
.map(|()| CommandResult::ClientRenamed),
|
||||
UiCommand::DeleteClient { identity } => app
|
||||
.delete_client(identity)
|
||||
.map(|record_removed| CommandResult::ClientDeleted { record_removed }),
|
||||
UiCommand::DiscardDraft => app.discard_draft().map(|()| CommandResult::DraftDiscarded),
|
||||
UiCommand::Commit => app.commit().map(CommandResult::Committed),
|
||||
UiCommand::CreateBackup => app.create_manual_backup().map(CommandResult::BackupCreated),
|
||||
UiCommand::ListBackups => app.list_backups().map(CommandResult::Backups),
|
||||
UiCommand::ImportSelectedFile { file_name } => app
|
||||
.import_selected_file(&file_name)
|
||||
.map(CommandResult::SelectedFileImported),
|
||||
UiCommand::PreviewRestart => app.preview_restart().map(|preview| {
|
||||
CommandResult::RestartPreview(RestartPreviewSummary {
|
||||
has_draft: preview.has_draft(),
|
||||
interface: preview.interface().to_owned(),
|
||||
systemd_unit: preview.systemd_unit().to_owned(),
|
||||
})
|
||||
}),
|
||||
UiCommand::Restart(choice) => {
|
||||
let result = app
|
||||
.preview_restart()
|
||||
.map(|preview| app.restart(preview, choice));
|
||||
result.map(CommandResult::Restarted)
|
||||
}
|
||||
UiCommand::PrepareDelivery {
|
||||
identity,
|
||||
path,
|
||||
include_qr,
|
||||
reissue,
|
||||
attempt,
|
||||
} => app
|
||||
.prepare_first_issue_destination(path)
|
||||
.map_err(crate::app::AppError::FileOutput)
|
||||
.map(|destination| {
|
||||
let summary = DeliveryDestinationSummary {
|
||||
identity,
|
||||
path: destination.path().to_path_buf(),
|
||||
overwrite_required: destination.overwrite_confirmation_required(),
|
||||
include_qr,
|
||||
reissue,
|
||||
attempt,
|
||||
prepared_generation: generation,
|
||||
};
|
||||
self.prepared_destination = Some(PreparedDestinationContext {
|
||||
identity,
|
||||
include_qr,
|
||||
mode: if reissue {
|
||||
DeliveryMode::Reissue
|
||||
} else {
|
||||
DeliveryMode::FirstIssue
|
||||
},
|
||||
attempt,
|
||||
generation,
|
||||
destination,
|
||||
});
|
||||
CommandResult::DeliveryDestination(summary)
|
||||
}),
|
||||
UiCommand::DeliverFirstIssue {
|
||||
attempt,
|
||||
overwrite_confirmed,
|
||||
prepared_generation,
|
||||
} => {
|
||||
let prepared = self
|
||||
.prepared_destination
|
||||
.take()
|
||||
.ok_or_else(|| OperationError {
|
||||
message: "No prepared output destination is available.".to_owned(),
|
||||
})?;
|
||||
if !matches!(prepared.mode, DeliveryMode::FirstIssue)
|
||||
|| prepared.attempt != attempt
|
||||
|| !destination_generation_matches(
|
||||
prepared.generation,
|
||||
prepared_generation,
|
||||
generation,
|
||||
)
|
||||
{
|
||||
return Err(OperationError {
|
||||
message: "Prepared output destination is stale; select it again."
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
let identity = prepared.identity;
|
||||
let include_qr = prepared.include_qr;
|
||||
let mut destination = prepared.destination;
|
||||
if destination.overwrite_confirmation_required() {
|
||||
if !overwrite_confirmed {
|
||||
return Err(OperationError {
|
||||
message: "Overwrite confirmation was not supplied.".to_owned(),
|
||||
});
|
||||
}
|
||||
destination = destination.confirm_overwrite().map_err(operation_error)?;
|
||||
}
|
||||
if include_qr {
|
||||
app.deliver_first_issue_file_and_qr(identity, attempt, destination)
|
||||
.map(|outcome| match outcome {
|
||||
FirstIssueDeliveryOutcome::Delivered(delivery) => {
|
||||
let (qr, qr_output) = qr_result(delivery.qr);
|
||||
CommandResult::FirstIssue(FirstIssueSummary {
|
||||
identity,
|
||||
status: FirstIssueStatus::Delivered,
|
||||
retry: None,
|
||||
activation_warning: delivery.activation_warning,
|
||||
qr,
|
||||
qr_output,
|
||||
})
|
||||
}
|
||||
FirstIssueDeliveryOutcome::PublishedButBookkeepingUncertain {
|
||||
value,
|
||||
..
|
||||
} => {
|
||||
let (qr, qr_output) = qr_result(value.qr);
|
||||
CommandResult::FirstIssue(FirstIssueSummary {
|
||||
identity,
|
||||
status: FirstIssueStatus::PublishedButBookkeepingUncertain,
|
||||
retry: None,
|
||||
activation_warning: value.activation_warning,
|
||||
qr,
|
||||
qr_output,
|
||||
})
|
||||
}
|
||||
FirstIssueDeliveryOutcome::Failed {
|
||||
error,
|
||||
retry,
|
||||
activation_warning,
|
||||
} => CommandResult::FirstIssue(FirstIssueSummary {
|
||||
identity,
|
||||
status: FirstIssueStatus::Failed(error),
|
||||
retry: Some(retry),
|
||||
activation_warning,
|
||||
qr: QrStatus::NotRequested,
|
||||
qr_output: None,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
app.deliver_first_issue_file(identity, attempt, destination)
|
||||
.map(|outcome| match outcome {
|
||||
FirstIssueDeliveryOutcome::Delivered(delivery) => {
|
||||
CommandResult::FirstIssue(FirstIssueSummary {
|
||||
identity,
|
||||
status: FirstIssueStatus::Delivered,
|
||||
retry: None,
|
||||
activation_warning: delivery.activation_warning,
|
||||
qr: QrStatus::NotRequested,
|
||||
qr_output: None,
|
||||
})
|
||||
}
|
||||
FirstIssueDeliveryOutcome::PublishedButBookkeepingUncertain {
|
||||
value,
|
||||
..
|
||||
} => CommandResult::FirstIssue(FirstIssueSummary {
|
||||
identity,
|
||||
status: FirstIssueStatus::PublishedButBookkeepingUncertain,
|
||||
retry: None,
|
||||
activation_warning: value.activation_warning,
|
||||
qr: QrStatus::NotRequested,
|
||||
qr_output: None,
|
||||
}),
|
||||
FirstIssueDeliveryOutcome::Failed {
|
||||
error,
|
||||
retry,
|
||||
activation_warning,
|
||||
} => CommandResult::FirstIssue(FirstIssueSummary {
|
||||
identity,
|
||||
status: FirstIssueStatus::Failed(error),
|
||||
retry: Some(retry),
|
||||
activation_warning,
|
||||
qr: QrStatus::NotRequested,
|
||||
qr_output: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
UiCommand::PreviewRestore { file_name } => {
|
||||
app.preview_restore(&file_name).map(|preview| {
|
||||
let changes = preview.change_summary(app.saved_bytes());
|
||||
let summary = RestorePreviewSummary {
|
||||
file_name: file_name.clone(),
|
||||
created_at_utc: preview.created_at_utc().to_owned(),
|
||||
reason: preview.reason(),
|
||||
historical_apply: preview.historical_apply().clone(),
|
||||
changes,
|
||||
preview_generation: generation,
|
||||
};
|
||||
self.restore_preview = Some(RestorePreviewContext {
|
||||
_archive_name: file_name,
|
||||
generation,
|
||||
preview,
|
||||
});
|
||||
CommandResult::RestorePreview(summary)
|
||||
})
|
||||
}
|
||||
UiCommand::CancelPreparedDelivery => {
|
||||
self.prepared_destination = None;
|
||||
Ok(CommandResult::Cancelled)
|
||||
}
|
||||
UiCommand::CancelRestorePreview => {
|
||||
self.restore_preview = None;
|
||||
Ok(CommandResult::Cancelled)
|
||||
}
|
||||
UiCommand::ConfirmRestore { preview_generation } => {
|
||||
let context = self.restore_preview.take().ok_or_else(|| OperationError {
|
||||
message: "Restore preview is no longer available; select the archive again."
|
||||
.to_owned(),
|
||||
})?;
|
||||
if !restore_preview_generation_matches(
|
||||
context.generation,
|
||||
preview_generation,
|
||||
generation,
|
||||
) {
|
||||
return Err(OperationError {
|
||||
message: "Restore preview is stale; select the archive again.".to_owned(),
|
||||
});
|
||||
}
|
||||
app.confirm_restore(context.preview)
|
||||
.map(CommandResult::Restored)
|
||||
}
|
||||
};
|
||||
result.map_err(|error| OperationError {
|
||||
message: error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_sensitive(
|
||||
&mut self,
|
||||
app: &mut Application,
|
||||
command: SensitiveCommand,
|
||||
) -> Result<CommandResult, OperationError> {
|
||||
match command {
|
||||
SensitiveCommand::PrepareInitialSetup(password) => app
|
||||
.prepare_initial_setup_from_sources(password.expose())
|
||||
.map(CommandResult::InitialSetupPrepared)
|
||||
.map_err(|error| OperationError {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
SensitiveCommand::ReissueClient {
|
||||
identity,
|
||||
password,
|
||||
overwrite_confirmed,
|
||||
prepared_generation,
|
||||
} => {
|
||||
let prepared = self
|
||||
.prepared_destination
|
||||
.take()
|
||||
.ok_or_else(|| OperationError {
|
||||
message: "No prepared output destination is available.".to_owned(),
|
||||
})?;
|
||||
if prepared.identity != identity
|
||||
|| !matches!(prepared.mode, DeliveryMode::Reissue)
|
||||
|| prepared.generation != prepared_generation
|
||||
{
|
||||
return Err(OperationError {
|
||||
message: "Prepared output destination is stale; select it again."
|
||||
.to_owned(),
|
||||
});
|
||||
}
|
||||
let include_qr = prepared.include_qr;
|
||||
let mut destination = prepared.destination;
|
||||
if destination.overwrite_confirmation_required() {
|
||||
if !overwrite_confirmed {
|
||||
return Err(OperationError {
|
||||
message: "Overwrite confirmation was not supplied.".to_owned(),
|
||||
});
|
||||
}
|
||||
destination = destination.confirm_overwrite().map_err(operation_error)?;
|
||||
}
|
||||
let delivery = app
|
||||
.reissue_client_file(identity, password.expose(), destination, include_qr)
|
||||
.map_err(|error| OperationError {
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
let (qr, qr_output) = match delivery.qr {
|
||||
None => (QrStatus::NotRequested, None),
|
||||
Some(result) => qr_result(result),
|
||||
};
|
||||
Ok(CommandResult::Reissued(ReissueSummary {
|
||||
activation_warning: delivery.activation_warning,
|
||||
qr,
|
||||
qr_output,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_sensitive_at(
|
||||
&mut self,
|
||||
app: &mut Application,
|
||||
command: SensitiveCommand,
|
||||
generation: u64,
|
||||
) -> Result<CommandResult, OperationError> {
|
||||
match &command {
|
||||
SensitiveCommand::ReissueClient {
|
||||
prepared_generation,
|
||||
..
|
||||
} if *prepared_generation != generation => {
|
||||
return Err(OperationError {
|
||||
message: "Prepared output destination is stale; select it again.".to_owned(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if matches!(command, SensitiveCommand::PrepareInitialSetup(_)) {
|
||||
self.prepared_destination = None;
|
||||
self.restore_preview = None;
|
||||
}
|
||||
self.execute_sensitive(app, command)
|
||||
}
|
||||
}
|
||||
|
||||
fn operation_error(error: impl std::fmt::Display) -> OperationError {
|
||||
OperationError {
|
||||
message: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn destination_generation_matches(stored: u64, requested: u64, current: u64) -> bool {
|
||||
stored == requested && stored == current
|
||||
}
|
||||
|
||||
pub(super) fn restore_preview_generation_matches(
|
||||
stored: u64,
|
||||
requested: u64,
|
||||
current: u64,
|
||||
) -> bool {
|
||||
stored == requested && current == requested.saturating_add(1)
|
||||
}
|
||||
|
||||
fn qr_result(result: Result<QrCodeOutput, QrError>) -> (QrStatus, Option<QrCodeOutput>) {
|
||||
match result {
|
||||
Ok(output) => (QrStatus::Ready, Some(output)),
|
||||
Err(error) => (QrStatus::Failed(error), None),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user