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        let err = result.unwrap_err();
128        assert!(
129            err.to_string()
130                .contains("Failed to read file '/nonexistent/file.rain'"),
131            "unexpected error: {err}"
132        );
133    }
134
135    #[test]
136    fn test_write_output_to_file() {
137        let deployment_data = DotrainSourceEmitData {
138            subject: "0x1234567890abcdef".to_string(),
139            meta_bytes: "0xdeadbeef".to_string(),
140            calldata: "0xcafebabe".to_string(),
141        };
142
143        let temp_file = NamedTempFile::new().unwrap();
144        let temp_path = temp_file.path().to_path_buf();
145
146        write_output(&deployment_data, Some(temp_path.clone())).unwrap();
147
148        let written_content = fs::read_to_string(&temp_path).unwrap();
149        assert!(written_content.contains("0x1234567890abcdef"));
150        assert!(written_content.contains("0xdeadbeef"));
151        assert!(written_content.contains("0xcafebabe"));
152
153        // The output is pretty-printed with two-space indentation, in
154        // field declaration order.
155        let expected = "{\n  \"subject\": \"0x1234567890abcdef\",\n  \"meta_bytes\": \"0xdeadbeef\",\n  \"calldata\": \"0xcafebabe\"\n}";
156        assert_eq!(written_content, expected);
157    }
158
159    /// write_output creates missing parent directories of the output
160    /// path before writing.
161    #[test]
162    fn test_write_output_creates_parent_dirs() {
163        let deployment_data = DotrainSourceEmitData {
164            subject: "0x01".to_string(),
165            meta_bytes: "0x02".to_string(),
166            calldata: "0x03".to_string(),
167        };
168
169        let dir = tempfile::tempdir().unwrap();
170        let nested = dir.path().join("a").join("b").join("out.json");
171        write_output(&deployment_data, Some(nested.clone())).unwrap();
172
173        let written = fs::read_to_string(&nested).unwrap();
174        assert!(written.contains("0x01"));
175        assert!(written.contains("0x02"));
176        assert!(written.contains("0x03"));
177    }
178
179    #[test]
180    fn test_full_generate_flow() {
181        // Create input file
182        let mut input_file = NamedTempFile::new().unwrap();
183        let test_content = "#main _ _: int-add(1 2) int-add(2 3)";
184        writeln!(input_file, "{}", test_content).unwrap();
185
186        // Create output file
187        let output_file = NamedTempFile::new().unwrap();
188        let output_path = output_file.path().to_path_buf();
189
190        // Execute generate command
191        let args = Generate {
192            command: GenerateCommand::Source(SourceArgs {
193                input_path: Some(input_file.path().to_path_buf()),
194                output_path: Some(output_path.clone()),
195            }),
196        };
197
198        generate(args).unwrap();
199
200        // Verify output
201        let output_content = fs::read_to_string(&output_path).unwrap();
202        let parsed: DotrainSourceEmitData = serde_json::from_str(&output_content).unwrap();
203
204        assert!(parsed.subject.starts_with("0x"));
205        assert!(parsed.meta_bytes.starts_with("0x"));
206        assert!(parsed.calldata.starts_with("0x"));
207        assert_eq!(parsed.subject.len(), 66); // 0x + 64 hex chars
208    }
209}