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