rain_metadata/cli/
generate.rs1use 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#[derive(Args)]
11pub struct Generate {
12 #[command(subcommand)]
13 pub command: GenerateCommand,
14}
15
16#[derive(Subcommand)]
18pub enum GenerateCommand {
19 Source(SourceArgs),
21}
22
23#[derive(Args)]
25pub struct SourceArgs {
26 #[arg(short = 'i', long = "input-path")]
28 input_path: Option<PathBuf>,
29
30 #[arg(short = 'o', long = "output-path")]
32 output_path: Option<PathBuf>,
33}
34
35fn read_input_content(input_path: Option<PathBuf>) -> Result<String, Error> {
37 match input_path {
38 Some(path) => {
39 fs::read_to_string(&path).map_err(|e| {
41 Error::InvalidInput(format!("Failed to read file '{}': {}", path.display(), e))
42 })
43 }
44 None => {
45 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
55fn write_output(data: &DotrainSourceEmitData, output_path: Option<PathBuf>) -> Result<(), Error> {
57 let json_output = serde_json::to_string_pretty(data).map_err(Error::SerdeJsonError)?;
59
60 match output_path {
61 Some(path) => {
62 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 fs::write(&path, json_output).map_err(|e| {
76 Error::InvalidInput(format!("Failed to write file '{}': {}", path.display(), e))
77 })
78 }
79 None => {
80 println!("{}", json_output);
82 Ok(())
83 }
84 }
85}
86
87pub fn generate(args: Generate) -> anyhow::Result<()> {
89 match args.command {
90 GenerateCommand::Source(source_args) => generate_source(source_args),
91 }
92}
93
94fn generate_source(args: SourceArgs) -> anyhow::Result<()> {
96 let content = read_input_content(args.input_path)?;
98
99 let tx_data = generate_dotrain_source_emit_tx_data(&content)?;
100
101 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 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 let expected = "{\n \"subject\": \"0x1234567890abcdef\",\n \"meta_bytes\": \"0xdeadbeef\",\n \"calldata\": \"0xcafebabe\"\n}";
156 assert_eq!(written_content, expected);
157 }
158
159 #[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 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 let output_file = NamedTempFile::new().unwrap();
188 let output_path = output_file.path().to_path_buf();
189
190 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 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); }
209}