wekan-cli 2.1.0

Agent-first, human-friendly command-line client for Wekan
Documentation
use clap::Args;

use crate::{
    client::{
        WekanClientFactory,
        cards::{
            CardCustomField, CardCustomFieldValue, CardDependency, CardDocument, CardLocation,
            CardPoker, CardSticker, CardStickerHighlight, CardVote,
        },
    },
    command_result::{
        CardDetail, CardDetailCustomField, CardDetailCustomFieldValue, CardDetailDependency,
        CardDetailLocation, CardDetailPoker, CardDetailSticker, CardDetailStickerHighlight,
        CardDetailVote, CommandSuccess,
    },
    commands::{
        authenticated::AuthenticatedContext, client_error::map_client_error_with_not_found,
    },
    credentials::CredentialStore,
    error::AppError,
    redaction::Redactor,
};

use crate::commands::transfer::identifier;

#[derive(Debug, Args)]
#[command(
    after_help = "Examples:\n  wekan card get card-id\n  wekan card get card-id --board board-id --list list-id\n\nID-only lookup includes archived cards; scoped lookup returns active cards."
)]
pub struct GetArgs {
    /// Wekan board ID.
    #[arg(long = "board", requires = "list_id", value_parser = identifier, help_heading = "Target")]
    pub board_id: Option<String>,

    /// Wekan list ID.
    #[arg(long = "list", requires = "board_id", value_parser = identifier, help_heading = "Target")]
    pub list_id: Option<String>,

    /// Wekan card ID.
    #[arg(value_parser = identifier, help_heading = "Target")]
    pub card_id: String,
}

pub(super) async fn execute(
    args: GetArgs,
    client_factory: &WekanClientFactory,
    credential_store: &dyn CredentialStore,
) -> Result<CommandSuccess, AppError> {
    let context = AuthenticatedContext::load(client_factory, credential_store)?;
    let redactor = Redactor::with_secret(context.record().token());
    let card = match (&args.board_id, &args.list_id) {
        (Some(board), Some(list)) => {
            context
                .client()
                .card(board, list, &args.card_id, context.record().token())
                .await
        }
        (None, None) => {
            context
                .client()
                .card_by_id(&args.card_id, context.record().token())
                .await
        }
        _ => unreachable!("parent selectors are paired by clap"),
    }
    .map_err(|error| {
        map_client_error_with_not_found(
            error,
            &redactor,
            "card lookup",
            "the requested Wekan card was not found",
        )
    })?;
    Ok(CommandSuccess::CardShown(Box::new(card_detail(card))))
}

pub(super) fn card_detail(card: CardDocument) -> CardDetail {
    CardDetail {
        scrum: card.scrum,
        drawings: card.drawings,
        custom_field_links: card.custom_field_links,
        sync_last_source: card.sync_last_source,
        email: card.email,
        scrum_revision: card.scrum_revision,
        parent_ids: card.parent_ids,
        map_x: card.map_x,
        map_y: card.map_y,
        column_set_values: card.column_set_values,
        card_kind: card.card_kind,
        last_recurrence_at: card.last_recurrence_at,
        sync_source_key: card.sync_source_key,
        sync_label_ids: card.sync_label_ids,
        sync_issue_state: card.sync_issue_state,
        sync_closed_by_we_kan: card.sync_closed_by_we_kan,
        sync_external_id: card.sync_external_id,
        sync_source_type: card.sync_source_type,
        attached_card_ids: card.attached_card_ids,
        flow_start_at: card.flow_start_at,
        flow_user_id: card.flow_user_id,
        pomodoro_start_at: card.pomodoro_start_at,
        pomodoro_phase: card.pomodoro_phase,
        pomodoro_user_id: card.pomodoro_user_id,
        show_list_on_card: card.show_list_on_card,
        list_entered_at: card.list_entered_at,
        recurrence_interval: card.recurrence_interval,
        last_move_reason: card.last_move_reason,
        flow_interruptions: card.flow_interruptions,
        pomodoro_count: card.pomodoro_count,
        pomodoro_work_minutes: card.pomodoro_work_minutes,
        checklists: card.checklists,
        card_id: card.card_id,
        title: card.title,
        watchers: card.watchers,
        archived: card.archived,
        archived_at: card.archived_at,
        deleted_at: card.deleted_at,
        deleted_by: card.deleted_by,
        delete_batch_id: card.delete_batch_id,
        parent_id: card.parent_id,
        list_id: card.list_id,
        swimlane_id: card.swimlane_id,
        board_id: card.board_id,
        cover_id: card.cover_id,
        color: card.color,
        created_at: card.created_at,
        modified_at: card.modified_at,
        custom_fields: card
            .custom_fields
            .into_iter()
            .map(card_detail_custom_field)
            .collect(),
        date_last_activity: card.date_last_activity,
        description: card.description,
        requested_by: card.requested_by,
        assigned_by: card.assigned_by,
        label_ids: card.label_ids,
        members: card.members,
        assignees: card.assignees,
        requesters: card.requesters,
        assigners: card.assigners,
        received_at: card.received_at,
        start_at: card.start_at,
        due_at: card.due_at,
        end_at: card.end_at,
        due_complete: card.due_complete,
        stickers: card.stickers.into_iter().map(card_detail_sticker).collect(),
        location_name: card.location_name,
        location_address: card.location_address,
        location_latitude: card.location_latitude,
        location_longitude: card.location_longitude,
        locations: card
            .locations
            .into_iter()
            .map(card_detail_location)
            .collect(),
        spent_time: card.spent_time,
        is_overtime: card.is_overtime,
        user_id: card.user_id,
        sort: card.sort,
        subtask_sort: card.subtask_sort,
        card_type: card.card_type,
        linked_id: card.linked_id,
        card_dependencies: card
            .card_dependencies
            .into_iter()
            .map(card_detail_dependency)
            .collect(),
        vote: card.vote.map(card_detail_vote),
        poker: card.poker.map(card_detail_poker),
        target_id_gantt: card.target_id_gantt,
        link_type_gantt: card.link_type_gantt,
        link_id_gantt: card.link_id_gantt,
        card_number: card.card_number,
        show_activities: card.show_activities,
        show_list_on_minicard: card.show_list_on_minicard,
        show_checklist_at_minicard: card.show_checklist_at_minicard,
        hide_finished_checklist_if_items_are_hidden: card
            .hide_finished_checklist_if_items_are_hidden,
    }
}

