typeshare_engine/
args.rs

1use std::path::{Path, PathBuf};
2
3use clap::builder::PossibleValuesParser;
4
5use crate::serde::args::{ArgType, CliArgsSet};
6
7#[derive(Debug, Clone, Copy)]
8pub enum OutputLocation<'a> {
9    File(&'a Path),
10    Folder(&'a Path),
11}
12
13#[derive(clap::Args, Debug)]
14#[group(multiple = false, required = true)]
15pub struct Output {
16    /// File to write output to. mtime will be preserved if the file contents
17    /// don't change
18    #[arg(short = 'o', long = "output-file")]
19    pub file: Option<PathBuf>,
20
21    /// Folder to write output to. mtime will be preserved if the file contents
22    /// don't change
23    #[arg(short = 'd', long = "output-folder")]
24    pub directory: Option<PathBuf>,
25}
26
27impl Output {
28    pub fn location(&self) -> OutputLocation<'_> {
29        match (&self.directory, &self.file) {
30            (Some(dir), None) => OutputLocation::Folder(dir),
31            (None, Some(file)) => OutputLocation::File(file),
32            (None, None) => panic!("got neither a file nor a directory; clap should prevent this"),
33            (Some(dir), Some(file)) => {
34                panic!("got both file '{file:?}' and directory '{dir:?}'; clap should prevent this")
35            }
36        }
37    }
38}
39
40#[derive(clap::Parser, Debug)]
41#[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)]
42pub struct StandardArgs {
43    #[command(subcommand)]
44    pub subcommand: Option<Command>,
45
46    /// Path to the config file for this typeshare
47    #[arg(short, long, visible_alias("config-file"))]
48    pub config: Option<PathBuf>,
49
50    /// The directories within which to recursively find and process rust files
51    #[arg(num_args(1..), required=true)]
52    pub directories: Vec<PathBuf>,
53
54    #[arg(long, exclusive(true))]
55    pub completions: Option<String>,
56
57    #[command(flatten)]
58    pub output: Output,
59
60    /// If given, only fields / types / variants matching at least one of these
61    /// OSes (per `cfg(target_os)`) will be emitted. If any `--target-os`
62    /// arguments are passed, they will override ALL target OSes passed via
63    /// a config file.
64    ///
65    /// Generally, typeshare will err on the side of generating things. For
66    /// instance, given `--target-os=ios` and `cfg(any(target_os="android", test))`,
67    /// it WILL generate a type, because that type does exist on iOS in test
68    /// mode: there exists a configuration where that type exists on iOS.
69    ///
70    /// In the future typeshare may be able to consider other cfgs.
71    #[arg(long, num_args=1..)]
72    pub target_os: Option<Vec<String>>,
73}
74
75#[derive(Debug, Clone, Copy, clap::Subcommand)]
76pub enum Command {
77    /// Generate shell completions
78    Completions {
79        /// The shell to generate the completions for
80        shell: clap_complete::Shell,
81    },
82}
83
84#[derive(Debug, Clone, Default)]
85#[non_exhaustive]
86pub struct PersonalizeClap {
87    name: Option<&'static str>,
88    version: Option<&'static str>,
89    author: Option<&'static str>,
90    about: Option<&'static str>,
91}
92
93impl PersonalizeClap {
94    pub const fn new() -> Self {
95        Self {
96            name: None,
97            version: None,
98            author: None,
99            about: None,
100        }
101    }
102
103    pub const fn name(self, name: &'static str) -> Self {
104        Self {
105            name: Some(name),
106            ..self
107        }
108    }
109
110    pub const fn version(self, version: &'static str) -> Self {
111        Self {
112            version: Some(version),
113            ..self
114        }
115    }
116
117    pub const fn author(self, author: &'static str) -> Self {
118        Self {
119            author: Some(author),
120            ..self
121        }
122    }
123
124    pub const fn about(self, about: &'static str) -> Self {
125        Self {
126            about: Some(about),
127            ..self
128        }
129    }
130}
131
132pub fn add_personalizations(
133    command: clap::Command,
134    personalizations: PersonalizeClap,
135) -> clap::Command {
136    let command = command.arg(
137        clap::Arg::new("version")
138            .short('V')
139            .long("version")
140            .action(clap::ArgAction::Version),
141    );
142
143    let command = match personalizations.name {
144        Some(name) => command.name(name),
145        None => command,
146    };
147
148    let command = match personalizations.version {
149        Some(version) => command.version(version),
150        None => command,
151    };
152
153    let command = match personalizations.author {
154        Some(author) => command.author(author),
155        None => command,
156    };
157
158    let command = match personalizations.about {
159        Some(about) => command.about(about),
160        None => command,
161    };
162
163    command
164}
165
166/// Add a `--lang` argument to the command. This argument will be optional if
167/// there is only one language
168pub fn add_lang_argument(command: clap::Command, languages: &[&'static str]) -> clap::Command {
169    let arg = clap::Arg::new("language")
170        .short('l')
171        .long("lang")
172        .value_name("LANGUAGE")
173        .value_parser(PossibleValuesParser::new(languages))
174        .action(clap::ArgAction::Set)
175        .help("the output language of generated types");
176
177    command.arg(match languages {
178        [] => panic!("need at least one language"),
179        [lang] => arg.required(false).default_value(lang),
180        _ => arg.required(true),
181    })
182}
183
184/// Given a CliArgsSet for a language, use the name of the language and
185/// information about its configuration to populate a clap command with
186/// args specific to that language
187pub fn add_language_params_to_clap(
188    command: clap::Command,
189    language: &'static str,
190    args: &CliArgsSet,
191) -> clap::Command {
192    if let Some(arg) = command
193        .get_arguments()
194        .find(|arg| arg.get_id().as_str().starts_with(language))
195    {
196        panic!(
197            "existing argument {id:?} conflicts with language {language}",
198            id = arg.get_id().as_str(),
199        )
200    }
201
202    args.iter().fold(command, |command, spec| {
203        let arg = clap::Arg::new(spec.full_key.to_owned())
204            .long(spec.full_key.to_owned())
205            .required(false);
206
207        command.arg(match spec.arg_type {
208            ArgType::Bool => arg.action(clap::ArgAction::SetTrue),
209            ArgType::Value => arg.action(clap::ArgAction::Set).value_name(spec.key),
210        })
211    })
212}