rust_actions/
parser.rs

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}