crux_cli/
args.rs

1use std::path::PathBuf;
2
3use camino::Utf8PathBuf;
4use clap::{Args, Parser, Subcommand, ValueHint::DirPath};
5use convert_case::{pattern, Boundary, Case, Casing};
6
7#[derive(Parser)]
8#[command(
9    name = "crux",
10    bin_name = "crux",
11    author,
12    version,
13    about,
14    long_about = None,
15    arg_required_else_help(true),
16    propagate_version = true
17)]
18pub struct Cli {
19    #[command(subcommand)]
20    pub command: Commands,
21}
22
23#[derive(Subcommand)]
24pub enum Commands {
25    #[command(visible_alias = "gen")]
26    Codegen(CodegenArgs),
27    #[command(visible_alias = "ffi")]
28    Bindgen(BindgenArgs),
29}
30
31#[derive(Args)]
32pub struct CodegenArgs {
33    /// name of the library containing your Crux App
34    #[arg(long, short, value_name = "STRING", env)]
35    pub crate_name: String,
36
37    /// Output directory for generated code
38    #[arg(
39        long,
40        short,
41        value_name = "DIR",
42        value_hint = DirPath,
43        default_value = "./shared/generated",
44    )]
45    pub out_dir: PathBuf,
46
47    /// Specify a package name for each language you want to generate code for.
48    #[command(flatten)]
49    pub generate: Generate,
50}
51
52#[derive(Args)]
53#[group(required = true, multiple = true)]
54pub struct Generate {
55    /// Java package name.
56    /// If not specified, no code will be generated for Java/Kotlin.
57    #[arg(
58        long,
59        short,
60        value_name = "dotted.case",
61        value_parser = dotted_case
62    )]
63    pub java: Option<String>,
64
65    /// Swift package name.
66    /// If not specified, no code will be generated for Swift.
67    #[arg(
68        long,
69        short,
70        value_name = "PascalCase",
71        value_parser = pascal_case
72    )]
73    pub swift: Option<String>,
74
75    /// TypeScript package name.
76    /// If not specified, no code will be generated for TypeScript.
77    #[arg(
78        long,
79        short,
80        value_name = "snake_case",
81        value_parser = snake_case
82    )]
83    pub typescript: Option<String>,
84}
85
86#[derive(Args)]
87pub struct BindgenArgs {
88    /// Package name of the crate containing your Crux App, e.g. "shared"
89    #[arg(long, short, value_name = "STRING", env)]
90    pub crate_name: String,
91
92    /// Output directory for generated code
93    #[arg(
94        long,
95        short,
96        value_name = "DIR",
97        value_hint = DirPath,
98        default_value = "./shared/generated",
99    )]
100    pub out_dir: Utf8PathBuf,
101}
102
103fn dotted_case(s: &str) -> Result<String, String> {
104    const DOT_CASE: Case = Case::Custom {
105        boundaries: &[Boundary::from_delim(".")],
106        pattern: pattern::lowercase,
107        delim: ".",
108    };
109    if s.is_case(DOT_CASE) {
110        Ok(s.to_string())
111    } else {
112        Err(format!("Invalid dotted case: {s}"))
113    }
114}
115
116fn pascal_case(s: &str) -> Result<String, String> {
117    if s.is_case(Case::Pascal) {
118        Ok(s.to_string())
119    } else {
120        Err(format!("Invalid pascal case: {s}"))
121    }
122}
123
124fn snake_case(s: &str) -> Result<String, String> {
125    if s.is_case(Case::Snake) {
126        Ok(s.to_string())
127    } else {
128        Err(format!("Invalid snake case: {s}"))
129    }
130}
131
132#[cfg(test)]
133mod cli_tests {
134    use super::*;
135
136    #[test]
137    fn test_cli() {
138        use clap::CommandFactory;
139        Cli::command().debug_assert();
140    }
141
142    #[test]
143    fn dotted() {
144        assert_eq!(
145            dotted_case("com.example.crux.shared.types").unwrap(),
146            "com.example.crux.shared.types"
147        );
148        assert_eq!(
149            dotted_case("comExampleCruxSharedTypes").unwrap_err(),
150            "Invalid dotted case: comExampleCruxSharedTypes"
151        );
152    }
153
154    #[test]
155    fn pascal() {
156        assert_eq!(pascal_case("SharedTypes").unwrap(), "SharedTypes");
157        assert_eq!(
158            pascal_case("shared_types").unwrap_err(),
159            "Invalid pascal case: shared_types"
160        );
161    }
162
163    #[test]
164    fn snake() {
165        assert_eq!(snake_case("shared_types").unwrap(), "shared_types");
166        assert_eq!(
167            snake_case("SharedTypes").unwrap_err(),
168            "Invalid snake case: SharedTypes"
169        );
170    }
171}