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