1use std::path::PathBuf;
2use std::thread;
3use std::time::Duration;
4
5use anyhow::Result;
6use clap::{Args, Parser, Subcommand};
7use serde_json::json;
8
9mod contract;
10mod init;
11mod output;
12mod project;
13mod python;
14mod typescript;
15
16use output::source_state;
17use project::{Project, build, check};
18
19#[derive(Debug, Parser)]
20#[command(
21 name = "rspyts",
22 version,
23 about = "Build one Rust API for Python and TypeScript"
24)]
25struct Cli {
26 #[command(subcommand)]
27 command: Command,
28}
29
30#[derive(Debug, Subcommand)]
31enum Command {
32 Init(InitArgs),
34 Build(ProjectArgs),
36 Watch(ProjectArgs),
38 Check(ProjectArgs),
40}
41
42#[derive(Debug, Args)]
43struct InitArgs {
44 path: PathBuf,
46}
47
48#[derive(Debug, Args)]
49struct ProjectArgs {
50 #[arg(long, default_value = "Cargo.toml")]
52 manifest_path: PathBuf,
53}
54
55pub fn run() -> Result<()> {
61 run_from(Cli::parse())
62}
63
64fn run_from(cli: Cli) -> Result<()> {
65 match cli.command {
66 Command::Init(args) => {
67 let report = init::create(&args.path)?;
68 println!("{}", serde_json::to_string_pretty(&report)?);
69 }
70 Command::Build(args) => {
71 let project = Project::read(&args.manifest_path)?;
72 let report = build(&project)?;
73 println!("{}", serde_json::to_string_pretty(&report)?);
74 }
75 Command::Check(args) => {
76 let project = Project::read(&args.manifest_path)?;
77 check(&project)?;
78 println!(
79 "{}",
80 serde_json::to_string_pretty(&json!({
81 "status": "ok",
82 "output": project.output(),
83 }))?
84 );
85 }
86 Command::Watch(args) => {
87 let project = Project::read(&args.manifest_path)?;
88 build(&project)?;
89 println!("rspyts is watching {}", project.workspace_root.display());
90 let mut state = source_state(&project.workspace_root)?;
91 loop {
92 thread::sleep(Duration::from_millis(500));
93 let next = source_state(&project.workspace_root)?;
94 if next != state {
95 match build(&project) {
96 Ok(_) => {
97 state = next;
98 println!("rspyts rebuilt {}", project.output().display());
99 }
100 Err(error) => eprintln!("rspyts build failed: {error:#}"),
101 }
102 }
103 }
104 }
105 }
106 Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111 use std::path::Path;
112
113 use rspyts::ir::{Manifest, TypeRef};
114
115 use super::{output, project, typescript};
116
117 #[test]
118 fn validates_package_names() {
119 assert!(project::validate_python_package("example.client").is_ok());
120 assert!(project::validate_python_package("example-client").is_err());
121 assert!(project::validate_typescript_package("@example/client").is_ok());
122 assert!(project::validate_typescript_package("Example").is_err());
123 }
124
125 #[test]
126 fn rejects_duplicate_public_names() {
127 assert!(project::unique_public_names("Python", ["Thing", "Other"].into_iter()).is_ok());
128 assert!(project::unique_public_names("Python", ["Thing", "Thing"].into_iter()).is_err());
129 }
130
131 #[test]
132 fn uses_bigint_for_wide_types() {
133 let manifest = Manifest {
134 ir_version: 1,
135 package_name: "fixture".into(),
136 package_version: "1.0.0".into(),
137 module_name: "native".into(),
138 types: vec![],
139 errors: vec![],
140 functions: vec![],
141 resources: vec![],
142 constants: vec![],
143 };
144 assert_eq!(
145 typescript::type_ref(
146 &TypeRef::Int {
147 signed: false,
148 bits: 64,
149 },
150 &manifest,
151 )
152 .unwrap(),
153 "bigint"
154 );
155 }
156
157 #[test]
158 fn ignores_python_cache_files_during_sync_checks() {
159 let directory = tempfile::tempdir().unwrap();
160 output::write(&directory.path().join("package.py"), "value = 1\n").unwrap();
161 output::write(&directory.path().join("__pycache__/package.pyc"), "cache").unwrap();
162 output::write(&directory.path().join("build/package.py"), "build").unwrap();
163 output::write(
164 &directory.path().join("package.egg-info/PKG-INFO"),
165 "metadata",
166 )
167 .unwrap();
168
169 let files = output::file_tree(directory.path()).unwrap();
170 assert_eq!(files.keys().collect::<Vec<_>>(), [Path::new("package.py")]);
171 }
172
173 #[test]
174 fn fingerprints_only_rust_and_cargo_sources() {
175 let directory = tempfile::tempdir().unwrap();
176 output::write(&directory.path().join("Cargo.toml"), "[workspace]\n").unwrap();
177 let initial = output::source_fingerprint(directory.path()).unwrap();
178
179 output::write(&directory.path().join("dist/generated.js"), "generated\n").unwrap();
180 assert_eq!(
181 output::source_fingerprint(directory.path()).unwrap(),
182 initial
183 );
184
185 output::write(
186 &directory.path().join("src/lib.rs"),
187 "pub fn changed() {}\n",
188 )
189 .unwrap();
190 assert_ne!(
191 output::source_fingerprint(directory.path()).unwrap(),
192 initial
193 );
194 }
195}