ic-query 0.55.8

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: sns::report::view::proposals
//!
//! Responsibility: apply SNS proposal list view ordering.
//! Does not own: proposal fetching, cache filtering, report assembly, or text rendering.
//! Boundary: sorts proposal rows without changing cache identity.

use crate::sns::report::{
    SNS_PROPOSAL_STATUS_ADOPTED_CODE, SNS_PROPOSAL_STATUS_REJECTED_CODE, SnsProposalDecisionState,
    SnsProposalEligibilityFilter, SnsProposalRow, SnsProposalSortDirection,
    SnsProposalStatusFilter, SnsProposalTopicFilter, SnsProposalsSort,
};
use crate::{
    report_sort::{
        compare_ascii_case_insensitive_text, compare_ord, sort_by_optional_text,
        sort_by_optional_u64, sort_by_text,
    },
    text_search::optional_text_contains_case_insensitive,
};

pub(in crate::sns::report) fn proposal_matches_before(
    proposal: &SnsProposalRow,
    before_proposal_id: Option<u64>,
) -> bool {
    before_proposal_id.is_none_or(|before| proposal.proposal_id < before)
}

pub(in crate::sns::report) fn proposal_matches_status(
    proposal: &SnsProposalRow,
    status: SnsProposalStatusFilter,
) -> bool {
    match status {
        SnsProposalStatusFilter::Any => true,
        SnsProposalStatusFilter::Open => proposal.decision_state == SnsProposalDecisionState::Open,
        SnsProposalStatusFilter::Decided => {
            proposal.decision_state == SnsProposalDecisionState::Decided
        }
        SnsProposalStatusFilter::Executed => {
            proposal.decision_state == SnsProposalDecisionState::Executed
        }
        SnsProposalStatusFilter::Failed => {
            proposal.decision_state == SnsProposalDecisionState::Failed
        }
        SnsProposalStatusFilter::Rejected => {
            proposal.status == Some(SNS_PROPOSAL_STATUS_REJECTED_CODE)
        }
        SnsProposalStatusFilter::Adopted => {
            proposal.status == Some(SNS_PROPOSAL_STATUS_ADOPTED_CODE)
        }
    }
}

pub(in crate::sns::report) fn proposal_matches_topic(
    proposal: &SnsProposalRow,
    topic: SnsProposalTopicFilter,
) -> bool {
    topic
        .topic_label()
        .is_none_or(|topic_label| proposal.topic.as_deref() == Some(topic_label))
}

pub(in crate::sns::report) fn proposal_matches_eligibility(
    proposal: &SnsProposalRow,
    eligibility: SnsProposalEligibilityFilter,
) -> bool {
    eligibility
        .eligibility_value()
        .is_none_or(|value| proposal.is_eligible_for_rewards == value)
}

pub(in crate::sns::report) fn proposal_matches_proposer(
    proposal: &SnsProposalRow,
    proposer_neuron_id: Option<&str>,
) -> bool {
    proposer_neuron_id.is_none_or(|proposer| {
        proposal
            .proposer_neuron_id
            .as_deref()
            .is_some_and(|neuron_id| neuron_id.starts_with(proposer))
    })
}

pub(in crate::sns::report) fn proposal_matches_query(
    proposal: &SnsProposalRow,
    query: Option<&str>,
) -> bool {
    let Some(query) = query else {
        return true;
    };
    let action = proposal.action.label();
    optional_text_contains_case_insensitive(Some(&proposal.title), query)
        || optional_text_contains_case_insensitive(Some(&action), query)
        || optional_text_contains_case_insensitive(Some(&proposal.summary), query)
        || optional_text_contains_case_insensitive(proposal.url.as_deref(), query)
        || optional_text_contains_case_insensitive(
            proposal.payload_text_rendering.as_deref(),
            query,
        )
}

