1use fancy_regex::{Match, Regex};
16use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase, ToTitleCase};
17use serde::{Deserialize, Serialize};
18use std::collections::HashSet;
19use std::fmt::Formatter;
20use std::path::PathBuf;
21use std::str::FromStr;
22use std::sync::LazyLock;
23use std::{fmt, io};
24use strum::IntoEnumIterator;
25use strum_macros::EnumIter;
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
28pub struct ComponentName(String);
29
30static COMPONENT_NAME_SPLIT_REGEX: LazyLock<Regex> =
31 LazyLock::new(|| Regex::new("(?=[A-Z\\-_:])").unwrap());
32
33impl ComponentName {
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37
38 pub fn parts(&self) -> Vec<String> {
39 let matches: Vec<Result<Match, fancy_regex::Error>> =
40 COMPONENT_NAME_SPLIT_REGEX.find_iter(&self.0).collect();
41 let mut parts: Vec<&str> = vec![];
42 let mut last = 0;
43 for m in matches.into_iter().flatten() {
44 let part = &self.0[last..m.start()];
45 if !part.is_empty() {
46 parts.push(part);
47 }
48 last = m.end();
49 }
50 parts.push(&self.0[last..]);
51
52 let mut result: Vec<String> = Vec::with_capacity(parts.len());
53 for part in parts {
54 let s = part.to_lowercase();
55 let s = s.strip_prefix('-').unwrap_or(&s);
56 let s = s.strip_prefix('_').unwrap_or(s);
57 let s = s.strip_prefix(':').unwrap_or(s);
58 result.push(s.to_string());
59 }
60 result
61 }
62
63 pub fn to_kebab_case(&self) -> String {
64 self.parts().join("-")
65 }
66
67 pub fn to_snake_case(&self) -> String {
68 self.parts().join("_")
69 }
70
71 pub fn to_pascal_case(&self) -> String {
72 self.parts().iter().map(|s| s.to_title_case()).collect()
73 }
74
75 pub fn to_camel_case(&self) -> String {
76 self.to_pascal_case().to_lower_camel_case()
77 }
78}
79
80impl From<&str> for ComponentName {
81 fn from(name: &str) -> Self {
82 ComponentName(name.to_string())
83 }
84}
85
86impl From<String> for ComponentName {
87 fn from(name: String) -> Self {
88 ComponentName(name)
89 }
90}
91
92impl fmt::Display for ComponentName {
93 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94 write!(f, "{}", self.0)
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum TemplateKind {
100 Standalone,
101 ComposableAppCommon {
102 group: ComposableAppGroupName,
103 skip_if_exists: Option<PathBuf>,
104 },
105 ComposableAppComponent {
106 group: ComposableAppGroupName,
107 },
108}
109
110impl TemplateKind {
111 pub fn is_common(&self) -> bool {
112 matches!(self, TemplateKind::ComposableAppCommon { .. })
113 }
114}
115
116#[derive(
117 Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumIter, Serialize, Deserialize,
118)]
119pub enum GuestLanguage {
120 Rust,
121 TypeScript,
122}
123
124impl GuestLanguage {
125 pub fn from_string(s: impl AsRef<str>) -> Option<GuestLanguage> {
126 match s.as_ref().to_lowercase().as_str() {
127 "rust" => Some(GuestLanguage::Rust),
128 "ts" | "typescript" => Some(GuestLanguage::TypeScript),
129 _ => None,
130 }
131 }
132
133 pub fn id(&self) -> String {
134 match self {
135 GuestLanguage::Rust => "rust".to_string(),
136 GuestLanguage::TypeScript => "ts".to_string(),
137 }
138 }
139
140 pub fn tier(&self) -> GuestLanguageTier {
141 match self {
142 GuestLanguage::Rust => GuestLanguageTier::Tier1,
143 GuestLanguage::TypeScript => GuestLanguageTier::Tier1,
144 }
145 }
146
147 pub fn name(&self) -> &'static str {
148 match self {
149 GuestLanguage::Rust => "Rust",
150 GuestLanguage::TypeScript => "TypeScript",
151 }
152 }
153}
154
155impl fmt::Display for GuestLanguage {
156 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
157 write!(f, "{}", self.name())
158 }
159}
160
161impl FromStr for GuestLanguage {
162 type Err = String;
163
164 fn from_str(s: &str) -> Result<Self, Self::Err> {
165 GuestLanguage::from_string(s).ok_or({
166 let all = GuestLanguage::iter()
167 .map(|x| format!("\"{x}\""))
168 .collect::<Vec<String>>()
169 .join(", ");
170 format!("Unknown guest language: {s}. Expected one of {all}")
171 })
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumIter, Serialize, Deserialize)]
176pub enum GuestLanguageTier {
177 Tier1,
178 Tier2,
179 Tier3,
180}
181
182impl GuestLanguageTier {
183 pub fn from_string(s: impl AsRef<str>) -> Option<GuestLanguageTier> {
184 match s.as_ref().to_lowercase().as_str() {
185 "tier1" | "1" => Some(GuestLanguageTier::Tier1),
186 "tier2" | "2" => Some(GuestLanguageTier::Tier2),
187 "tier3" | "3" => Some(GuestLanguageTier::Tier3),
188 _ => None,
189 }
190 }
191
192 pub fn level(&self) -> u8 {
193 match self {
194 GuestLanguageTier::Tier1 => 1,
195 GuestLanguageTier::Tier2 => 2,
196 GuestLanguageTier::Tier3 => 3,
197 }
198 }
199
200 pub fn name(&self) -> &'static str {
201 match self {
202 GuestLanguageTier::Tier1 => "tier1",
203 GuestLanguageTier::Tier2 => "tier2",
204 GuestLanguageTier::Tier3 => "tier3",
205 }
206 }
207}
208
209impl fmt::Display for GuestLanguageTier {
210 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
211 write!(f, "{}", self.name())
212 }
213}
214
215impl FromStr for GuestLanguageTier {
216 type Err = String;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 GuestLanguageTier::from_string(s).ok_or(format!("Unexpected guest language tier {s}"))
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
224pub struct PackageName((String, String));
225
226impl PackageName {
227 pub fn from_string(s: impl AsRef<str>) -> Option<PackageName> {
228 let parts: Vec<&str> = s.as_ref().split(':').collect();
229 match parts.as_slice() {
230 &[n1, n2] if !n1.is_empty() && !n2.is_empty() => {
231 Some(PackageName((n1.to_string(), n2.to_string())))
232 }
233 _ => None,
234 }
235 }
236
237 pub fn to_pascal_case(&self) -> String {
238 format!(
239 "{}{}",
240 self.0 .0.to_pascal_case(),
241 self.0 .1.to_pascal_case()
242 )
243 }
244
245 pub fn to_snake_case(&self) -> String {
246 format!(
247 "{}_{}",
248 self.0 .0.to_snake_case(),
249 self.0 .1.to_snake_case()
250 )
251 }
252
253 pub fn to_string_with_double_colon(&self) -> String {
254 format!("{}::{}", self.0 .0, self.0 .1)
255 }
256
257 pub fn to_string_with_colon(&self) -> String {
258 format!("{}:{}", self.0 .0, self.0 .1)
259 }
260
261 pub fn to_string_with_slash(&self) -> String {
262 format!("{}/{}", self.0 .0, self.0 .1)
263 }
264
265 pub fn to_kebab_case(&self) -> String {
266 format!("{}-{}", self.0 .0, self.0 .1)
267 }
268
269 pub fn to_rust_binding(&self) -> String {
270 format!(
271 "{}::{}",
272 self.0 .0.to_snake_case(),
273 self.0 .1.to_snake_case()
274 )
275 }
276
277 pub fn namespace(&self) -> String {
278 self.0 .0.to_string()
279 }
280
281 pub fn namespace_title_case(&self) -> String {
282 self.0 .0.to_title_case()
283 }
284
285 pub fn namespace_snake_case(&self) -> String {
286 self.0 .0.to_snake_case()
287 }
288
289 pub fn name_snake_case(&self) -> String {
290 self.0 .1.to_snake_case()
291 }
292}
293
294impl fmt::Display for PackageName {
295 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
296 write!(f, "{}", self.to_string_with_colon())
297 }
298}
299
300impl FromStr for PackageName {
301 type Err = String;
302
303 fn from_str(s: &str) -> Result<Self, Self::Err> {
304 PackageName::from_string(s).ok_or(format!(
305 "Unexpected package name {s}. Must be in 'pack:name' format"
306 ))
307 }
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
311pub struct TemplateName(String);
312
313impl TemplateName {
314 pub fn as_str(&self) -> &str {
315 &self.0
316 }
317}
318
319impl From<&str> for TemplateName {
320 fn from(s: &str) -> Self {
321 TemplateName(s.to_string())
322 }
323}
324
325impl From<String> for TemplateName {
326 fn from(s: String) -> Self {
327 TemplateName(s)
328 }
329}
330
331impl fmt::Display for TemplateName {
332 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
333 write!(f, "{}", self.0)
334 }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
338pub struct ComposableAppGroupName(String);
339
340impl ComposableAppGroupName {
341 pub fn as_str(&self) -> &str {
342 &self.0
343 }
344}
345
346impl Default for ComposableAppGroupName {
347 fn default() -> Self {
348 ComposableAppGroupName("default".to_string())
349 }
350}
351
352impl From<&str> for ComposableAppGroupName {
353 fn from(s: &str) -> Self {
354 ComposableAppGroupName(s.to_string())
355 }
356}
357
358impl From<String> for ComposableAppGroupName {
359 fn from(s: String) -> Self {
360 ComposableAppGroupName(s)
361 }
362}
363
364impl fmt::Display for ComposableAppGroupName {
365 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
366 write!(f, "{}", self.0)
367 }
368}
369
370#[derive(Debug, Copy, Clone)]
371pub enum TargetExistsResolveMode {
372 Skip,
373 MergeOrSkip,
374 Fail,
375 MergeOrFail,
376}
377
378pub type MergeContents = Box<dyn FnOnce(&[u8]) -> io::Result<Vec<u8>>>;
379
380pub enum TargetExistsResolveDecision {
381 Skip,
382 Merge(MergeContents),
383}
384
385#[derive(Debug, Clone)]
386pub struct Template {
387 pub name: TemplateName,
388 pub kind: TemplateKind,
389 pub language: GuestLanguage,
390 pub description: String,
391 pub template_path: PathBuf,
392 pub instructions: String,
393 pub wit_deps: Vec<PathBuf>,
394 pub wit_deps_targets: Option<Vec<PathBuf>>,
395 pub exclude: HashSet<String>,
396 pub transform_exclude: HashSet<String>,
397 pub transform: bool,
398 pub dev_only: bool,
399}
400
401#[derive(Debug, Clone)]
402pub struct TemplateParameters {
403 pub component_name: ComponentName,
404 pub package_name: PackageName,
405 pub target_path: PathBuf,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub(crate) struct TemplateMetadata {
410 pub description: String,
411 #[serde(rename = "appCommonGroup")]
412 pub app_common_group: Option<String>,
413 #[serde(rename = "appCommonSkipIfExists")]
414 pub app_common_skip_if_exists: Option<String>,
415 #[serde(rename = "appComponentGroup")]
416 pub app_component_group: Option<String>,
417 #[serde(rename = "requiresGolemHostWIT")]
418 pub requires_golem_host_wit: Option<bool>,
419 #[serde(rename = "requiresWASI")]
420 pub requires_wasi: Option<bool>,
421 #[serde(rename = "witDepsPaths")]
422 pub wit_deps_paths: Option<Vec<String>>,
423 pub exclude: Option<Vec<String>>,
424 pub instructions: Option<String>,
425 #[serde(rename = "transformExclude")]
426 pub transform_exclude: Option<Vec<String>>,
427 pub transform: Option<bool>,
428 #[serde(rename = "devOnly")]
429 pub dev_only: Option<bool>,
430}
431
432#[cfg(test)]
433mod tests {
434 use crate::model::{ComponentName, PackageName};
435 use test_r::test;
436
437 #[allow(dead_code)]
438 fn n1() -> ComponentName {
439 "my-test-component".into()
440 }
441
442 #[allow(dead_code)]
443 fn n2() -> ComponentName {
444 "MyTestComponent".into()
445 }
446
447 #[allow(dead_code)]
448 fn n3() -> ComponentName {
449 "myTestComponent".into()
450 }
451
452 #[allow(dead_code)]
453 fn n4() -> ComponentName {
454 "my_test_component".into()
455 }
456
457 #[test]
458 pub fn component_name_to_pascal_case() {
459 assert_eq!(n1().to_pascal_case(), "MyTestComponent");
460 assert_eq!(n2().to_pascal_case(), "MyTestComponent");
461 assert_eq!(n3().to_pascal_case(), "MyTestComponent");
462 assert_eq!(n4().to_pascal_case(), "MyTestComponent");
463 }
464
465 #[test]
466 pub fn component_name_to_camel_case() {
467 assert_eq!(n1().to_camel_case(), "myTestComponent");
468 assert_eq!(n2().to_camel_case(), "myTestComponent");
469 assert_eq!(n3().to_camel_case(), "myTestComponent");
470 assert_eq!(n4().to_camel_case(), "myTestComponent");
471 }
472
473 #[test]
474 pub fn component_name_to_snake_case() {
475 assert_eq!(n1().to_snake_case(), "my_test_component");
476 assert_eq!(n2().to_snake_case(), "my_test_component");
477 assert_eq!(n3().to_snake_case(), "my_test_component");
478 assert_eq!(n4().to_snake_case(), "my_test_component");
479 }
480
481 #[test]
482 pub fn component_name_to_kebab_case() {
483 assert_eq!(n1().to_kebab_case(), "my-test-component");
484 assert_eq!(n2().to_kebab_case(), "my-test-component");
485 assert_eq!(n3().to_kebab_case(), "my-test-component");
486 assert_eq!(n4().to_kebab_case(), "my-test-component");
487 }
488
489 #[allow(dead_code)]
490 fn p1() -> PackageName {
491 PackageName::from_string("foo:bar").unwrap()
492 }
493
494 #[allow(dead_code)]
495 fn p2() -> PackageName {
496 PackageName::from_string("foo:bar-baz").unwrap()
497 }
498
499 #[test]
500 pub fn package_name_to_pascal_case() {
501 assert_eq!(p1().to_pascal_case(), "FooBar");
502 assert_eq!(p2().to_pascal_case(), "FooBarBaz");
503 }
504
505 #[test]
506 pub fn package_name_with_number() {
507 assert_eq!(
508 PackageName::from_string("example:demo1")
509 .unwrap()
510 .to_rust_binding(),
511 "example::demo1"
512 )
513 }
514}