1use edit_session::{EditSession, FinishError};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6struct Context {
7 document: u64,
8 target: u64,
9 revision: u64,
10}
11
12#[derive(Debug, PartialEq, Eq)]
13enum ConfirmError {
14 Closed,
15 Draft(FinishError),
16 PermissionDenied,
17 RevisionExhausted,
18}
19
20#[derive(Debug, PartialEq, Eq)]
21enum Outcome {
22 Applied,
23 Unchanged,
24}
25
26struct Host {
27 value: String,
28 context: Context,
29 allowed: bool,
30 undo: Vec<String>,
31 redo: Vec<String>,
32}
33
34impl Host {
35 fn new() -> Self {
36 Self {
37 value: "original".into(),
38 context: Context {
39 document: 7,
40 target: 2,
41 revision: 42,
42 },
43 allowed: true,
44 undo: Vec::new(),
45 redo: Vec::new(),
46 }
47 }
48
49 fn apply(&mut self, proposed: &str) -> Result<(), ConfirmError> {
50 if !self.allowed {
53 return Err(ConfirmError::PermissionDenied);
54 }
55 let next_revision = self
56 .context
57 .revision
58 .checked_add(1)
59 .ok_or(ConfirmError::RevisionExhausted)?;
60 self.undo.push(self.value.clone());
61 self.value = proposed.to_owned();
62 self.redo.clear();
63 self.context.revision = next_revision;
64 Ok(())
65 }
66}
67
68struct Dialog {
69 text: String,
71 draft: Option<EditSession<String, Context>>,
72}
73
74impl Dialog {
75 fn open(host: &Host) -> Self {
76 Self {
77 text: host.value.clone(),
78 draft: Some(EditSession::new(host.value.clone(), host.context)),
79 }
80 }
81
82 fn set_text(&mut self, text: &str) -> Result<(), &'static str> {
83 let draft = self.draft.as_mut().ok_or("dialog is closed")?;
84 self.text = text.to_owned();
85 draft.propose(if text.trim().is_empty() {
86 Err("enter a title")
87 } else {
88 Ok(self.text.clone())
89 })
90 }
91
92 fn confirm(&mut self, host: &mut Host) -> Result<Outcome, ConfirmError> {
93 let draft = self.draft.as_ref().ok_or(ConfirmError::Closed)?;
96 draft
97 .check(&host.value, &host.context)
98 .map_err(ConfirmError::Draft)?;
99 let outcome = if draft.preview() == draft.original() {
100 Outcome::Unchanged } else {
102 host.apply(draft.preview())?;
104 Outcome::Applied
105 };
106 self.cancel(); Ok(outcome)
108 }
109
110 fn cancel(&mut self) {
111 if let Some(draft) = self.draft.take() {
112 draft.cancel();
113 }
114 }
115}
116
117fn main() -> Result<(), ConfirmError> {
118 let mut host = Host::new();
119 let mut dialog = Dialog::open(&host);
120 assert_eq!(dialog.set_text("edited title"), Ok(()));
121 host.allowed = false;
122 assert_eq!(
123 dialog.confirm(&mut host),
124 Err(ConfirmError::PermissionDenied)
125 );
126 assert_eq!(dialog.text, "edited title");
127 assert!(dialog.draft.is_some());
128 host.allowed = true;
129 assert_eq!(dialog.confirm(&mut host)?, Outcome::Applied);
130 assert_eq!(host.value, "edited title");
131 assert_eq!(host.undo, ["original"]);
132 assert_eq!(dialog.confirm(&mut host), Err(ConfirmError::Closed));
133 println!("Denied command retained the draft; retry applied once and closed the dialog.");
134 Ok(())
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn denied_edit() -> (Host, Dialog) {
142 let mut host = Host::new();
143 host.redo.push("future".into());
144 let mut dialog = Dialog::open(&host);
145 assert_eq!(dialog.set_text("edited"), Ok(()));
146 host.allowed = false;
147 assert_eq!(
148 dialog.confirm(&mut host),
149 Err(ConfirmError::PermissionDenied)
150 );
151 (host, dialog)
152 }
153
154 #[test]
155 fn rejection_preserves_fields_document_revision_and_history_then_retry_commits_once() {
156 let (mut host, mut dialog) = denied_edit();
157 assert_eq!(host.value, "original");
158 assert_eq!(host.context.revision, 42);
159 assert!(host.undo.is_empty());
160 assert_eq!(host.redo, ["future"]);
161 assert_eq!(dialog.text, "edited");
162 assert!(dialog.draft.is_some());
163 host.allowed = true;
164 assert_eq!(dialog.confirm(&mut host), Ok(Outcome::Applied));
165 assert_eq!(host.value, "edited");
166 assert_eq!(host.context.revision, 43);
167 assert_eq!(host.undo, ["original"]);
168 assert!(host.redo.is_empty());
169 assert!(dialog.draft.is_none());
170 }
171
172 #[test]
173 fn repeated_confirm_cannot_duplicate_the_command() {
174 let (mut host, mut dialog) = denied_edit();
175 host.allowed = true;
176 assert_eq!(dialog.confirm(&mut host), Ok(Outcome::Applied));
177 assert_eq!(dialog.confirm(&mut host), Err(ConfirmError::Closed));
178 assert_eq!(host.undo, ["original"]);
179 assert_eq!(host.context.revision, 43);
180 }
181
182 #[test]
183 fn invalid_correction_keeps_raw_input_and_does_not_apply_last_valid_preview() {
184 let (mut host, mut dialog) = denied_edit();
185 host.allowed = true;
186 assert!(dialog.set_text(" ").is_err());
187 assert_eq!(
188 dialog.confirm(&mut host),
189 Err(ConfirmError::Draft(FinishError::InvalidProposal))
190 );
191 assert_eq!(dialog.text, " ");
192 assert_eq!(host.value, "original");
193 assert!(host.undo.is_empty());
194 assert_eq!(dialog.set_text("corrected"), Ok(()));
195 assert_eq!(dialog.confirm(&mut host), Ok(Outcome::Applied));
196 assert_eq!(host.value, "corrected");
197 }
198
199 #[test]
200 fn retry_never_retargets_a_different_document_or_target() {
201 for change_document in [true, false] {
202 let (mut host, mut dialog) = denied_edit();
203 host.allowed = true;
204 if change_document {
205 host.context.document += 1;
206 } else {
207 host.context.target += 1;
208 }
209 assert_eq!(
210 dialog.confirm(&mut host),
211 Err(ConfirmError::Draft(FinishError::ContextChanged))
212 );
213 assert_eq!(host.value, "original");
214 assert!(host.undo.is_empty());
215 assert_eq!(host.redo, ["future"]);
216 assert_eq!(dialog.text, "edited");
217 }
218 }
219
220 #[test]
221 fn edit_then_undo_before_retry_is_stale_even_at_the_original_value() {
222 let (mut host, mut dialog) = denied_edit();
223 host.allowed = true;
224 host.context.revision += 2; assert_eq!(
226 dialog.confirm(&mut host),
227 Err(ConfirmError::Draft(FinishError::ContextChanged))
228 );
229 assert_eq!(host.value, "original");
230 assert!(host.undo.is_empty());
231 assert_eq!(host.redo, ["future"]);
232 }
233
234 #[test]
235 fn unversioned_host_value_change_still_blocks_retry() {
236 let (mut host, mut dialog) = denied_edit();
237 host.allowed = true;
238 host.value = "newer value".into();
239 assert_eq!(
240 dialog.confirm(&mut host),
241 Err(ConfirmError::Draft(FinishError::ValueChanged))
242 );
243 assert_eq!(host.value, "newer value");
244 assert!(host.undo.is_empty());
245 }
246
247 #[test]
248 fn noop_skips_the_denied_command_and_preserves_redo() {
249 let (mut host, mut dialog) = denied_edit();
250 assert_eq!(dialog.set_text("original"), Ok(()));
251 assert_eq!(dialog.confirm(&mut host), Ok(Outcome::Unchanged));
252 assert!(dialog.draft.is_none());
253 assert_eq!(host.context.revision, 42);
254 assert!(host.undo.is_empty());
255 assert_eq!(host.redo, ["future"]);
256 }
257
258 #[test]
259 fn cancel_after_rejection_discards_only_the_local_draft() {
260 let (mut host, mut dialog) = denied_edit();
261 dialog.cancel();
262 assert_eq!(dialog.confirm(&mut host), Err(ConfirmError::Closed));
263 assert_eq!(host.value, "original");
264 assert_eq!(host.context.revision, 42);
265 assert!(host.undo.is_empty());
266 assert_eq!(host.redo, ["future"]);
267 }
268}