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 #[arg(long, short, value_name = "STRING", env)]
35 pub crate_name: String,
36
37 #[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 #[command(flatten)]
49 pub generate: Generate,
50}
51
52#[derive(Args)]
53#[group(required = true, multiple = true)]
54pub struct Generate {
55 #[arg(
58 long,
59 short,
60 value_name = "dotted.case",
61 value_parser = dotted_case
62 )]
63 pub java: Option<String>,
64
65 #[arg(
68 long,
69 short,
70 value_name = "PascalCase",
71 value_parser = pascal_case
72 )]
73 pub swift: Option<String>,
74
75 #[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 #[arg(long, short, value_name = "STRING", env)]
90 pub crate_name: String,
91
92 #[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}