1#![deny(missing_docs)]
6
7use std::path::PathBuf;
8
9use clap::{ArgGroup, Args};
10use color_eyre::eyre::{Context, Result};
11use typify::{CrateVers, TypeSpace, TypeSpaceSettings, UnknownPolicy};
12
13#[derive(Args)]
15#[command(author, version, about)]
16#[command(group(
17 ArgGroup::new("build")
18 .args(["builder", "no_builder"]),
19))]
20pub struct CliArgs {
21 pub input: PathBuf,
23
24 #[arg(short, long, default_value = "false", group = "build")]
26 pub builder: bool,
27
28 #[arg(short = 'B', long, default_value = "false", group = "build")]
31 pub no_builder: bool,
32
33 #[arg(short, long = "additional-derive", value_name = "derive")]
35 pub additional_derives: Vec<String>,
36
37 #[arg(short, long)]
42 pub output: Option<PathBuf>,
43
44 #[arg(long = "crate")]
47 crates: Vec<CrateSpec>,
48
49 #[arg(long = "map-type")]
51 map_type: Option<String>,
52
53 #[arg(
56 long = "unknown-crates",
57 value_parser = ["generate", "allow", "deny"]
58 )]
59 unknown_crates: Option<String>,
60}
61
62impl CliArgs {
63 pub fn output_path(&self) -> Option<PathBuf> {
65 match &self.output {
66 Some(output_path) => {
67 if output_path == &PathBuf::from("-") {
68 None
69 } else {
70 Some(output_path.clone())
71 }
72 }
73 None => {
74 let mut output = self.input.clone();
75 output.set_extension("rs");
76 Some(output)
77 }
78 }
79 }
80
81 pub fn use_builder(&self) -> bool {
83 !self.no_builder
84 }
85}
86
87#[derive(Debug, Clone)]
88struct CrateSpec {
89 name: String,
90 version: CrateVers,
91 rename: Option<String>,
92}
93
94impl std::str::FromStr for CrateSpec {
95 type Err = &'static str;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 fn is_crate(s: &str) -> bool {
99 !s.contains(|cc: char| !cc.is_alphabetic() && cc != '-' && cc != '_')
100 }
101
102 fn convert(s: &str) -> Option<CrateSpec> {
103 let (rename, s) = if let Some(ii) = s.find('=') {
104 let rename = &s[..ii];
105 let rest = &s[ii + 1..];
106 if !is_crate(rename) {
107 return None;
108 }
109 (Some(rename.to_string()), rest)
110 } else {
111 (None, s)
112 };
113
114 let ii = s.find('@')?;
115 let crate_str = &s[..ii];
116 let vers_str = &s[ii + 1..];
117
118 if !is_crate(crate_str) {
119 return None;
120 }
121 let version = CrateVers::parse(vers_str)?;
122
123 Some(CrateSpec {
124 name: crate_str.to_string(),
125 version,
126 rename,
127 })
128 }
129
130 convert(s).ok_or("crate specifier must be of the form 'cratename@version'")
131 }
132}
133
134pub fn convert(args: &CliArgs) -> Result<String> {
136 let content = std::fs::read_to_string(&args.input)
137 .wrap_err_with(|| format!("Failed to open input file: {}", &args.input.display()))?;
138
139 let schema = serde_json::from_str::<schemars::schema::RootSchema>(&content)
140 .wrap_err("Failed to parse input file as JSON Schema")?;
141
142 let mut settings = TypeSpaceSettings::default();
143 settings.with_struct_builder(args.use_builder());
144
145 for derive in &args.additional_derives {
146 settings.with_derive(derive.clone());
147 }
148
149 for CrateSpec {
150 name,
151 version,
152 rename,
153 } in &args.crates
154 {
155 settings.with_crate(name, version.clone(), rename.as_ref());
156 }
157
158 if let Some(map_type) = &args.map_type {
159 settings.with_map_type(map_type.clone());
160 }
161
162 if let Some(unknown_crates) = &args.unknown_crates {
163 let unknown_crates = match unknown_crates.as_str() {
164 "generate" => UnknownPolicy::Generate,
165 "allow" => UnknownPolicy::Allow,
166 "deny" => UnknownPolicy::Deny,
167 _ => unreachable!(),
168 };
169 settings.with_unknown_crates(unknown_crates);
170 }
171
172 let mut type_space = TypeSpace::new(&settings);
173 type_space
174 .add_root_schema(schema)
175 .wrap_err("Schema conversion failed")?;
176
177 let intro = "#![allow(clippy::redundant_closure_call)]
178#![allow(clippy::needless_lifetimes)]
179#![allow(clippy::match_single_binding)]
180#![allow(clippy::clone_on_copy)]
181";
182
183 let contents = format!("{intro}\n{}", type_space.to_stream());
184
185 let contents = rustfmt_wrapper::rustfmt(contents).wrap_err("Failed to format Rust code")?;
186
187 Ok(contents)
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn test_output_parsing_stdout() {
196 let args = CliArgs {
197 input: PathBuf::from("input.json"),
198 builder: false,
199 additional_derives: vec![],
200 output: Some(PathBuf::from("-")),
201 no_builder: false,
202 crates: vec![],
203 map_type: None,
204 unknown_crates: Default::default(),
205 };
206
207 assert_eq!(args.output_path(), None);
208 }
209
210 #[test]
211 fn test_output_parsing_file() {
212 let args = CliArgs {
213 input: PathBuf::from("input.json"),
214 builder: false,
215 additional_derives: vec![],
216 output: Some(PathBuf::from("some_file.rs")),
217 no_builder: false,
218 crates: vec![],
219 map_type: None,
220 unknown_crates: Default::default(),
221 };
222
223 assert_eq!(args.output_path(), Some(PathBuf::from("some_file.rs")));
224 }
225
226 #[test]
227 fn test_output_parsing_default() {
228 let args = CliArgs {
229 input: PathBuf::from("input.json"),
230 builder: false,
231 additional_derives: vec![],
232 output: None,
233 no_builder: false,
234 crates: vec![],
235 map_type: None,
236 unknown_crates: Default::default(),
237 };
238
239 assert_eq!(args.output_path(), Some(PathBuf::from("input.rs")));
240 }
241
242 #[test]
243 fn test_use_btree_map() {
244 let args = CliArgs {
245 input: PathBuf::from("input.json"),
246 builder: false,
247 additional_derives: vec![],
248 output: None,
249 no_builder: false,
250 crates: vec![],
251 map_type: Some("::std::collections::BTreeMap".to_string()),
252 unknown_crates: Default::default(),
253 };
254
255 assert_eq!(
256 args.map_type,
257 Some("::std::collections::BTreeMap".to_string())
258 );
259 }
260
261 #[test]
262 fn test_builder_as_default_style() {
263 let args = CliArgs {
264 input: PathBuf::from("input.json"),
265 builder: false,
266 additional_derives: vec![],
267 output: None,
268 no_builder: false,
269 crates: vec![],
270 map_type: None,
271 unknown_crates: Default::default(),
272 };
273
274 assert!(args.use_builder());
275 }
276
277 #[test]
278 fn test_no_builder() {
279 let args = CliArgs {
280 input: PathBuf::from("input.json"),
281 builder: false,
282 additional_derives: vec![],
283 output: None,
284 no_builder: true,
285 crates: vec![],
286 map_type: None,
287 unknown_crates: Default::default(),
288 };
289
290 assert!(!args.use_builder());
291 }
292
293 #[test]
294 fn test_builder_opt_in() {
295 let args = CliArgs {
296 input: PathBuf::from("input.json"),
297 builder: true,
298 additional_derives: vec![],
299 output: None,
300 no_builder: false,
301 crates: vec![],
302 map_type: None,
303 unknown_crates: Default::default(),
304 };
305
306 assert!(args.use_builder());
307 }
308}