golem_examples/
model.rs

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}