pub(super) fn card_detail_custom_field(field: CardCustomField) -> CardDetailCustomField {
    CardDetailCustomField {
        custom_field_id: field.custom_field_id,
        value: field.value.map(card_detail_custom_field_value),
    }
}

fn card_detail_custom_field_value(value: CardCustomFieldValue) -> CardDetailCustomFieldValue {
    match value {
        CardCustomFieldValue::String(value) => CardDetailCustomFieldValue::String(value),
        CardCustomFieldValue::Number(value) => CardDetailCustomFieldValue::Number(value),
        CardCustomFieldValue::Boolean(value) => CardDetailCustomFieldValue::Boolean(value),
        CardCustomFieldValue::Strings(value) => CardDetailCustomFieldValue::Strings(value),
    }
}

fn card_detail_sticker(sticker: CardSticker) -> CardDetailSticker {
    CardDetailSticker {
        icon: sticker.icon,
        name: sticker.name,
        highlight: sticker.highlight.map(card_detail_sticker_highlight),
        position: sticker.position,
    }
}

const fn card_detail_sticker_highlight(
    highlight: CardStickerHighlight,
) -> CardDetailStickerHighlight {
    match highlight {
        CardStickerHighlight::Underline => CardDetailStickerHighlight::Underline,
        CardStickerHighlight::Round => CardDetailStickerHighlight::Round,
    }
}

fn card_detail_location(location: CardLocation) -> CardDetailLocation {
    CardDetailLocation {
        location_id: location.location_id,
        name: location.name,
        address: location.address,
        latitude: location.latitude,
        longitude: location.longitude,
    }
}

fn card_detail_dependency(dependency: CardDependency) -> CardDetailDependency {
    CardDetailDependency {
        card_id: dependency.card_id,
        dependency_type: dependency.dependency_type,
        color: dependency.color,
        icon: dependency.icon,
    }
}

fn card_detail_vote(vote: CardVote) -> CardDetailVote {
    CardDetailVote {
        question: vote.question,
        positive: vote.positive,
        negative: vote.negative,
        end: vote.end,
        public: vote.public,
        allow_non_board_members: vote.allow_non_board_members,
    }
}

fn card_detail_poker(poker: CardPoker) -> CardDetailPoker {
    CardDetailPoker {
        question: poker.question,
        one: poker.one,
        two: poker.two,
        three: poker.three,
        five: poker.five,
        eight: poker.eight,
        thirteen: poker.thirteen,
        twenty: poker.twenty,
        forty: poker.forty,
        one_hundred: poker.one_hundred,
        unsure: poker.unsure,
        end: poker.end,
        allow_non_board_members: poker.allow_non_board_members,
        estimation: poker.estimation,
    }
}