canic-cli 0.35.11

Operator CLI for Canic fleet backup and restore workflows
Documentation
use crate::cycles::model::{
    CycleTopupEventPage, CycleTopupEventSample, CycleTopupStatus, CycleTrackerPage,
    CycleTrackerSample,
};
use canic_host::response_parse::{
    field_value_after_equals, find_field, parse_json_u64, parse_json_u128, parse_u64_digits,
    parse_u128_digits,
};

pub(super) fn parse_cycle_tracker_page(output: &str) -> Option<CycleTrackerPage> {
    let value = serde_json::from_str::<serde_json::Value>(output).ok()?;
    let entries_value = find_field(&value, "entries")?;
    let entries = entries_value
        .as_array()?
        .iter()
        .filter_map(parse_cycle_tracker_sample_json)
        .collect::<Vec<_>>();
    let total = find_field(&value, "total")
        .and_then(parse_json_u64)
        .unwrap_or(entries.len() as u64);

    Some(CycleTrackerPage { entries, total })
}

fn parse_cycle_tracker_sample_json(value: &serde_json::Value) -> Option<CycleTrackerSample> {
    Some(CycleTrackerSample {
        timestamp_secs: find_field(value, "timestamp_secs").and_then(parse_json_u64)?,
        cycles: find_field(value, "cycles").and_then(parse_json_u128)?,
    })
}

pub(super) fn parse_topup_event_page(output: &str) -> Option<CycleTopupEventPage> {
    let value = serde_json::from_str::<serde_json::Value>(output).ok()?;
    let entries = find_field(&value, "entries")?
        .as_array()?
        .iter()
        .filter_map(parse_topup_event_json)
        .collect::<Vec<_>>();
    let total = find_field(&value, "total")
        .and_then(parse_json_u64)
        .unwrap_or(entries.len() as u64);

    Some(CycleTopupEventPage { entries, total })
}

fn parse_topup_event_json(value: &serde_json::Value) -> Option<CycleTopupEventSample> {
    Some(CycleTopupEventSample {
        timestamp_secs: find_field(value, "timestamp_secs").and_then(parse_json_u64)?,
        transferred_cycles: find_field(value, "transferred_cycles").and_then(parse_optional_u128),
        status: find_field(value, "status").and_then(parse_topup_status_json)?,
    })
}

fn parse_optional_u128(value: &serde_json::Value) -> Option<u128> {
    match value {
        serde_json::Value::Null => None,
        serde_json::Value::Object(map) => map.values().find_map(parse_json_u128),
        serde_json::Value::Array(values) => values.iter().find_map(parse_json_u128),
        _ => parse_json_u128(value),
    }
}

fn parse_topup_status_json(value: &serde_json::Value) -> Option<CycleTopupStatus> {
    match value {
        serde_json::Value::String(status) => parse_topup_status(status),
        serde_json::Value::Object(map) => map.keys().find_map(|key| parse_topup_status(key)),
        serde_json::Value::Array(values) => values.iter().find_map(parse_topup_status_json),
        _ => None,
    }
}

pub(super) fn parse_cycle_tracker_page_text(output: &str) -> Option<CycleTrackerPage> {
    let mut entries = Vec::new();
    for chunk in output.split("record") {
        if !(chunk.contains("timestamp_secs") && chunk.contains("cycles")) {
            continue;
        }
        let timestamp_secs =
            field_number_after(chunk, "timestamp_secs").and_then(parse_u64_digits)?;
        let cycles = field_number_after(chunk, "cycles").and_then(parse_u128_digits)?;
        entries.push(CycleTrackerSample {
            timestamp_secs,
            cycles,
        });
    }
    let total = field_number_after(output, "total")
        .and_then(parse_u64_digits)
        .unwrap_or(entries.len() as u64);
    Some(CycleTrackerPage { entries, total })
}

pub(super) fn parse_topup_event_page_text(output: &str) -> Option<CycleTopupEventPage> {
    let mut entries = Vec::new();
    for chunk in output.split("record") {
        if !(chunk.contains("timestamp_secs") && chunk.contains("status")) {
            continue;
        }
        let timestamp_secs =
            field_number_after(chunk, "timestamp_secs").and_then(parse_u64_digits)?;
        let transferred_cycles =
            field_number_after(chunk, "transferred_cycles").and_then(parse_u128_digits);
        let status = parse_topup_status(chunk)?;
        entries.push(CycleTopupEventSample {
            timestamp_secs,
            transferred_cycles,
            status,
        });
    }
    let total = field_number_after(output, "total")
        .and_then(parse_u64_digits)
        .unwrap_or(entries.len() as u64);
    Some(CycleTopupEventPage { entries, total })
}

fn parse_topup_status(text: &str) -> Option<CycleTopupStatus> {
    if text.contains("RequestOk") || text.contains("request_ok") {
        Some(CycleTopupStatus::RequestOk)
    } else if text.contains("RequestErr") || text.contains("request_err") {
        Some(CycleTopupStatus::RequestErr)
    } else if text.contains("RequestScheduled") || text.contains("request_scheduled") {
        Some(CycleTopupStatus::RequestScheduled)
    } else {
        None
    }
}

fn field_number_after<'a>(text: &'a str, field: &str) -> Option<&'a str> {
    field_value_after_equals(text, field)
}