pub(in crate::sns::report) fn sort_sns_proposal_rows(
    proposals: &mut [SnsProposalRow],
    sort: SnsProposalsSort,
    direction: SnsProposalSortDirection,
) {
    let descending = matches!(direction, SnsProposalSortDirection::Desc);
    let by_id = |left: &SnsProposalRow, right: &SnsProposalRow| {
        compare_ord(left.proposal_id, right.proposal_id, descending)
    };
    match sort {
        SnsProposalsSort::Api => {}
        SnsProposalsSort::Id => proposals.sort_by(by_id),
        SnsProposalsSort::Status => sort_by_status(proposals, direction),
        SnsProposalsSort::Action => sort_by_action(proposals, direction),
        SnsProposalsSort::Eligible => sort_by_bool(proposals, direction, |proposal| {
            proposal.is_eligible_for_rewards
        }),
        SnsProposalsSort::Title => sort_by_text(
            proposals,
            descending,
            |proposal| proposal.title.as_str(),
            by_id,
        ),
        SnsProposalsSort::Topic | SnsProposalsSort::Proposer => sort_by_optional_text(
            proposals,
            descending,
            |proposal| match sort {
                SnsProposalsSort::Topic => proposal.topic.as_deref(),
                SnsProposalsSort::Proposer => proposal.proposer_neuron_id.as_deref(),
                _ => unreachable!("sort key is selected by the outer match"),
            },
            by_id,
        ),
        _ => sort_by_optional_u64(
            proposals,
            descending,
            |proposal| match sort {
                SnsProposalsSort::ActionId => Some(proposal.action_id),
                SnsProposalsSort::Yes => proposal.latest_tally.as_ref().map(|tally| tally.yes),
                SnsProposalsSort::No => proposal.latest_tally.as_ref().map(|tally| tally.no),
                SnsProposalsSort::TotalVotes => {
                    proposal.latest_tally.as_ref().map(|tally| tally.total)
                }
                SnsProposalsSort::TallyTime => proposal
                    .latest_tally
                    .as_ref()
                    .map(|tally| tally.timestamp_seconds),
                SnsProposalsSort::Ballots => Some(proposal.ballot_count as u64),
                SnsProposalsSort::RejectCost => Some(proposal.reject_cost_e8s),
                SnsProposalsSort::RewardRound => Some(proposal.reward_event_round),
                SnsProposalsSort::RewardEnd => proposal.reward_event_end_timestamp_seconds,
                SnsProposalsSort::Created => Some(proposal.proposal_creation_timestamp_seconds),
                SnsProposalsSort::Decided => proposal.decided_timestamp_seconds,
                SnsProposalsSort::Executed => proposal.executed_timestamp_seconds,
                SnsProposalsSort::Failed => proposal.failed_timestamp_seconds,
                SnsProposalsSort::Api
                | SnsProposalsSort::Id
                | SnsProposalsSort::Status
                | SnsProposalsSort::Action
                | SnsProposalsSort::Eligible
                | SnsProposalsSort::Title
                | SnsProposalsSort::Topic
                | SnsProposalsSort::Proposer => unreachable!("nonnumeric sorts are handled above"),
            },
            by_id,
        ),
    }
}

fn sort_by_action(proposals: &mut [SnsProposalRow], direction: SnsProposalSortDirection) {
    let descending = matches!(direction, SnsProposalSortDirection::Desc);
    proposals.sort_by(|left, right| {
        compare_ascii_case_insensitive_text(
            left.action.label().as_ref(),
            right.action.label().as_ref(),
            descending,
        )
        .then_with(|| compare_ord(left.proposal_id, right.proposal_id, descending))
    });
}

fn sort_by_bool(
    proposals: &mut [SnsProposalRow],
    direction: SnsProposalSortDirection,
    key: impl Fn(&SnsProposalRow) -> bool,
) {
    let descending = matches!(direction, SnsProposalSortDirection::Desc);
    proposals.sort_by(|left, right| {
        compare_ord(key(left), key(right), descending)
            .then_with(|| compare_ord(left.proposal_id, right.proposal_id, descending))
    });
}

fn sort_by_status(proposals: &mut [SnsProposalRow], direction: SnsProposalSortDirection) {
    let descending = matches!(direction, SnsProposalSortDirection::Desc);
    proposals.sort_by(|left, right| {
        compare_ord(left.decision_state, right.decision_state, descending)
            .then_with(|| compare_ord(left.proposal_id, right.proposal_id, descending))
    });
}