Skip to main content

rain_metadata/cli/
generate.rs

1use std::path::PathBuf;
2use std::io::{self, Read};
3use std::fs;
4use clap::{Args, Subcommand};
5
6use crate::error::Error;
7use crate::metaboard::{DotrainSourceEmitData, generate_dotrain_source_emit_tx_data};
8
9/// Generate tx data to emit metadata
10#[derive(Args)]
11pub struct Generate {
12    #[command(subcommand)]
13    pub command: GenerateCommand,
14}
15
16/// Generate subcommands
17#[derive(Subcommand)]
18pub enum GenerateCommand {
19    /// Generate deployment data for dotrain source code
20    Source(SourceArgs),
21}
22
23/// Arguments for generating source deployment data
24#[derive(Args)]
25pub struct SourceArgs {
26    /// Path to input .rain file. If not provided, reads from stdin
27    #[arg(short = 'i', long = "input-path")]
28    input_path: Option<PathBuf>,
29
30    /// Path to output JSON file. If not provided, prints to stdout  
31    #[arg(short = 'o', long = "output-path")]
32    output_path: Option<PathBuf>,
33}
34
35/// Read content from input source (file or stdin)
36fn read_input_content(input_path: Option<PathBuf>) -> Result<String, Error> {
37    match input_path {
38        Some(path) => {
39            // Read from file
40            fs::read_to_string(&path).map_err(|e| {
41                Error::InvalidInput(format!("Failed to read file '{}': {}", path.display(), e))
42            })
43        }
44        None => {
45            // Read from stdin
46            let mut buffer = String::new();
47            io::stdin()
48                .read_to_string(&mut buffer)
49                .map_err(|e| Error::InvalidInput(format!("Failed to read from stdin: {}", e)))?;
50            Ok(buffer)
51        }
52    }
53}
54
55/// Write output to destination (file or stdout)
56fn write_output(data: &DotrainSourceEmitData, output_path: Option<PathBuf>) -> Result<(), Error> {
57    // Serialize to pretty JSON
58    let json_output = serde_json::to_string_pretty(data).map_err(Error::SerdeJsonError)?;
59
60    match output_path {
61        Some(path) => {
62            // Ensure output directory exists before writing
63            if let Some(parent) = path.parent() {
64                if !parent.as_os_str().is_empty() {
65                    if let Err(e) = fs::create_dir_all(parent) {
66                        return Err(Error::InvalidInput(format!(
67                            "Failed to create output directory '{}': {}",
68                            parent.display(),
69                            e
70                        )));
71                    }
72                }
73            }
74            // Write to file
75            fs::write(&path, json_output).map_err(|e| {
76                Error::InvalidInput(format!("Failed to write file '{}': {}", path.display(), e))
77            })
78        }
79        None => {
80            // Write to stdout
81            println!("{}", json_output);
82            Ok(())
83        }
84    }
85}
86
87/// Execute the generate command
88pub fn generate(args: Generate) -> anyhow::Result<()> {
89    match args.command {
90        GenerateCommand::Source(source_args) => generate_source(source_args),
91    }
92}
93
94/// Execute the generate source command
95fn generate_source(args: SourceArgs) -> anyhow::Result<()> {
96    // Read input content
97    let content = read_input_content(args.input_path)?;
98
99    let tx_data = generate_dotrain_source_emit_tx_data(&content)?;
100
101    // Write output
102    write_output(&tx_data, args.output_path)?;
103
104    Ok(())
105}
106
107#[cfg(all(test, not(target_family = "wasm")))]
108mod tests {
109    use super::*;
110    use std::io::Write;
111    use tempfile::NamedTempFile;
112
113    #[test]
114    fn test_read_input_content_from_file() {
115        // Create a temporary file with test content
116        let mut temp_file = NamedTempFile::new().unwrap();
117        let test_content = "#main _ _: int-add(1 2)";
118        writeln!(temp_file, "{}", test_content).unwrap();
119
120        let content = read_input_content(Some(temp_file.path().to_path_buf())).unwrap();
121        assert_eq!(content.trim(), test_content);
122    }
123
124    #[test]
125    fn test_read_input_content_nonexistent_file() {
126        let result = read_input_content(Some(PathBuf::from("/nonexistent/file.rain")));
127        assert!(result.is_err());
128    }
129
130    #[test]
131    fn test_write_output_to_file() {
132        let deployment_data = DotrainSourceEmitData {
133            subject: "0x1234567890abcdef".to_string(),
134            meta_bytes: "0xdeadbeef".to_string(),
135            calldata: "0xcafebabe".to_string(),
136        };
137
138        let temp_file = NamedTempFile::new().unwrap();
139        let temp_path = temp_file.path().to_path_buf();
140
141        write_output(&deployment_data, Some(temp_path.clone())).unwrap();
142
143        let written_content = fs::read_to_string(&temp_path).unwrap();
144        assert!(written_content.contains("0x1234567890abcdef"));
145        assert!(written_content.contains("0xdeadbeef"));
146        assert!(written_content.contains("0xcafebabe"));
147    }
148
149    #[test]
150    fn test_full_generate_flow() {
151        // Create input file
152        let mut input_file = NamedTempFile::new().unwrap();
153        let test_content = "#main _ _: int-add(1 2) int-add(2 3)";
154        writeln!(input_file, "{}", test_content).unwrap();
155
156        // Create output file
157        let output_file = NamedTempFile::new().unwrap();
158        let output_path = output_file.path().to_path_buf();
159
160        // Execute generate command
161        let args = Generate {
162            command: GenerateCommand::Source(SourceArgs {
163                input_path: Some(input_file.path().to_path_buf()),
164                output_path: Some(output_path.clone()),
165            }),
166        };
167
168        generate(args).unwrap();
169
170        // Verify output
171        let output_content = fs::read_to_string(&output_path).unwrap();
172        let parsed: DotrainSourceEmitData = serde_json::from_str(&output_content).unwrap();
173
174        assert!(parsed.subject.starts_with("0x"));
175        assert!(parsed.meta_bytes.starts_with("0x"));
176        assert!(parsed.calldata.starts_with("0x"));
177        assert_eq!(parsed.subject.len(), 66); // 0x + 64 hex chars
178    }
179}