Skip to main content

helper_comparison/
helper_comparison.rs

1//! Standalone synchronous teaching model. No upstream bug or adoption claim.
2//! Run with cargo test --locked --example helper_comparison.
3use 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
12// BEGIN local lifecycle: count separately from common host and tests.
13struct 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}
48// END local lifecycle.
49
50// BEGIN library adapter: same application-facing result, no local lifecycle.
51struct 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}
73// END library adapter.
74
75// Shared host/test plumbing is required in either implementation.
76enum 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        // No await, callback or intervening mutation between comparison and write.
96        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; // Bounded synthetic sequence; production uses checked arithmetic.
136        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}