piecework_cli 0.2.0

Client to interact with a piecework application running on holochain
Documentation
use colored::*;
use holochain_client::AgentPubKey;
use holochain_types::dna::ActionHash;
use prettytable::{Row, Table};
use serde::Serialize;
use serde_json::Value;
use std::fmt::Debug;

/// Creates a table from a collection of serializable items
pub fn format_table<T: Serialize + Debug>(items: &[T], title: &str) -> Table {
    let mut table = Table::new();

    // Print title
    println!("\n{}", title.bold().underline());

    // Get headers from the first item or return empty table if none
    let headers = if !items.is_empty() {
        if let Ok(value) = serde_json::to_value(&items[0]) {
            if let Value::Object(map) = value {
                let keys: Vec<String> = map.keys().cloned().collect();
                if !keys.is_empty() {
                    keys
                } else {
                    // For empty objects, use type name instead
                    vec![std::any::type_name::<T>().to_string()]
                }
            } else {
                vec!["Value".to_string()]
            }
        } else {
            vec!["Item".to_string()]
        }
    } else {
        // For empty collections, try to provide descriptive header from type name
        vec![format!("{} (empty)", std::any::type_name::<T>())]
    };

    // Add headers row
    table.add_row(Row::from_iter(headers.iter().map(|h| h.bold())));

    // Add item rows
    for item in items {
        if let Ok(value) = serde_json::to_value(item) {
            match value {
                Value::Object(map) => {
                    let values = headers
                        .iter()
                        .map(|key| map.get(key).map(|v| format_value(v)).unwrap_or_default())
                        .collect::<Vec<String>>();
                    table.add_row(Row::from_iter(values));
                }
                _ => {
                    // For non-object values, just show the raw value
                    table.add_row(Row::from_iter(vec![value.to_string()]));
                }
            }
        } else {
            // Fallback for non-serializable types
            table.add_row(Row::from_iter(vec![format!("{:?}", item)]));
        }
    }

    table
}
fn format_value(value: &Value) -> String {
    // check if it starts with 'uhC'
    if value.to_string().starts_with("\"uhCA") {
        // return only last 5 characters in the same order
        format!(
            "Agent(...{})",
            value
                .to_string()
                .chars()
                .skip(49)
                .take(5)
                .collect::<String>()
        )
    } else if value.to_string().starts_with("\"uhCk") {
        // return only last 5 characters in the same order
        format!(
            "Action(...{})",
            value
                .to_string()
                .chars()
                .skip(49)
                .take(5)
                .collect::<String>()
        )
    } else {
        match value {
            Value::Array(arr) => {
                // check if vec contains numbers if it does return Number(n)
                if arr.iter().any(|v| v.is_number()) {
                    // get the vec of numbers as numbers
                    let numbers = arr.iter().filter_map(|v| v.as_u64()).collect::<Vec<u64>>();
                    // Convert u64 to u8 for AgentPubKey::from_raw_39
                    let bytes = numbers.iter().map(|&n| n as u8).collect::<Vec<u8>>();
                    if bytes.len() == 39 {
                        if let Ok(agent_pub_key) = AgentPubKey::from_raw_39(bytes.clone()) {
                            format!(
                                "Agent(...{})",
                                agent_pub_key
                                    .to_string()
                                    .chars()
                                    .skip(48)
                                    .take(5)
                                    .collect::<String>()
                            )
                        } else if let Ok(action_hash) = ActionHash::from_raw_39(bytes) {
                            format!(
                                "Action(...{})",
                                action_hash
                                    .to_string()
                                    .chars()
                                    .skip(48)
                                    .take(5)
                                    .collect::<String>()
                            )
                        } else {
                            format!(
                                "Number({})",
                                value
                                    .to_string()
                                    .chars()
                                    .skip(value.to_string().len() - 5)
                                    .collect::<String>()
                            )
                        }
                    } else {
                        // lets display the last 5 characters with Number(n)
                        format!(
                            "Number({})",
                            value
                                .to_string()
                                .chars()
                                .skip(value.to_string().len() - 5)
                                .collect::<String>()
                        )
                    }
                } else {
                    value.to_string()
                }
            }
            _ => value.to_string(),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::*;
    use holochain_client::AgentPubKey;
    use holochain_types::dna::{ActionHash, ActionHashB64, AgentPubKeyB64};
    use serde::Serialize;

    #[derive(Debug, Serialize)]
    struct TestTransaction {
        id: String,
        amount: f64,
        status: String,
        agent_hash: AgentPubKey,
        agent_hash_b64: AgentPubKeyB64,
        action_hash: ActionHash,
        action_hash_b64: ActionHashB64,
        code: Vec<String>,
        sub_transactions: Vec<TestSubTransaction>,
    }
    #[derive(Debug, Serialize)]
    struct TestSubTransaction {
        id: String,
        amount: f64,
        status: String,
        agent_hash: AgentPubKey,
        agent_hash_b64: AgentPubKeyB64,
    }
    #[derive(Debug, Serialize)]
    struct EmptyStruct {}

    #[test]
    fn test_format_table() {
        let agent_hash_b64 =
            AgentPubKeyB64::from_str("uhCAkBi4aREMzPHnnNgh0N5hZj4Rs7P-9RdIAuv7bwKyVZq7CS9iq")
                .unwrap();
        let action_hash_b64 =
            ActionHashB64::from_str("uhCkkDagDYBufaxPe5THhmg3qPyjMrdjHTtZSigWFxmcR-dBjyMKL")
                .unwrap();
        let transactions = vec![
            TestTransaction {
                id: "123".into(),
                amount: 100.0,
                status: "pending".into(),
                agent_hash: agent_hash_b64.clone().into(),
                agent_hash_b64: agent_hash_b64.clone(),
                action_hash: action_hash_b64.clone().into(),
                action_hash_b64: action_hash_b64.clone(),
                code: vec![],
                sub_transactions: vec![TestSubTransaction {
                    id: "123".into(),
                    amount: 100.0,
                    status: "pending".into(),
                    agent_hash: agent_hash_b64.clone().into(),
                    agent_hash_b64: agent_hash_b64.clone(),
                }],
            },
            TestTransaction {
                id: "456".into(),
                amount: 200.0,
                status: "completed".into(),
                agent_hash: agent_hash_b64.clone().into(),
                agent_hash_b64: agent_hash_b64.clone(),
                action_hash: action_hash_b64.clone().into(),
                action_hash_b64: action_hash_b64.clone(),
                code: vec![],
                sub_transactions: vec![
                    TestSubTransaction {
                        id: "456".into(),
                        amount: 200.0,
                        status: "completed".into(),
                        agent_hash: agent_hash_b64.clone().into(),
                        agent_hash_b64: agent_hash_b64.clone(),
                    },
                    TestSubTransaction {
                        id: "789".into(),
                        amount: 300.0,
                        status: "completed".into(),
                        agent_hash: agent_hash_b64.clone().into(),
                        agent_hash_b64: agent_hash_b64.clone(),
                    },
                ],
            },
        ];
        let table = format_table(&transactions, "Test Transactions");
        table.printstd();
        assert!(table.to_string().contains("123"));
        assert!(table.to_string().contains("456"));
        assert!(table.to_string().contains("100.0"));
        assert!(table.to_string().contains("200.0"));
    }

    #[test]
    fn test_empty_vec() {
        let empty_vec: Vec<TestTransaction> = vec![];
        let table = format_table(&empty_vec, "Empty Table");
        // Table should have a header with type info
        assert!(table.to_string().contains("empty"));
        assert!(table.to_string().contains("TestTransaction"));
    }

    #[test]
    fn test_empty_struct() {
        let empty_struct_vec = vec![EmptyStruct {}, EmptyStruct {}];
        let table = format_table(&empty_struct_vec, "Empty Structs");
        // Table should have a representable form
        let table_str = table.to_string();
        assert!(table_str.contains("EmptyStruct")); // Check header exists
        assert!(table_str.split('\n').count() > 2); // At least two lines (header + data)
    }
}