spacetimedb_cli/subcommands/
generate.rs1#![warn(clippy::uninlined_format_args)]
2
3use anyhow::Context;
4use clap::parser::ValueSource;
5use clap::Arg;
6use clap::ArgAction::Set;
7use fs_err as fs;
8use spacetimedb_codegen::{generate, Csharp, Lang, Rust, TypeScript, AUTO_GENERATED_PREFIX};
9use spacetimedb_lib::de::serde::DeserializeWrapper;
10use spacetimedb_lib::{sats, RawModuleDef};
11use spacetimedb_schema;
12use spacetimedb_schema::def::ModuleDef;
13use std::path::{Path, PathBuf};
14use std::process::{Command, Stdio};
15
16use crate::tasks::csharp::dotnet_format;
17use crate::tasks::rust::rustfmt;
18use crate::util::{resolve_sibling_binary, y_or_n};
19use crate::Config;
20use crate::{build, common_args};
21use clap::builder::PossibleValue;
22use std::collections::BTreeSet;
23use std::io::Read;
24
25pub fn cli() -> clap::Command {
26 clap::Command::new("generate")
27 .about("Generate client files for a spacetime module.")
28 .override_usage("spacetime generate --lang <LANG> --out-dir <DIR> [--project-path <DIR> | --bin-path <PATH>]")
29 .arg(
30 Arg::new("wasm_file")
31 .value_parser(clap::value_parser!(PathBuf))
32 .long("bin-path")
33 .short('b')
34 .group("source")
35 .conflicts_with("project_path")
36 .conflicts_with("build_options")
37 .help("The system path (absolute or relative) to the compiled wasm binary we should inspect"),
38 )
39 .arg(
40 Arg::new("project_path")
41 .value_parser(clap::value_parser!(PathBuf))
42 .default_value(".")
43 .long("project-path")
44 .short('p')
45 .group("source")
46 .help("The system path (absolute or relative) to the project you would like to inspect"),
47 )
48 .arg(
49 Arg::new("json_module")
50 .hide(true)
51 .num_args(0..=1)
52 .value_parser(clap::value_parser!(PathBuf))
53 .long("module-def")
54 .group("source")
55 .help("Generate from a ModuleDef encoded as json"),
56 )
57 .arg(
58 Arg::new("out_dir")
59 .value_parser(clap::value_parser!(PathBuf))
60 .required(true)
61 .long("out-dir")
62 .short('o')
63 .help("The system path (absolute or relative) to the generate output directory"),
64 )
65 .arg(
66 Arg::new("namespace")
67 .default_value("SpacetimeDB.Types")
68 .long("namespace")
69 .help("The namespace that should be used"),
70 )
71 .arg(
72 Arg::new("lang")
73 .required(true)
74 .long("lang")
75 .short('l')
76 .value_parser(clap::value_parser!(Language))
77 .help("The language to generate"),
78 )
79 .arg(
80 Arg::new("build_options")
81 .long("build-options")
82 .alias("build-opts")
83 .action(Set)
84 .default_value("")
85 .help("Options to pass to the build command, for example --build-options='--lint-dir='"),
86 )
87 .arg(common_args::yes())
88 .after_help("Run `spacetime help publish` for more detailed information.")
89}
90
91pub async fn exec(config: Config, args: &clap::ArgMatches) -> anyhow::Result<()> {
92 exec_ex(config, args, extract_descriptions).await
93}
94
95pub async fn exec_ex(
97 config: Config,
98 args: &clap::ArgMatches,
99 extract_descriptions: ExtractDescriptions,
100) -> anyhow::Result<()> {
101 let project_path = args.get_one::<PathBuf>("project_path").unwrap();
102 let wasm_file = args.get_one::<PathBuf>("wasm_file").cloned();
103 let json_module = args.get_many::<PathBuf>("json_module");
104 let out_dir = args.get_one::<PathBuf>("out_dir").unwrap();
105 let lang = *args.get_one::<Language>("lang").unwrap();
106 let namespace = args.get_one::<String>("namespace").unwrap();
107 let force = args.get_flag("force");
108 let build_options = args.get_one::<String>("build_options").unwrap();
109
110 if args.value_source("namespace") == Some(ValueSource::CommandLine) && lang != Language::Csharp {
111 return Err(anyhow::anyhow!("--namespace is only supported with --lang csharp"));
112 }
113
114 let module: ModuleDef = if let Some(mut json_module) = json_module {
115 let DeserializeWrapper::<RawModuleDef>(module) = if let Some(path) = json_module.next() {
116 serde_json::from_slice(&fs::read(path)?)?
117 } else {
118 serde_json::from_reader(std::io::stdin().lock())?
119 };
120 module.try_into()?
121 } else {
122 let wasm_path = if let Some(path) = wasm_file {
123 println!("Skipping build. Instead we are inspecting {}", path.display());
124 path.clone()
125 } else {
126 build::exec_with_argstring(config.clone(), project_path, build_options).await?
127 };
128 let spinner = indicatif::ProgressBar::new_spinner();
129 spinner.enable_steady_tick(std::time::Duration::from_millis(60));
130 spinner.set_message("Extracting schema from wasm...");
131 extract_descriptions(&wasm_path).context("could not extract schema")?
132 };
133
134 fs::create_dir_all(out_dir)?;
135
136 let mut paths = BTreeSet::new();
137
138 let csharp_lang;
139 let gen_lang = match lang {
140 Language::Csharp => {
141 csharp_lang = Csharp { namespace };
142 &csharp_lang as &dyn Lang
143 }
144 Language::Rust => &Rust,
145 Language::TypeScript => &TypeScript,
146 };
147
148 for (fname, code) in generate(&module, gen_lang) {
149 let fname = Path::new(&fname);
150 if let Some(parent) = fname.parent().filter(|p| !p.as_os_str().is_empty()) {
152 fs::create_dir_all(out_dir.join(parent))?;
153 }
154 let path = out_dir.join(fname);
155 fs::write(&path, code)?;
156 paths.insert(path);
157 }
158
159 let mut auto_generated_buf: [u8; AUTO_GENERATED_PREFIX.len()] = [0; AUTO_GENERATED_PREFIX.len()];
161 let files_to_delete = walkdir::WalkDir::new(out_dir)
162 .into_iter()
163 .map(|entry_result| {
164 let entry = entry_result?;
165 if !entry.file_type().is_file() {
167 return Ok(None);
168 }
169 let path = entry.into_path();
170 if paths.contains(&path) {
172 return Ok(None);
173 }
174 let mut file = fs::File::open(&path)?;
176 Ok(match file.read_exact(&mut auto_generated_buf) {
177 Ok(()) => (auto_generated_buf == AUTO_GENERATED_PREFIX.as_bytes()).then_some(path),
178 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => None,
179 Err(err) => return Err(err.into()),
180 })
181 })
182 .filter_map(Result::transpose)
183 .collect::<anyhow::Result<Vec<_>>>()?;
184
185 if !files_to_delete.is_empty() {
186 println!("The following files were not generated by this command and will be deleted:");
187 for path in &files_to_delete {
188 println!(" {}", path.to_str().unwrap());
189 }
190
191 if y_or_n(force, "Are you sure you want to delete these files?")? {
192 for path in files_to_delete {
193 fs::remove_file(path)?;
194 }
195 println!("Files deleted successfully.");
196 } else {
197 println!("Files not deleted.");
198 }
199 }
200
201 if let Err(err) = lang.format_files(paths) {
202 eprintln!("Could not format generated files: {err}");
205 }
206
207 println!("Generate finished successfully.");
208 Ok(())
209}
210
211#[derive(Clone, Copy, PartialEq)]
212pub enum Language {
213 Csharp,
214 TypeScript,
215 Rust,
216}
217
218impl clap::ValueEnum for Language {
219 fn value_variants<'a>() -> &'a [Self] {
220 &[Self::Csharp, Self::TypeScript, Self::Rust]
221 }
222 fn to_possible_value(&self) -> Option<PossibleValue> {
223 Some(match self {
224 Self::Csharp => clap::builder::PossibleValue::new("csharp").aliases(["c#", "cs"]),
225 Self::TypeScript => clap::builder::PossibleValue::new("typescript").aliases(["ts", "TS"]),
226 Self::Rust => clap::builder::PossibleValue::new("rust").aliases(["rs", "RS"]),
227 })
228 }
229}
230
231impl Language {
232 fn format_files(&self, generated_files: BTreeSet<PathBuf>) -> anyhow::Result<()> {
233 match self {
234 Language::Rust => rustfmt(generated_files)?,
235 Language::Csharp => dotnet_format(generated_files)?,
236 Language::TypeScript => {
237 }
239 }
240
241 Ok(())
242 }
243}
244
245pub type ExtractDescriptions = fn(&Path) -> anyhow::Result<ModuleDef>;
246fn extract_descriptions(wasm_file: &Path) -> anyhow::Result<ModuleDef> {
247 let bin_path = resolve_sibling_binary("spacetimedb-standalone")?;
248 let child = Command::new(&bin_path)
249 .arg("extract-schema")
250 .arg(wasm_file)
251 .stdout(Stdio::piped())
252 .spawn()
253 .with_context(|| format!("failed to spawn {}", bin_path.display()))?;
254 let sats::serde::SerdeWrapper::<RawModuleDef>(module) = serde_json::from_reader(child.stdout.unwrap())?;
255 Ok(module.try_into()?)
256}