Skip to main content

tui_dispatch_debug/
session.rs

1use crate::cli::DebugCliArgs;
2use crate::debug::{glob_match, ActionLoggerConfig, DebugLayer, DebugState};
3use crate::replay::ReplayItem;
4use crate::snapshot::{ActionSnapshot, SnapshotError, StateSnapshot};
5use ratatui::backend::{Backend, TestBackend};
6use ratatui::layout::{Rect, Size};
7use ratatui::Terminal;
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10use std::cell::RefCell;
11use std::error::Error;
12use std::fmt;
13use std::future::Future;
14use std::io;
15use std::io::Write;
16use std::path::PathBuf;
17use std::rc::Rc;
18use std::time::Duration;
19use tui_dispatch_core::bus::EventOutcome;
20use tui_dispatch_core::runtime::{EffectContext, RenderContext, Runtime, RuntimeStore};
21use tui_dispatch_core::store::{ComposedMiddleware, Middleware};
22use tui_dispatch_core::testing::RenderHarness;
23use tui_dispatch_core::{
24    Action, ActionParams, BindingContext, ComponentId, EventBus, EventContext, EventKind,
25    EventRoutingState, Keybindings,
26};
27
28/// Records actions for --debug-actions-out snapshots with optional filtering.
29#[derive(Clone)]
30pub struct DebugActionRecorder<A> {
31    actions: Rc<RefCell<Vec<A>>>,
32    filter: ActionLoggerConfig,
33}
34
35impl<A> DebugActionRecorder<A> {
36    pub fn new(filter: ActionLoggerConfig) -> Self {
37        Self {
38            actions: Rc::new(RefCell::new(Vec::new())),
39            filter,
40        }
41    }
42
43    pub fn actions(&self) -> Vec<A>
44    where
45        A: Clone,
46    {
47        self.actions.borrow().clone()
48    }
49}
50
51impl<S, A: Action> Middleware<S, A> for DebugActionRecorder<A> {
52    fn before(&mut self, action: &A, _state: &S) -> bool {
53        if self.filter.should_log(action.name()) {
54            self.actions.borrow_mut().push(action.clone());
55        }
56        true
57    }
58
59    fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
60        vec![]
61    }
62}
63
64/// Output from a debug-aware app run.
65pub struct DebugRunOutput<S> {
66    state: S,
67    render_output: Option<String>,
68}
69
70impl<S> DebugRunOutput<S> {
71    pub fn new(state: S, render_output: Option<String>) -> Self {
72        Self {
73            state,
74            render_output,
75        }
76    }
77
78    pub fn state(&self) -> &S {
79        &self.state
80    }
81
82    pub fn into_state(self) -> S {
83        self.state
84    }
85
86    pub fn render_output(&self) -> Option<&str> {
87        self.render_output.as_deref()
88    }
89
90    pub fn take_render_output(self) -> Option<String> {
91        self.render_output
92    }
93
94    pub fn write_render_output(&self) -> io::Result<()> {
95        if let Some(output) = self.render_output.as_ref() {
96            let mut stdout = io::stdout();
97            stdout.write_all(output.as_bytes())?;
98            stdout.flush()?;
99        }
100        Ok(())
101    }
102}
103
104pub type DebugSessionResult<T> = Result<T, DebugSessionError>;
105
106#[derive(Debug)]
107pub enum DebugSessionError {
108    Snapshot(SnapshotError),
109    Fallback(Box<dyn Error + Send + Sync>),
110    MissingActionRecorder { path: PathBuf },
111}
112
113impl DebugSessionError {
114    fn fallback<E>(error: E) -> Self
115    where
116        E: Error + Send + Sync + 'static,
117    {
118        Self::Fallback(Box::new(error))
119    }
120}
121
122impl fmt::Display for DebugSessionError {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::Snapshot(error) => write!(f, "snapshot error: {error:?}"),
126            Self::Fallback(error) => write!(f, "fallback error: {error}"),
127            Self::MissingActionRecorder { path } => write!(
128                f,
129                "debug actions out requested but no recorder attached: {}",
130                path.display()
131            ),
132        }
133    }
134}
135
136impl std::error::Error for DebugSessionError {}
137
138/// Error from replay with await markers.
139#[derive(Debug)]
140pub enum ReplayError {
141    /// Timeout waiting for action to be dispatched.
142    Timeout { pattern: String },
143    /// Broadcast channel closed unexpectedly.
144    ChannelClosed,
145}
146
147impl fmt::Display for ReplayError {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::Timeout { pattern } => {
151                write!(f, "timeout waiting for action matching '{pattern}'")
152            }
153            Self::ChannelClosed => write!(f, "action broadcast channel closed"),
154        }
155    }
156}
157
158impl std::error::Error for ReplayError {}
159
160/// Wait for an action matching one of the patterns to be dispatched.
161async fn wait_for_action(
162    action_rx: &mut tokio::sync::broadcast::Receiver<String>,
163    patterns: &[String],
164    timeout: Duration,
165) -> Result<(), ReplayError> {
166    let deadline = tokio::time::Instant::now() + timeout;
167
168    loop {
169        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
170        if remaining.is_zero() {
171            return Err(ReplayError::Timeout {
172                pattern: patterns.join(" | "),
173            });
174        }
175
176        match tokio::time::timeout(remaining, action_rx.recv()).await {
177            Ok(Ok(action_name)) => {
178                // Check if any pattern matches
179                for pattern in patterns {
180                    if glob_match(pattern, &action_name) {
181                        return Ok(());
182                    }
183                }
184                // Not a match, keep waiting
185            }
186            Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => {
187                return Err(ReplayError::ChannelClosed);
188            }
189            Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {
190                // Missed some messages, but keep waiting
191                continue;
192            }
193            Err(_) => {
194                // Timeout
195                return Err(ReplayError::Timeout {
196                    pattern: patterns.join(" | "),
197                });
198            }
199        }
200    }
201}
202
203/// Helper for wiring debug CLI flags into an app runtime.
204#[derive(Debug)]
205pub struct DebugSession {
206    args: DebugCliArgs,
207}
208
209impl DebugSession {
210    pub fn new(args: DebugCliArgs) -> Self {
211        Self { args }
212    }
213
214    pub fn args(&self) -> &DebugCliArgs {
215        &self.args
216    }
217
218    pub fn enabled(&self) -> bool {
219        self.args.enabled
220    }
221
222    pub fn render_once(&self) -> bool {
223        self.args.render_once
224    }
225
226    pub fn use_alt_screen(&self) -> bool {
227        !self.args.render_once
228    }
229
230    pub fn action_filter(&self) -> ActionLoggerConfig {
231        self.args.action_filter()
232    }
233
234    pub fn auto_fetch(&self) -> bool {
235        self.args.auto_fetch()
236    }
237
238    pub fn load_state_or_else<S, F, E>(&self, fallback: F) -> DebugSessionResult<S>
239    where
240        S: DeserializeOwned,
241        F: FnOnce() -> Result<S, E>,
242        E: Error + Send + Sync + 'static,
243    {
244        if let Some(path) = self.args.state_in.as_ref() {
245            StateSnapshot::load_json(path)
246                .map(|snapshot| snapshot.into_state())
247                .map_err(DebugSessionError::Snapshot)
248        } else {
249            fallback().map_err(DebugSessionError::fallback)
250        }
251    }
252
253    pub async fn load_state_or_else_async<S, F, Fut, E>(&self, fallback: F) -> DebugSessionResult<S>
254    where
255        S: DeserializeOwned,
256        F: FnOnce() -> Fut,
257        Fut: Future<Output = Result<S, E>>,
258        E: Error + Send + Sync + 'static,
259    {
260        if let Some(path) = self.args.state_in.as_ref() {
261            StateSnapshot::load_json(path)
262                .map(|snapshot| snapshot.into_state())
263                .map_err(DebugSessionError::Snapshot)
264        } else {
265            fallback().await.map_err(DebugSessionError::fallback)
266        }
267    }
268
269    pub fn load_state_or<S, F>(&self, fallback: F) -> DebugSessionResult<S>
270    where
271        S: DeserializeOwned,
272        F: FnOnce() -> S,
273    {
274        self.load_state_or_else(|| Ok::<S, std::convert::Infallible>(fallback()))
275    }
276
277    /// Load replay items from `--debug-actions-in`.
278    ///
279    /// This auto-detects the format: either a simple `Vec<A>` or `Vec<ReplayItem<A>>`.
280    /// Both formats are supported for backwards compatibility.
281    pub fn load_replay_items<A>(&self) -> DebugSessionResult<Vec<ReplayItem<A>>>
282    where
283        A: DeserializeOwned,
284    {
285        let Some(path) = self.args.actions_in.as_ref() else {
286            return Ok(Vec::new());
287        };
288
289        let contents =
290            std::fs::read_to_string(path).map_err(|e| DebugSessionError::Snapshot(e.into()))?;
291
292        // Try parsing as Vec<ReplayItem<A>> first (supports await markers)
293        if let Ok(items) = serde_json::from_str::<Vec<ReplayItem<A>>>(&contents) {
294            return Ok(items);
295        }
296
297        // Fall back to Vec<A> (simple action list, wrap in ReplayItem::Action)
298        let actions: Vec<A> = serde_json::from_str(&contents)
299            .map_err(|e| DebugSessionError::Snapshot(SnapshotError::Json(e)))?;
300        Ok(actions.into_iter().map(ReplayItem::Action).collect())
301    }
302
303    /// Load actions from `--debug-actions-in` (legacy API, ignores await markers).
304    #[deprecated(note = "Use load_replay_items instead")]
305    pub fn load_actions<A>(&self) -> DebugSessionResult<Vec<A>>
306    where
307        A: DeserializeOwned,
308    {
309        self.load_replay_items().map(|items| {
310            items
311                .into_iter()
312                .filter_map(|item| item.into_action())
313                .collect()
314        })
315    }
316
317    pub fn action_recorder<A: Action>(&self) -> Option<DebugActionRecorder<A>> {
318        self.args
319            .actions_out
320            .as_ref()
321            .map(|_| DebugActionRecorder::new(self.action_filter()))
322    }
323
324    pub fn middleware_with_recorder<S, A: Action>(
325        &self,
326    ) -> (ComposedMiddleware<S, A>, Option<DebugActionRecorder<A>>) {
327        let mut middleware = ComposedMiddleware::new();
328        let recorder = self.action_recorder();
329        if let Some(recorder) = recorder.clone() {
330            middleware.add(recorder);
331        }
332        (middleware, recorder)
333    }
334
335    pub fn save_actions<A>(
336        &self,
337        recorder: Option<&DebugActionRecorder<A>>,
338    ) -> DebugSessionResult<()>
339    where
340        A: Clone + Serialize,
341    {
342        let Some(path) = self.args.actions_out.as_ref() else {
343            return Ok(());
344        };
345        let Some(recorder) = recorder else {
346            return Err(DebugSessionError::MissingActionRecorder {
347                path: path.to_path_buf(),
348            });
349        };
350        ActionSnapshot::new(recorder.actions())
351            .save_json(path)
352            .map_err(DebugSessionError::Snapshot)
353    }
354
355    /// Save JSON schema for the state type if `--debug-state-schema-out` was set.
356    #[cfg(feature = "json-schema")]
357    pub fn save_state_schema<S>(&self) -> DebugSessionResult<()>
358    where
359        S: crate::JsonSchema,
360    {
361        if let Some(path) = self.args.state_schema_out.as_ref() {
362            crate::save_schema::<S, _>(path).map_err(DebugSessionError::Snapshot)
363        } else {
364            Ok(())
365        }
366    }
367
368    /// Save JSON schema for replay items (actions + await markers).
369    ///
370    /// This generates a schema for `Vec<ReplayItem<A>>` which includes:
371    /// - All action variants from `A`
372    /// - `_await` and `_await_any` markers for async coordination
373    /// - An `$defs.awaitable_actions` list of Did* action names
374    #[cfg(feature = "json-schema")]
375    pub fn save_actions_schema<A>(&self) -> DebugSessionResult<()>
376    where
377        A: crate::JsonSchema,
378    {
379        if let Some(path) = self.args.actions_schema_out.as_ref() {
380            crate::save_replay_schema::<A, _>(path).map_err(DebugSessionError::Snapshot)
381        } else {
382            Ok(())
383        }
384    }
385
386    /// Get the replay timeout duration from CLI args.
387    pub fn replay_timeout(&self) -> Duration {
388        Duration::from_secs(self.args.replay_timeout)
389    }
390
391    #[allow(clippy::too_many_arguments)]
392    pub async fn run_effect_app<B, S, A, E, St, FInit, FRender, FEvent, FQuit, FEffect, R>(
393        &self,
394        terminal: &mut Terminal<B>,
395        mut store: St,
396        debug_layer: DebugLayer<A>,
397        replay_items: Vec<ReplayItem<A>>,
398        auto_action: Option<A>,
399        quit_action: Option<A>,
400        init_runtime: FInit,
401        mut render: FRender,
402        mut map_event: FEvent,
403        mut should_quit: FQuit,
404        mut handle_effect: FEffect,
405    ) -> io::Result<DebugRunOutput<S>>
406    where
407        B: Backend,
408        B::Error: Send + Sync + 'static,
409        S: Clone + DebugState + Serialize + 'static,
410        A: Action + ActionParams,
411        St: RuntimeStore<S, A, E>,
412        FInit: FnOnce(&mut Runtime<S, A, E, tui_dispatch_core::runtime::Direct, St>),
413        FRender: FnMut(&mut ratatui::Frame, Rect, &S, RenderContext),
414        FEvent: FnMut(&EventKind, &S) -> R,
415        R: Into<EventOutcome<A>>,
416        FQuit: FnMut(&A) -> bool,
417        FEffect: FnMut(E, &mut EffectContext<A>),
418    {
419        let size = terminal.size().unwrap_or_else(|_| Size::new(80, 24));
420        let width = size.width.max(1);
421        let height = size.height.max(1);
422        let auto_action = auto_action;
423
424        // Check if replay items contain any await markers
425        let has_awaits = replay_items.iter().any(|item| item.is_await());
426        let replay_timeout = self.replay_timeout();
427
428        if self.args.render_once {
429            let final_state = if has_awaits {
430                // Need runtime for effects when replay has await markers
431                let runtime = Runtime::from_store(store);
432                let mut action_rx = runtime.subscribe_actions();
433                let action_tx = runtime.action_tx();
434
435                // Spawn replay task that processes items with await support
436                let replay_items_clone = replay_items;
437                let auto_action_clone = auto_action.clone();
438                let auto_fetch = self.auto_fetch();
439                let replay_handle = tokio::spawn(async move {
440                    for item in replay_items_clone {
441                        match item {
442                            ReplayItem::Action(action) => {
443                                let _ = action_tx.send(action);
444                            }
445                            ReplayItem::AwaitOne { _await: pattern } => {
446                                wait_for_action(&mut action_rx, &[pattern], replay_timeout).await?;
447                            }
448                            ReplayItem::AwaitAny {
449                                _await_any: patterns,
450                            } => {
451                                wait_for_action(&mut action_rx, &patterns, replay_timeout).await?;
452                            }
453                        }
454                    }
455                    if auto_fetch {
456                        if let Some(action) = auto_action_clone {
457                            let _ = action_tx.send(action);
458                        }
459                    }
460                    Ok::<(), ReplayError>(())
461                });
462
463                let mut runtime = runtime;
464                init_runtime(&mut runtime);
465
466                let quit_action = quit_action.ok_or_else(|| {
467                    io::Error::new(
468                        io::ErrorKind::InvalidInput,
469                        "replay with await markers requires a quit action",
470                    )
471                })?;
472
473                // Send quit after replay completes
474                let action_tx = runtime.action_tx();
475                let quit = quit_action.clone();
476                tokio::spawn(async move {
477                    // Wait for replay to complete
478                    let _ = replay_handle.await;
479                    // Small delay to let final effects settle
480                    tokio::time::sleep(Duration::from_millis(100)).await;
481                    let _ = action_tx.send(quit);
482                });
483
484                let backend = TestBackend::new(width, height);
485                let mut test_terminal = match Terminal::new(backend) {
486                    Ok(terminal) => terminal,
487                    Err(error) => match error {},
488                };
489                runtime
490                    .run_with_effects(
491                        &mut test_terminal,
492                        |_frame, _area, _state, _ctx| {},
493                        |_event, _state| EventOutcome::<A>::ignored(),
494                        |action| should_quit(action),
495                        |effect, ctx| handle_effect(effect, ctx),
496                    )
497                    .await?;
498
499                runtime.state().clone()
500            } else {
501                // Simple dispatch mode (no effects, ignores awaits)
502                for item in replay_items {
503                    if let ReplayItem::Action(action) = item {
504                        let _ = store.dispatch(action);
505                    }
506                }
507                if self.auto_fetch() {
508                    if let Some(action) = auto_action.clone() {
509                        let _ = store.dispatch(action);
510                    }
511                }
512                store.state().clone()
513            };
514
515            let mut harness = RenderHarness::new(width, height);
516            let output = harness.render_to_string_plain(|frame| {
517                render(frame, frame.area(), &final_state, RenderContext::default());
518            });
519
520            return Ok(DebugRunOutput::new(final_state, Some(output)));
521        }
522
523        // Normal interactive mode - just enqueue actions (ignore awaits)
524        let debug_layer = debug_layer
525            .with_state_snapshots::<S>()
526            .active(self.args.enabled);
527        let mut runtime = Runtime::from_store(store).with_debug(debug_layer);
528        init_runtime(&mut runtime);
529
530        for item in replay_items {
531            if let ReplayItem::Action(action) = item {
532                runtime.enqueue(action);
533            }
534        }
535        if self.auto_fetch() {
536            if let Some(action) = auto_action {
537                runtime.enqueue(action);
538            }
539        }
540
541        let result = runtime
542            .run_with_effects(
543                terminal,
544                |frame, area, state, render_ctx| {
545                    render(frame, area, state, render_ctx);
546                },
547                |event, state| map_event(event, state),
548                |action| should_quit(action),
549                |effect, ctx| handle_effect(effect, ctx),
550            )
551            .await;
552
553        match result {
554            Ok(()) => Ok(DebugRunOutput::new(runtime.state().clone(), None)),
555            Err(err) => Err(err),
556        }
557    }
558
559    #[allow(clippy::too_many_arguments)]
560    pub async fn run_effect_app_with_bus<B, S, A, E, St, Id, Ctx, FInit, FRender, FQuit, FEffect>(
561        &self,
562        terminal: &mut Terminal<B>,
563        mut store: St,
564        debug_layer: DebugLayer<A>,
565        replay_items: Vec<ReplayItem<A>>,
566        auto_action: Option<A>,
567        quit_action: Option<A>,
568        init_runtime: FInit,
569        bus: EventBus<S, A, Id, Ctx>,
570        keybindings: Keybindings<Ctx>,
571        mut render: FRender,
572        mut should_quit: FQuit,
573        mut handle_effect: FEffect,
574    ) -> io::Result<DebugRunOutput<S>>
575    where
576        B: Backend,
577        B::Error: Send + Sync + 'static,
578        S: Clone + DebugState + Serialize + EventRoutingState<Id, Ctx> + 'static,
579        A: Action + ActionParams,
580        St: RuntimeStore<S, A, E>,
581        Id: ComponentId + 'static,
582        Ctx: BindingContext + 'static,
583        FInit: FnOnce(&mut Runtime<S, A, E, tui_dispatch_core::runtime::Direct, St>),
584        FRender: FnMut(&mut ratatui::Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
585        FQuit: FnMut(&A) -> bool,
586        FEffect: FnMut(E, &mut EffectContext<A>),
587    {
588        let size = terminal.size().unwrap_or_else(|_| Size::new(80, 24));
589        let width = size.width.max(1);
590        let height = size.height.max(1);
591        let auto_action = auto_action;
592
593        // Check if replay items contain any await markers
594        let has_awaits = replay_items.iter().any(|item| item.is_await());
595        let replay_timeout = self.replay_timeout();
596
597        if self.args.render_once {
598            let final_state = if has_awaits {
599                // Need runtime for effects when replay has await markers
600                let runtime = Runtime::from_store(store);
601                let mut action_rx = runtime.subscribe_actions();
602                let action_tx = runtime.action_tx();
603
604                // Spawn replay task that processes items with await support
605                let replay_items_clone = replay_items;
606                let auto_action_clone = auto_action.clone();
607                let auto_fetch = self.auto_fetch();
608                let replay_handle = tokio::spawn(async move {
609                    for item in replay_items_clone {
610                        match item {
611                            ReplayItem::Action(action) => {
612                                let _ = action_tx.send(action);
613                            }
614                            ReplayItem::AwaitOne { _await: pattern } => {
615                                wait_for_action(&mut action_rx, &[pattern], replay_timeout).await?;
616                            }
617                            ReplayItem::AwaitAny {
618                                _await_any: patterns,
619                            } => {
620                                wait_for_action(&mut action_rx, &patterns, replay_timeout).await?;
621                            }
622                        }
623                    }
624                    if auto_fetch {
625                        if let Some(action) = auto_action_clone {
626                            let _ = action_tx.send(action);
627                        }
628                    }
629                    Ok::<(), ReplayError>(())
630                });
631
632                let mut runtime = runtime;
633                init_runtime(&mut runtime);
634
635                let quit_action = quit_action.ok_or_else(|| {
636                    io::Error::new(
637                        io::ErrorKind::InvalidInput,
638                        "replay with await markers requires a quit action",
639                    )
640                })?;
641
642                // Send quit after replay completes
643                let action_tx = runtime.action_tx();
644                let quit = quit_action.clone();
645                tokio::spawn(async move {
646                    // Wait for replay to complete
647                    let _ = replay_handle.await;
648                    // Small delay to let final effects settle
649                    tokio::time::sleep(Duration::from_millis(100)).await;
650                    let _ = action_tx.send(quit);
651                });
652
653                let backend = TestBackend::new(width, height);
654                let mut test_terminal = match Terminal::new(backend) {
655                    Ok(terminal) => terminal,
656                    Err(error) => match error {},
657                };
658                runtime
659                    .run_with_effects(
660                        &mut test_terminal,
661                        |_frame, _area, _state, _ctx| {},
662                        |_event, _state| EventOutcome::<A>::ignored(),
663                        |action| should_quit(action),
664                        |effect, ctx| handle_effect(effect, ctx),
665                    )
666                    .await?;
667
668                runtime.state().clone()
669            } else {
670                // Simple dispatch mode (no effects, ignores awaits)
671                for item in replay_items {
672                    if let ReplayItem::Action(action) = item {
673                        let _ = store.dispatch(action);
674                    }
675                }
676                if self.auto_fetch() {
677                    if let Some(action) = auto_action.clone() {
678                        let _ = store.dispatch(action);
679                    }
680                }
681                store.state().clone()
682            };
683
684            let mut harness = RenderHarness::new(width, height);
685            let output = harness.render_to_string_plain(|frame| {
686                let mut event_ctx = EventContext::<Id>::default();
687                render(
688                    frame,
689                    frame.area(),
690                    &final_state,
691                    RenderContext::default(),
692                    &mut event_ctx,
693                );
694            });
695
696            return Ok(DebugRunOutput::new(final_state, Some(output)));
697        }
698
699        // Normal interactive mode - just enqueue actions (ignore awaits)
700        let debug_layer = debug_layer
701            .with_state_snapshots::<S>()
702            .active(self.args.enabled);
703        let mut runtime = Runtime::from_store(store).with_debug(debug_layer);
704        init_runtime(&mut runtime);
705
706        for item in replay_items {
707            if let ReplayItem::Action(action) = item {
708                runtime.enqueue(action);
709            }
710        }
711        if self.auto_fetch() {
712            if let Some(action) = auto_action {
713                runtime.enqueue(action);
714            }
715        }
716
717        let mut bus_runtime = runtime.with_event_bus(bus, keybindings);
718
719        let result = bus_runtime
720            .run_with_effects(
721                terminal,
722                |frame, area, state, render_ctx, event_ctx| {
723                    render(frame, area, state, render_ctx, event_ctx);
724                },
725                |action| should_quit(action),
726                |effect, ctx| handle_effect(effect, ctx),
727            )
728            .await;
729
730        match result {
731            Ok(()) => Ok(DebugRunOutput::new(bus_runtime.state().clone(), None)),
732            Err(err) => Err(err),
733        }
734    }
735}