wekan-cli 0.1.0

Agent-first, human-friendly command-line client for Wekan
Documentation
use crate::{
    command_result::CommandSuccess,
    commands::{
        self, PreparedCommand, RootCommand,
        auth::{AuthCommand, PreparedAuthCommand},
    },
    config::{ServerSelection, TargetResolver, profiles::FileProfileStore},
    credentials::{
        CredentialStore, KeyringCredentialStore, SecretInputProvider, SystemSecretInputProvider,
    },
    error::AppError,
    input::{ConfirmationProvider, SystemConfirmationProvider},
};

pub struct App<S, P, F = FileProfileStore, C = SystemConfirmationProvider> {
    target_resolver: TargetResolver,
    credential_store: S,
    secret_input: P,
    profile_store: F,
    confirmation: C,
}

impl
    App<
        KeyringCredentialStore,
        SystemSecretInputProvider,
        FileProfileStore,
        SystemConfirmationProvider,
    >
{
    pub fn production(selection: ServerSelection) -> Self {
        Self::production_with_confirmation_interactivity(selection, true)
    }

    pub fn production_with_confirmation_interactivity(
        selection: ServerSelection,
        allow_interactive_confirmation: bool,
    ) -> Self {
        Self::with_dependencies(
            selection,
            KeyringCredentialStore,
            SystemSecretInputProvider,
            FileProfileStore::production(),
            SystemConfirmationProvider::for_output(allow_interactive_confirmation),
        )
    }
}

impl<S, P, F> App<S, P, F, SystemConfirmationProvider>
where
    S: CredentialStore,
    P: SecretInputProvider,
    F: crate::config::profiles::ProfileStore,
{
    pub const fn with_profile_store(
        selection: ServerSelection,
        credential_store: S,
        secret_input: P,
        profile_store: F,
    ) -> Self {
        Self::with_dependencies(
            selection,
            credential_store,
            secret_input,
            profile_store,
            SystemConfirmationProvider::interactive(),
        )
    }
}

impl<S, P, F, C> App<S, P, F, C>
where
    S: CredentialStore,
    P: SecretInputProvider,
    F: crate::config::profiles::ProfileStore,
    C: ConfirmationProvider,
{
    pub const fn with_dependencies(
        selection: ServerSelection,
        credential_store: S,
        secret_input: P,
        profile_store: F,
        confirmation: C,
    ) -> Self {
        Self {
            target_resolver: TargetResolver::new(selection),
            credential_store,
            secret_input,
            profile_store,
            confirmation,
        }
    }

    pub fn with_confirmation_provider<C2>(self, confirmation: C2) -> App<S, P, F, C2>
    where
        C2: ConfirmationProvider,
    {
        App {
            target_resolver: self.target_resolver,
            credential_store: self.credential_store,
            secret_input: self.secret_input,
            profile_store: self.profile_store,
            confirmation,
        }
    }

    pub const fn credential_store(&self) -> &S {
        &self.credential_store
    }

    pub async fn execute(&self, command: RootCommand) -> Result<CommandSuccess, AppError> {
        match command {
            RootCommand::Api(args) => self.execute_api(args).await,
            RootCommand::Auth(args) => self.execute_auth(args).await,
            RootCommand::User(args) => self.execute_user(args).await,
            RootCommand::Board(args) => self.execute_board(args).await,
            RootCommand::List(args) => self.execute_list(args).await,
            RootCommand::Card(args) => self.execute_card(args).await,
            RootCommand::Comment(args) => self.execute_comment(args).await,
            RootCommand::Swimlane(args) => self.execute_swimlane(args).await,
            RootCommand::Profile(args) => {
                if self.target_resolver.has_explicit_target() {
                    return Err(AppError::invalid_input(
                        "--server and --profile cannot be used with profile-management commands",
                    ));
                }
                commands::dispatch(
                    PreparedCommand::Profile { args },
                    &self.credential_store,
                    &self.secret_input,
                    &self.profile_store,
                    &self.confirmation,
                )
                .await
            }
        }
    }

    async fn execute_api(
        &self,
        args: crate::commands::api::ApiArgs,
    ) -> Result<CommandSuccess, AppError> {
        let target = self.target_resolver.resolve(
            &self.profile_store,
            crate::config::MissingProfileResolution::Reject,
        )?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::Api {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }

    async fn execute_auth(
        &self,
        args: crate::commands::auth::AuthArgs,
    ) -> Result<CommandSuccess, AppError> {
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let allow_initialization = missing_profile_resolution.allows_initialization();
        let mut target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        let prepared = match args.command {
            AuthCommand::Login(args) => Ok(PreparedAuthCommand::Login {
                args,
                target: &mut target,
            }),
            AuthCommand::Logout(args) => Ok(PreparedAuthCommand::Logout {
                args,
                client_factory: target.client_factory(),
            }),
            AuthCommand::Register(args) => Ok(PreparedAuthCommand::Register {
                args,
                target: &mut target,
            }),
            AuthCommand::Status(args) => Ok(PreparedAuthCommand::Status {
                args,
                client_factory: target.client_factory(),
            }),
        };
        let result = match prepared {
            Ok(command) => {
                commands::dispatch(
                    PreparedCommand::Auth { command },
                    &self.credential_store,
                    &self.secret_input,
                    &self.profile_store,
                    &self.confirmation,
                )
                .await
            }
            Err(error) => Err(error),
        };
        let profile_created = target.profile_created();
        let profile_active = target.profile_active();
        result.map_err(|error| {
            let error = error.with_profile_context(profile);
            if allow_initialization {
                error.with_profile_state(profile_created, profile_active)
            } else {
                error
            }
        })
    }

    async fn execute_user(
        &self,
        args: crate::commands::users::UserArgs,
    ) -> Result<CommandSuccess, AppError> {
        args.command.validate()?;
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::User {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }

    async fn execute_board(
        &self,
        args: crate::commands::boards::BoardArgs,
    ) -> Result<CommandSuccess, AppError> {
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::Board {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }

    async fn execute_list(
        &self,
        args: crate::commands::lists::ListArgs,
    ) -> Result<CommandSuccess, AppError> {
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::List {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }

    async fn execute_card(
        &self,
        args: Box<crate::commands::cards::CardArgs>,
    ) -> Result<CommandSuccess, AppError> {
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::Card {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }

    async fn execute_swimlane(
        &self,
        args: crate::commands::swimlanes::SwimlaneArgs,
    ) -> Result<CommandSuccess, AppError> {
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::Swimlane {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }

    async fn execute_comment(
        &self,
        args: crate::commands::comments::CommentArgs,
    ) -> Result<CommandSuccess, AppError> {
        let missing_profile_resolution = args.command.missing_profile_resolution();
        let target = self
            .target_resolver
            .resolve(&self.profile_store, missing_profile_resolution)?;
        let profile = target.profile().to_owned();
        commands::dispatch(
            PreparedCommand::Comment {
                args,
                client_factory: target.client_factory(),
            },
            &self.credential_store,
            &self.secret_input,
            &self.profile_store,
            &self.confirmation,
        )
        .await
        .map_err(|error| error.with_profile_context(profile))
    }
}

#[cfg(test)]
mod tests;