1use edit_session::{EditSession, FinishError};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6struct Context {
7 document: u64,
8 object: u64,
9 revision: u64,
10}
11
12struct LocalDraft {
14 original: i32,
15 preview: i32,
16 context: Context,
17 valid: bool,
18}
19impl LocalDraft {
20 fn new(value: i32, context: Context) -> Self {
21 Self {
22 original: value,
23 preview: value,
24 context,
25 valid: true,
26 }
27 }
28 fn propose(&mut self, value: Result<i32, &'static str>) {
29 self.valid = value.is_ok();
30 if let Ok(value) = value {
31 self.preview = value;
32 }
33 }
34 fn finish(self, live: i32, context: Context) -> Result<Option<i32>, &'static str> {
35 if context != self.context {
36 return Err("context changed");
37 }
38 if live != self.original {
39 return Err("value changed");
40 }
41 if !self.valid {
42 return Err("invalid proposal");
43 }
44 Ok((self.preview != self.original).then_some(self.preview))
45 }
46 fn cancel(self) {}
47}
48struct LibraryDraft(EditSession<i32, Context>);
52impl LibraryDraft {
53 fn new(value: i32, context: Context) -> Self {
54 Self(EditSession::new(value, context))
55 }
56 fn propose(&mut self, value: Result<i32, &'static str>) {
57 let _ = self.0.propose(value);
58 }
59 fn finish(self, live: i32, context: Context) -> Result<Option<i32>, &'static str> {
60 self.0
61 .finish(&live, &context)
62 .map(|v| v.map(|c| c.after))
63 .map_err(|e| match e {
64 FinishError::ContextChanged => "context changed",
65 FinishError::ValueChanged => "value changed",
66 FinishError::InvalidProposal => "invalid proposal",
67 })
68 }
69 fn cancel(self) {
70 self.0.cancel();
71 }
72}
73enum Draft {
77 Local(LocalDraft),
78 Library(LibraryDraft),
79}
80impl Draft {
81 fn open(library: bool, host: &Host) -> Self {
82 if library {
83 Self::Library(LibraryDraft::new(host.value, host.context))
84 } else {
85 Self::Local(LocalDraft::new(host.value, host.context))
86 }
87 }
88 fn propose(&mut self, value: Result<i32, &'static str>) {
89 match self {
90 Self::Local(d) => d.propose(value),
91 Self::Library(d) => d.propose(value),
92 }
93 }
94 fn finish(self, host: &mut Host) -> Result<(), &'static str> {
95 let proposal = match self {
97 Self::Local(d) => d.finish(host.value, host.context),
98 Self::Library(d) => d.finish(host.value, host.context),
99 }?;
100 if let Some(value) = proposal {
101 host.apply(value);
102 }
103 Ok(())
104 }
105 fn cancel(self, _host: &mut Host) {
106 match self {
107 Self::Local(d) => d.cancel(),
108 Self::Library(d) => d.cancel(),
109 }
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
114struct Host {
115 value: i32,
116 context: Context,
117 commands: usize,
118 redo: usize,
119}
120impl Host {
121 fn new() -> Self {
122 Self {
123 value: 100,
124 context: Context {
125 document: 7,
126 object: 2,
127 revision: 42,
128 },
129 commands: 0,
130 redo: 1,
131 }
132 }
133 fn apply(&mut self, value: i32) {
134 self.value = value;
135 self.context.revision += 1; self.commands += 1;
137 self.redo = 0;
138 }
139}
140
141fn main() {
142 for library in [false, true] {
143 let label = if library {
144 "edit-session (current checkout)"
145 } else {
146 "local helper"
147 };
148 let mut host = Host::new();
149 let mut draft = Draft::open(library, &host);
150 draft.propose(Ok(75));
151 host.apply(120);
152 draft.cancel(&mut host);
153 println!(
154 "{label}: Cancel after external edit -> live {}, revision {}, commands {}",
155 host.value, host.context.revision, host.commands
156 );
157 let mut draft = Draft::open(library, &host);
158 draft.propose(Ok(90));
159 host.context.object = 3;
160 println!(
161 "{label}: same value, different object -> {:?}",
162 draft.finish(&mut host)
163 );
164 let mut draft = Draft::open(library, &host);
165 draft.propose(Ok(90));
166 host.apply(150);
167 host.apply(120);
168 println!("{label}: edit then Undo -> {:?}", draft.finish(&mut host));
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 fn each(test: impl Fn(bool)) {
176 for library in [false, true] {
177 test(library);
178 }
179 }
180 #[test]
181 fn cancel_preserves_external_edit_and_history() {
182 each(|library| {
183 let mut h = Host::new();
184 let mut d = Draft::open(library, &h);
185 d.propose(Ok(75));
186 h.apply(120);
187 let before = h.clone();
188 d.cancel(&mut h);
189 assert_eq!(h, before);
190 });
191 }
192 #[test]
193 fn same_value_different_object_is_rejected() {
194 each(|library| {
195 let mut h = Host::new();
196 let mut d = Draft::open(library, &h);
197 d.propose(Ok(75));
198 h.context.object = 3;
199 let before = h.clone();
200 assert_eq!(d.finish(&mut h), Err("context changed"));
201 assert_eq!(h, before);
202 });
203 }
204 #[test]
205 fn same_value_different_document_is_rejected() {
206 each(|library| {
207 let mut h = Host::new();
208 let mut d = Draft::open(library, &h);
209 d.propose(Ok(75));
210 h.context.document = 8;
211 let before = h.clone();
212 assert_eq!(d.finish(&mut h), Err("context changed"));
213 assert_eq!(h, before);
214 });
215 }
216 #[test]
217 fn edit_then_undo_is_still_stale() {
218 each(|library| {
219 let mut h = Host::new();
220 let mut d = Draft::open(library, &h);
221 d.propose(Ok(75));
222 h.apply(120);
223 h.apply(100);
224 let before = h.clone();
225 assert_eq!(d.finish(&mut h), Err("context changed"));
226 assert_eq!(h, before);
227 });
228 }
229 #[test]
230 fn changed_value_without_revision_is_rejected() {
231 each(|library| {
232 let mut h = Host::new();
233 let mut d = Draft::open(library, &h);
234 d.propose(Ok(75));
235 h.value = 120;
236 let before = h.clone();
237 assert_eq!(d.finish(&mut h), Err("value changed"));
238 assert_eq!(h, before);
239 });
240 }
241 #[test]
242 fn invalid_input_cannot_commit_previous_valid_preview() {
243 each(|library| {
244 let mut h = Host::new();
245 let mut d = Draft::open(library, &h);
246 d.propose(Ok(75));
247 d.propose(Err("not a number"));
248 let before = h.clone();
249 assert_eq!(d.finish(&mut h), Err("invalid proposal"));
250 assert_eq!(h, before);
251 });
252 }
253 #[test]
254 fn no_op_preserves_redo() {
255 each(|library| {
256 let mut h = Host::new();
257 let d = Draft::open(library, &h);
258 let before = h.clone();
259 assert_eq!(d.finish(&mut h), Ok(()));
260 assert_eq!(h, before);
261 });
262 }
263 #[test]
264 fn fresh_draft_commits_once() {
265 each(|library| {
266 let mut h = Host::new();
267 let mut d = Draft::open(library, &h);
268 d.propose(Ok(75));
269 assert_eq!(d.finish(&mut h), Ok(()));
270 assert_eq!(
271 (h.value, h.context.revision, h.commands, h.redo),
272 (75, 43, 1, 0)
273 );
274 });
275 }
276}