jfsm 0.1.5

A command-line tool to read file system metadata then return it in JSON format (output and errors).
Documentation
use serde::{Deserialize, Serialize};
use serde_json;
use clap::Parser;

// Import our modules
use jfsm::{Cli, JsonError, Outputer, JsonOutputer, raw::RawMetadata, time::TimeMetadata, perms::PermsMetadata, fpdir::FpDirMetadata, request::Request, ErrorMessage};

#[derive(Serialize, Deserialize, Debug)]
struct ResponseStruct {
    #[serde(skip_serializing_if = "Option::is_none")]
    path: Option<FpDirMetadata>,
    raw: RawMetadata,
    #[serde(skip_serializing_if = "Option::is_none")]
    perms: Option<PermsMetadata>,
    #[serde(skip_serializing_if = "Option::is_none")]
    time: Option<TimeMetadata>,
    #[serde(skip_serializing_if = "Option::is_none")]
    ts: Option<u128>,
}

#[derive(Serialize, Deserialize, Debug)]
struct OutputJsonMessage {
    request: Request,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    data: Option<OutputData>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum OutputData {
    Success { response: ResponseStruct },
    Error { error: ErrorMessage },
}

fn main() {
    let cli = Cli::parse();
    let request = cli.build_request();
    
    match compute_output_message(&cli, &request) {
        Ok(message) => {
            let json_outputer = JsonOutputer;
            let json_string = serde_json::to_string_pretty(&message).unwrap_or_else(|_| {
                serde_json::to_string(&message).unwrap_or_default()
            });
            let output = json_outputer.output(&json_string);
            println!("{}", output);
        }
        Err(e) => {
            // Determine error code based on message
            let error_code = if e.message == "PATH-NOT-EXIST" {
                6
            } else if e.message == "PERMISSION-ERROR" {
                77
            } else if e.message == "PARSING-ERROR" {
                65
            } else {
                1
            };
            
            // Create an error message
            let error_message = ErrorMessage {
                code: error_code,
                message: e.message.clone(),
            };
            
            // Create output with error only (no response data)
            let output = OutputJsonMessage {
                request: request.clone(),
                data: Some(OutputData::Error { error: error_message }),
            };
            
            let json_outputer = JsonOutputer;
            let json_string = serde_json::to_string_pretty(&output).unwrap_or_else(|_| {
                serde_json::to_string(&output).unwrap_or_default()
            });
            let output_str = json_outputer.output(&json_string);
            println!("{}", output_str);
            std::process::exit(error_code);
        }
    }
}

fn compute_output_message(cli: &Cli, request: &Request) -> Result<OutputJsonMessage, JsonError> {
    // Get raw metadata
    let raw_metadata = RawMetadata::from_path(&cli.filepath)?;
    
    let mut response = ResponseStruct {
        path: None,
        raw: raw_metadata,
        perms: None,
        time: None,
        ts: None,
    };
    
    // Add detailed information based on flags
    if cli.all || cli.path {
        let fpdir_metadata = FpDirMetadata::from_raw(&response.raw);
        response.path = Some(fpdir_metadata);
    }
    
    if cli.all || cli.perms {
        let perms_metadata = PermsMetadata::from_raw(&response.raw);
        response.perms = Some(perms_metadata);
    }
    
    if cli.all || cli.time {
        let time_metadata = TimeMetadata::from_raw(&response.raw);
        response.time = Some(time_metadata);
    }
    
    // Set the timestamp just before serialization
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("Time went backwards")
        .as_nanos();
    response.ts = Some(timestamp);
    
    let output = OutputJsonMessage {
        request: request.clone(),
        data: Some(OutputData::Success { response }),
    };
    
    Ok(output)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::NamedTempFile;
    use std::io::Write;

    #[test]
    fn test_output_message() {
        // Create a temporary file
        let mut file = NamedTempFile::new().expect("Failed to create temp file");
        writeln!(file, "test content").expect("Failed to write to temp file");
        
        let filepath = file.path().to_str().expect("Failed to convert path to string");
        let cli = Cli {
            filepath: filepath.to_string(),
            all: false,
            path: false,
            perms: false,
            time: false,
            id: None,
        };
        
        // Build request
        let request = cli.build_request();
        
        // Compute our output
        let message = compute_output_message(&cli, &request).expect("Failed to compute output message");
        let json_outputer = JsonOutputer;
        let json_string = serde_json::to_string_pretty(&message).unwrap_or_else(|_| {
            serde_json::to_string(&message).unwrap_or_default()
        });
        let output = json_outputer.output(&json_string);
        
        // Parse the JSON to check if it has the expected structure
        let parsed: serde_json::Value = serde_json::from_str(&output).expect("Failed to parse JSON");
        
        // Verify the structure has request and response fields
        assert!(parsed.get("request").is_some());
        assert!(parsed.get("response").is_some());
        
        // Extract the response object
        let response_obj = parsed.get("response").expect("Response field missing");
        let raw_obj = response_obj.get("raw").expect("Raw field missing");
        let pathname = raw_obj.get("pathname").expect("Pathname field missing");
        assert!(!pathname.as_str().unwrap().is_empty());
        
        // Check that ts field is present
        let ts = response_obj.get("ts").expect("Ts field missing");
        assert!(ts.is_number());
    }
}