1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::Path;
4
5use crate::Result;
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct Feature {
9 pub name: String,
10 #[serde(default)]
11 pub env: HashMap<String, String>,
12 #[serde(default)]
13 pub containers: HashMap<String, String>,
14 #[serde(default)]
15 pub background: Vec<Step>,
16 pub scenarios: Vec<Scenario>,
17}
18
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct Scenario {
21 pub name: String,
22 pub steps: Vec<Step>,
23}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct Step {
27 #[serde(default)]
28 pub name: String,
29 #[serde(default)]
30 pub id: Option<String>,
31 pub uses: String,
32 #[serde(default)]
33 pub with: HashMap<String, serde_json::Value>,
34 #[serde(default, rename = "continue-on-error")]
35 pub continue_on_error: bool,
36 #[serde(default, rename = "pre-assert")]
37 pub pre_assert: Vec<String>,
38 #[serde(default, rename = "post-assert")]
39 pub post_assert: Vec<String>,
40}
41
42impl Feature {
43 pub fn from_yaml(yaml: &str) -> Result<Self> {
44 let feature: Feature = serde_yaml::from_str(yaml)?;
45 Ok(feature)
46 }
47
48 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
49 let content = std::fs::read_to_string(path)?;
50 Self::from_yaml(&content)
51 }
52}
53
54pub fn parse_features(path: impl AsRef<Path>) -> Result<Vec<Feature>> {
55 let path = path.as_ref();
56 let mut features = Vec::new();
57
58 if path.is_file() {
59 features.push(Feature::from_file(path)?);
60 } else if path.is_dir() {
61 for entry in std::fs::read_dir(path)? {
62 let entry = entry?;
63 let path = entry.path();
64 if path.is_file() {
65 let ext = path.extension().and_then(|e| e.to_str());
66 if matches!(ext, Some("yaml") | Some("yml")) {
67 features.push(Feature::from_file(&path)?);
68 }
69 }
70 }
71 }
72
73 Ok(features)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn test_parse_feature() {
82 let yaml = r#"
83name: User Management
84
85env:
86 DB_URL: postgres://localhost/test
87
88containers:
89 postgres: postgres:15
90
91scenarios:
92 - name: Create user
93 steps:
94 - name: Create user
95 id: user
96 uses: user/create
97 with:
98 username: alice
99 email: alice@test.com
100
101 - name: Verify
102 uses: assert/not_empty
103 with:
104 value: ${{ steps.user.outputs.id }}
105"#;
106
107 let feature = Feature::from_yaml(yaml).unwrap();
108 assert_eq!(feature.name, "User Management");
109 assert_eq!(feature.scenarios.len(), 1);
110 assert_eq!(feature.scenarios[0].steps.len(), 2);
111 assert_eq!(feature.scenarios[0].steps[0].uses, "user/create");
112 }
113}