Skip to main content

wekan_cli/
app.rs

1use crate::{
2    command_result::{CommandSuccess, StartupUpdateOutcome},
3    commands::{self, RootCommand},
4    config::{ServerSelection, TargetResolver, profiles::FileProfileStore},
5    credentials::{
6        CredentialStore, KeyringCredentialStore, SecretInputProvider, SystemSecretInputProvider,
7    },
8    error::AppError,
9    input::{ConfirmationProvider, SystemConfirmationProvider},
10    output::OutputFormat,
11    update::{FileCache, StartupEligibility, UpdateService},
12};
13
14pub struct App<S, P, F = FileProfileStore, C = SystemConfirmationProvider> {
15    target_resolver: TargetResolver,
16    credential_store: S,
17    secret_input: P,
18    profile_store: F,
19    confirmation: C,
20    update_service_factory: fn() -> Result<UpdateService, AppError>,
21}
22
23impl
24    App<
25        KeyringCredentialStore,
26        SystemSecretInputProvider,
27        FileProfileStore,
28        SystemConfirmationProvider,
29    >
30{
31    pub fn production(selection: ServerSelection) -> Self {
32        Self::production_with_confirmation_interactivity(selection, true)
33    }
34
35    pub fn production_with_confirmation_interactivity(
36        selection: ServerSelection,
37        allow_interactive_confirmation: bool,
38    ) -> Self {
39        Self::with_dependencies(
40            selection,
41            KeyringCredentialStore,
42            SystemSecretInputProvider,
43            FileProfileStore::production(),
44            SystemConfirmationProvider::for_output(allow_interactive_confirmation),
45        )
46    }
47}
48
49impl<S, P, F> App<S, P, F, SystemConfirmationProvider>
50where
51    S: CredentialStore,
52    P: SecretInputProvider,
53    F: crate::config::profiles::ProfileStore,
54{
55    pub const fn with_profile_store(
56        selection: ServerSelection,
57        credential_store: S,
58        secret_input: P,
59        profile_store: F,
60    ) -> Self {
61        Self::with_dependencies(
62            selection,
63            credential_store,
64            secret_input,
65            profile_store,
66            SystemConfirmationProvider::interactive(),
67        )
68    }
69}
70
71impl<S, P, F, C> App<S, P, F, C>
72where
73    S: CredentialStore,
74    P: SecretInputProvider,
75    F: crate::config::profiles::ProfileStore,
76    C: ConfirmationProvider,
77{
78    pub const fn with_dependencies(
79        selection: ServerSelection,
80        credential_store: S,
81        secret_input: P,
82        profile_store: F,
83        confirmation: C,
84    ) -> Self {
85        Self {
86            target_resolver: TargetResolver::new(selection),
87            credential_store,
88            secret_input,
89            profile_store,
90            confirmation,
91            update_service_factory: UpdateService::production,
92        }
93    }
94
95    pub fn with_confirmation_provider<C2>(self, confirmation: C2) -> App<S, P, F, C2>
96    where
97        C2: ConfirmationProvider,
98    {
99        App {
100            target_resolver: self.target_resolver,
101            credential_store: self.credential_store,
102            secret_input: self.secret_input,
103            profile_store: self.profile_store,
104            confirmation,
105            update_service_factory: self.update_service_factory,
106        }
107    }
108
109    pub const fn credential_store(&self) -> &S {
110        &self.credential_store
111    }
112
113    pub(crate) fn check_for_update(
114        &self,
115        output: OutputFormat,
116        command: &RootCommand,
117    ) -> impl std::future::Future<Output = Result<StartupUpdateOutcome, AppError>> + '_ {
118        let eligible = StartupEligibility::production(
119            output,
120            matches!(command, RootCommand::SelfManagement(_)),
121        );
122        // Keep release HTTP state off the process entry future's stack.
123        Box::pin(async move {
124            if !eligible.eligible() {
125                return Ok(StartupUpdateOutcome::Continue);
126            }
127            // Discovery and cache failures are optional startup work. Failures
128            // after affirmative installation consent remain explicit errors.
129            let Ok(service) = (self.update_service_factory)() else {
130                return Ok(StartupUpdateOutcome::Continue);
131            };
132            let Ok(mut cache) = FileCache::production() else {
133                return Ok(StartupUpdateOutcome::Continue);
134            };
135            service.automatic(&mut cache, &self.confirmation).await
136        })
137    }
138
139    pub fn execute(
140        &self,
141        command: RootCommand,
142    ) -> impl std::future::Future<Output = Result<CommandSuccess, AppError>> + '_ {
143        // Keep the dispatch future off callers' stacks. The complete native
144        // response models make it large enough to overflow Windows test and
145        // embedding threads when nested in a multi-command workflow.
146        // Select local maintenance before constructing the large API dispatch
147        // future. Adding another CommandSuccess-producing await inside that
148        // state machine exceeds Windows' process-entry stack budget.
149        let task: std::pin::Pin<
150            Box<dyn std::future::Future<Output = Result<CommandSuccess, AppError>> + Send + '_>,
151        > = if let RootCommand::SelfManagement(args) = command {
152            if self.target_resolver.has_explicit_target() {
153                Box::pin(async {
154                    Err(AppError::invalid_input(
155                        "--server and --profile cannot be used with self-management commands",
156                    ))
157                })
158            } else {
159                match (self.update_service_factory)() {
160                    Ok(service) => Box::pin(commands::self_management::dispatch(
161                        args.command,
162                        service,
163                        &self.confirmation,
164                    )),
165                    Err(error) => Box::pin(async move { Err(error) }),
166                }
167            }
168        } else {
169            Box::pin(self.execute_command(command))
170        };
171        task
172    }
173
174    async fn execute_command(&self, command: RootCommand) -> Result<CommandSuccess, AppError> {
175        if let RootCommand::Skill(args) = command {
176            return commands::skill::dispatch(args.command);
177        }
178        if let RootCommand::Profile(args) = command {
179            if self.target_resolver.has_explicit_target() {
180                return Err(AppError::invalid_input(
181                    "--server and --profile cannot be used with profile-management commands",
182                ));
183            }
184            return commands::profile::dispatch(
185                args.command,
186                &self.profile_store,
187                &self.credential_store,
188                &self.confirmation,
189            );
190        }
191        command.validate_input()?;
192        let policy = match &command {
193            RootCommand::Auth(args) => args.command.missing_profile_resolution(),
194            RootCommand::System(_) => crate::config::MissingProfileResolution::AnonymousRequest,
195            _ => crate::config::MissingProfileResolution::Reject,
196        };
197        let mut target = self.target_resolver.resolve(&self.profile_store, policy)?;
198        let profile = target.profile().to_owned();
199        let result = match command {
200            RootCommand::System(args) => {
201                commands::system::dispatch(args.command, target.client_factory()).await
202            }
203            RootCommand::Admin(args) => {
204                commands::admin::dispatch(
205                    args.command,
206                    target.client_factory(),
207                    &self.credential_store,
208                    &self.secret_input,
209                    &self.confirmation,
210                )
211                .await
212            }
213            RootCommand::Rule(args) => {
214                commands::rules::dispatch(
215                    args.command,
216                    target.client_factory(),
217                    &self.credential_store,
218                    &self.confirmation,
219                )
220                .await
221            }
222            RootCommand::Integration(args) => {
223                commands::integrations::dispatch(
224                    args.command,
225                    target.client_factory(),
226                    &self.credential_store,
227                    &self.secret_input,
228                    &self.confirmation,
229                )
230                .await
231            }
232            RootCommand::Dependency(args) => {
233                commands::dependencies::dispatch(
234                    args.command,
235                    target.client_factory(),
236                    &self.credential_store,
237                    &self.confirmation,
238                )
239                .await
240            }
241            RootCommand::Attachment(args) => {
242                commands::attachments::dispatch(
243                    args.command,
244                    target.client_factory(),
245                    &self.credential_store,
246                    &self.confirmation,
247                )
248                .await
249            }
250            RootCommand::Api(args) => {
251                commands::api::dispatch(
252                    args.command,
253                    target.client_factory(),
254                    &self.credential_store,
255                    &self.confirmation,
256                )
257                .await
258            }
259            RootCommand::User(args) => {
260                commands::users::dispatch(
261                    args.command,
262                    target.client_factory(),
263                    &self.credential_store,
264                    &self.secret_input,
265                    &self.confirmation,
266                )
267                .await
268            }
269            RootCommand::Board(args) => {
270                commands::boards::dispatch(
271                    args.command,
272                    target.client_factory(),
273                    &self.credential_store,
274                    &self.secret_input,
275                    &self.confirmation,
276                )
277                .await
278            }
279            RootCommand::List(args) => {
280                commands::lists::dispatch(
281                    args.command,
282                    target.client_factory(),
283                    &self.credential_store,
284                    &self.confirmation,
285                )
286                .await
287            }
288            RootCommand::Card(args) => {
289                commands::cards::dispatch(
290                    args.command,
291                    target.client_factory(),
292                    &self.credential_store,
293                    &self.confirmation,
294                )
295                .await
296            }
297            RootCommand::Checklist(args) => {
298                commands::checklists::dispatch(
299                    args.command,
300                    target.client_factory(),
301                    &self.credential_store,
302                    &self.confirmation,
303                )
304                .await
305            }
306            RootCommand::CustomField(args) => {
307                commands::custom_fields::dispatch(
308                    args.command,
309                    target.client_factory(),
310                    &self.credential_store,
311                    &self.confirmation,
312                )
313                .await
314            }
315            RootCommand::Comment(args) => {
316                commands::comments::dispatch(
317                    args.command,
318                    target.client_factory(),
319                    &self.credential_store,
320                    &self.confirmation,
321                )
322                .await
323            }
324            RootCommand::Swimlane(args) => {
325                commands::swimlanes::dispatch(
326                    args.command,
327                    target.client_factory(),
328                    &self.credential_store,
329                    &self.confirmation,
330                )
331                .await
332            }
333            RootCommand::Auth(args) => {
334                commands::auth::dispatch(
335                    args.command,
336                    &mut target,
337                    &self.credential_store,
338                    &self.secret_input,
339                    &self.confirmation,
340                )
341                .await
342            }
343            RootCommand::Profile(_) | RootCommand::Skill(_) | RootCommand::SelfManagement(_) => {
344                unreachable!("local commands return before target resolution")
345            }
346        };
347        result.map_err(|error| {
348            let error = error.with_profile_context(profile);
349            if policy.allows_initialization() {
350                error.with_profile_state(target.profile_created(), target.profile_active())
351            } else {
352                error
353            }
354        })
355    }
356}
357
358#[cfg(test)]
359mod tests;