use crate::{
buffer::{BufferEdit, BufferEditError, BufferEditShape},
ecs::components::buffer::{BufferEntity, BufferRevision},
plugin::{CapabilityAtom, PluginIdentity},
text_stream::TextRevision,
};
use bevy::prelude::Message;
use std::fmt::{Debug, Formatter};
use std::num::NonZeroU64;
pub use super::error::BufferEditRejectionReason;
pub use super::error::EditHistoryRejectionReason;
pub use super::error::PluginProposalDecisionRejectionReason;
#[derive(Clone, Debug, Eq, Message, PartialEq)]
pub struct BufferEditRequested {
pub target: BufferEntity,
pub edit: BufferEdit,
}
#[derive(Clone, Debug, Eq, Message, PartialEq)]
pub struct RevisionGuardedBufferEditRequested {
pub target: BufferEntity,
pub provenance: PluginBufferEditProvenance,
pub edit: BufferEdit,
}
impl RevisionGuardedBufferEditRequested {
#[must_use]
pub(crate) fn with_proposal_id(mut self, proposal: PluginProposalId) -> Self {
self.provenance = self.provenance.with_proposal_id(proposal);
self
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PluginProposalId(NonZeroU64);
impl PluginProposalId {
#[must_use]
pub(crate) const fn from_nonzero(value: NonZeroU64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
}
#[derive(Clone, Copy, Debug, Eq, Message, PartialEq)]
pub struct PluginBufferEditProposalApplyRequested {
pub proposal: PluginProposalId,
}
#[derive(Clone, Copy, Debug, Eq, Message, PartialEq)]
pub struct PluginBufferEditProposalRejectRequested {
pub proposal: PluginProposalId,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginBufferEditProposalDecision {
Apply,
Reject,
}
#[derive(Clone, Copy, Debug, Eq, Message, PartialEq)]
pub struct PluginBufferEditProposalDecisionRejected {
pub proposal: PluginProposalId,
pub decision: PluginBufferEditProposalDecision,
pub reason: PluginProposalDecisionRejectionReason,
}
#[derive(Clone, Eq, Message, PartialEq)]
pub struct PluginBufferEditProposalReceipt {
proposal: PluginProposalId,
source_identity: PluginIdentity,
capability: PluginBufferProposalCapability,
target: BufferEntity,
base_revision: TextRevision,
proposed: BufferEditShape,
outcome: PluginBufferEditProposalReceiptOutcome,
}
impl PluginBufferEditProposalReceipt {
#[must_use]
pub(crate) fn owner_applied(
provenance: &PluginBufferEditProvenance,
target: BufferEntity,
proposed: BufferEditShape,
previous_revision: BufferRevision,
current_revision: BufferRevision,
) -> Option<Self> {
Some(Self {
proposal: provenance.proposal_id()?,
source_identity: provenance.source_identity.clone(),
capability: provenance.capability(),
target,
base_revision: provenance.base_revision(),
proposed,
outcome: PluginBufferEditProposalReceiptOutcome::Applied {
previous_revision,
current_revision,
},
})
}
#[must_use]
pub(crate) fn owner_rejected(
provenance: &PluginBufferEditProvenance,
target: BufferEntity,
proposed: BufferEditShape,
reason: &BufferEditRejectionReason,
) -> Option<Self> {
Some(Self {
proposal: provenance.proposal_id()?,
source_identity: provenance.source_identity.clone(),
capability: provenance.capability(),
target,
base_revision: provenance.base_revision(),
proposed,
outcome: PluginBufferEditProposalReceiptOutcome::Rejected {
owner: PluginBufferEditProposalReceiptOwner::BufferOwner,
reason: PluginBufferEditProposalReceiptRejectionReason::from(reason),
},
})
}
#[must_use]
pub(crate) fn rejected_before_owner(
proposal: PluginProposalId,
provenance: &PluginBufferEditProvenance,
target: BufferEntity,
proposed: BufferEditShape,
) -> Self {
Self {
proposal,
source_identity: provenance.source_identity.clone(),
capability: provenance.capability(),
target,
base_revision: provenance.base_revision(),
proposed,
outcome: PluginBufferEditProposalReceiptOutcome::Rejected {
owner: PluginBufferEditProposalReceiptOwner::ProposalLane,
reason: PluginBufferEditProposalReceiptRejectionReason::RejectedBeforeOwner,
},
}
}
#[must_use]
pub const fn proposal(&self) -> PluginProposalId {
self.proposal
}
#[must_use]
pub fn source_identity(&self) -> &str {
self.source_identity_proof().as_str()
}
#[must_use]
pub const fn source_identity_proof(&self) -> &PluginIdentity {
&self.source_identity
}
#[must_use]
pub const fn capability(&self) -> PluginBufferProposalCapability {
self.capability
}
#[must_use]
pub const fn target(&self) -> BufferEntity {
self.target
}
#[must_use]
pub const fn base_revision(&self) -> TextRevision {
self.base_revision
}
#[must_use]
pub const fn proposed(&self) -> &BufferEditShape {
&self.proposed
}
#[must_use]
pub const fn outcome(&self) -> &PluginBufferEditProposalReceiptOutcome {
&self.outcome
}
}
impl Debug for PluginBufferEditProposalReceipt {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginBufferEditProposalReceipt")
.field("proposal", &self.proposal)
.field("source_identity", &self.source_identity.as_str())
.field("capability", &self.capability)
.field("target", &PluginProposalReceiptTargetShape::Buffer)
.field("base_revision", &self.base_revision)
.field("proposed", &self.proposed)
.field("outcome", &self.outcome)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PluginBufferEditProposalReceiptOutcome {
Applied {
previous_revision: BufferRevision,
current_revision: BufferRevision,
},
Rejected {
owner: PluginBufferEditProposalReceiptOwner,
reason: PluginBufferEditProposalReceiptRejectionReason,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginBufferEditProposalReceiptOwner {
ProposalLane,
BufferOwner,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PluginBufferEditProposalReceiptRejectionReason {
RejectedBeforeOwner,
MissingTarget,
InvalidEdit {
error: BufferEditError,
},
StaleRevision {
expected: TextRevision,
actual: TextRevision,
},
}
impl From<&BufferEditRejectionReason> for PluginBufferEditProposalReceiptRejectionReason {
fn from(reason: &BufferEditRejectionReason) -> Self {
match reason {
BufferEditRejectionReason::MissingTarget { .. } => Self::MissingTarget,
BufferEditRejectionReason::InvalidEdit { error } => Self::InvalidEdit {
error: error.clone(),
},
BufferEditRejectionReason::StaleRevision { expected, actual } => Self::StaleRevision {
expected: *expected,
actual: *actual,
},
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PluginProposalReceiptTargetShape {
Buffer,
}
#[derive(Clone, Eq, PartialEq)]
pub struct PluginBufferEditProvenance {
source_identity: PluginIdentity,
capability: PluginBufferProposalCapability,
base_revision: TextRevision,
proposal: Option<PluginProposalId>,
}
impl PluginBufferEditProvenance {
#[must_use]
pub const fn buffer_propose_edit(
source_identity: PluginIdentity,
base_revision: TextRevision,
) -> Self {
Self {
source_identity,
capability: PluginBufferProposalCapability::buffer_propose_edit(),
base_revision,
proposal: None,
}
}
#[must_use]
pub(crate) const fn with_proposal_id(mut self, proposal: PluginProposalId) -> Self {
self.proposal = Some(proposal);
self
}
#[must_use]
pub fn source_identity(&self) -> &str {
self.source_identity_proof().as_str()
}
#[must_use]
pub const fn source_identity_proof(&self) -> &PluginIdentity {
&self.source_identity
}
#[must_use]
pub const fn capability(&self) -> PluginBufferProposalCapability {
self.capability
}
#[must_use]
pub const fn base_revision(&self) -> TextRevision {
self.base_revision
}
#[must_use]
pub const fn proposal_id(&self) -> Option<PluginProposalId> {
self.proposal
}
}
impl Debug for PluginBufferEditProvenance {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginBufferEditProvenance")
.field("source_identity", &self.source_identity.as_str())
.field("capability", &self.capability)
.field("base_revision", &self.base_revision)
.field("proposal", &self.proposal)
.finish()
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct PluginBufferProposalCapability {
atom: CapabilityAtom,
}
impl PluginBufferProposalCapability {
#[must_use]
pub const fn buffer_propose_edit() -> Self {
Self {
atom: CapabilityAtom::BufferProposeEdit,
}
}
#[must_use]
pub const fn atom(self) -> CapabilityAtom {
self.atom
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.atom.as_str()
}
}
impl Debug for PluginBufferProposalCapability {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Message, PartialEq)]
pub struct BufferEdited {
pub target: BufferEntity,
pub previous_revision: crate::ecs::components::buffer::BufferRevision,
pub current_revision: crate::ecs::components::buffer::BufferRevision,
}
#[derive(Clone, Debug, Eq, Message, PartialEq)]
pub struct BufferEditRejected {
pub target: BufferEntity,
pub rejected: BufferEditShape,
pub reason: BufferEditRejectionReason,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EditHistoryDirection {
Undo,
Redo,
}
#[derive(Clone, Copy, Debug, Eq, Message, PartialEq)]
pub struct EditHistoryRequested {
pub target: BufferEntity,
pub direction: EditHistoryDirection,
}
impl EditHistoryRequested {
#[must_use]
pub const fn undo(target: BufferEntity) -> Self {
Self {
target,
direction: EditHistoryDirection::Undo,
}
}
#[must_use]
pub const fn redo(target: BufferEntity) -> Self {
Self {
target,
direction: EditHistoryDirection::Redo,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Message, PartialEq)]
pub struct EditHistoryApplied {
pub target: BufferEntity,
pub direction: EditHistoryDirection,
pub previous_revision: crate::ecs::components::buffer::BufferRevision,
pub current_revision: crate::ecs::components::buffer::BufferRevision,
}
#[derive(Clone, Debug, Eq, Message, PartialEq)]
pub struct EditHistoryRejected {
pub target: BufferEntity,
pub direction: EditHistoryDirection,
pub reason: EditHistoryRejectionReason,
}
#[cfg(test)]
mod tests {
use super::{
BufferRevision, PluginBufferEditProposalReceipt, PluginBufferEditProvenance,
PluginProposalId,
};
use crate::{
buffer::{BufferEdit, BufferEditShape},
ecs::components::buffer::BufferEntity,
plugin::{CapabilityAtom, PluginIdentity},
text_stream::TextRevision,
};
use bevy::prelude::Entity;
use std::num::NonZeroU64;
#[test]
fn provenance_and_receipts_preserve_identity_proofs() {
let identity = PluginIdentity::try_new("proof-source").expect("identity");
let provenance = PluginBufferEditProvenance::buffer_propose_edit(
identity.clone(),
TextRevision::from(7),
)
.with_proposal_id(PluginProposalId::from_nonzero(
NonZeroU64::new(1).expect("non-zero id"),
));
let edit = BufferEdit::insert(0, "secret payload");
let receipt = PluginBufferEditProposalReceipt::owner_applied(
&provenance,
BufferEntity(Entity::from_raw_u32(1).expect("entity")),
BufferEditShape::from_edit(&edit),
BufferRevision {
revision: TextRevision::from(7),
},
BufferRevision {
revision: TextRevision::from(8),
},
)
.expect("proposal id should produce a receipt");
assert_eq!(provenance.source_identity_proof(), &identity);
assert_eq!(
provenance.capability().atom(),
CapabilityAtom::BufferProposeEdit
);
assert_eq!(provenance.capability().as_str(), "buffer.propose_edit");
assert_eq!(receipt.source_identity_proof(), &identity);
assert_eq!(
receipt.capability().atom(),
CapabilityAtom::BufferProposeEdit
);
assert_eq!(receipt.source_identity(), "proof-source");
assert!(!format!("{receipt:?}").contains("secret payload"));
}
}