Skip to main content

stormchaser_cli/commands/
rules.rs

1use crate::utils::{handle_response, parse_key_val_list, require_token};
2use anyhow::Result;
3use clap::Subcommand;
4use serde_json::json;
5
6#[derive(Subcommand)]
7pub enum RuleCommands {
8    /// List event rules
9    List,
10    /// Create a new event rule
11    Create {
12        name: String,
13        #[arg(long)]
14        webhook_id: stormchaser_model::WebhookId,
15        #[arg(long)]
16        event_pattern: String,
17        #[arg(long)]
18        workflow: String,
19        #[arg(long)]
20        repo: String,
21        #[arg(long)]
22        path: String,
23        #[arg(long, default_value = "main")]
24        git_ref: String,
25        #[arg(long)]
26        description: Option<String>,
27        /// Input mappings in name=CEL_EXPR format
28        #[arg(short, long)]
29        mapping: Vec<String>,
30    },
31    /// Delete an event rule
32    Delete { id: stormchaser_model::RuleId },
33}
34
35pub async fn handle(
36    url: &str,
37    token: Option<&str>,
38    http_client: &reqwest_middleware::ClientWithMiddleware,
39    command: RuleCommands,
40) -> Result<()> {
41    match command {
42        RuleCommands::List => {
43            let token = require_token(token)?;
44            let res = http_client
45                .get(format!("{}/api/v1/rules", url))
46                .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
47                .send()
48                .await?;
49            handle_response(res).await?;
50        }
51        RuleCommands::Create {
52            name,
53            webhook_id,
54            event_pattern,
55            workflow,
56            repo,
57            path,
58            git_ref,
59            description,
60            mapping,
61        } => {
62            let mappings = parse_key_val_list(mapping);
63            let token = require_token(token)?;
64            let res = http_client
65                .post(format!("{}/api/v1/rules", url))
66                .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
67                .json(&json!({
68                    "name": name,
69                    "webhook_id": webhook_id,
70                    "event_type_pattern": event_pattern,
71                    "workflow_name": workflow,
72                    "repo_url": repo,
73                    "workflow_path": path,
74                    "git_ref": git_ref,
75                    "description": description,
76                    "input_mappings": mappings,
77                }))
78                .send()
79                .await?;
80            handle_response(res).await?;
81        }
82        RuleCommands::Delete { id } => {
83            let token = require_token(token)?;
84            let res = http_client
85                .delete(format!("{}/api/v1/rules/{}", url, id))
86                .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
87                .send()
88                .await?;
89            handle_response(res).await?;
90        }
91    }
92    Ok(())
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use reqwest_middleware::ClientBuilder;
99    use wiremock::matchers::{header, method, path};
100    use wiremock::{Mock, MockServer, ResponseTemplate};
101
102    #[tokio::test]
103    async fn test_rules_list() {
104        let server = MockServer::start().await;
105        Mock::given(method("GET"))
106            .and(path("/api/v1/rules"))
107            .and(header(reqwest::header::AUTHORIZATION, "Bearer test-token"))
108            .respond_with(ResponseTemplate::new(200).set_body_json(json!([])))
109            .mount(&server)
110            .await;
111
112        let client = ClientBuilder::new(reqwest::Client::new()).build();
113        let cmd = RuleCommands::List;
114
115        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
116        assert!(result.is_ok());
117    }
118
119    #[tokio::test]
120    async fn test_rules_create() {
121        let server = MockServer::start().await;
122        Mock::given(method("POST"))
123            .and(path("/api/v1/rules"))
124            .and(header(reqwest::header::AUTHORIZATION, "Bearer test-token"))
125            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "created"})))
126            .mount(&server)
127            .await;
128
129        let client = ClientBuilder::new(reqwest::Client::new()).build();
130        let cmd = RuleCommands::Create {
131            name: "test-rule".to_string(),
132            webhook_id: stormchaser_model::WebhookId::new_v4(),
133            event_pattern: "push".to_string(),
134            workflow: "my-wf".to_string(),
135            repo: "https://github.com/a/b".to_string(),
136            path: "wf.storm".to_string(),
137            git_ref: "main".to_string(),
138            description: Some("desc".to_string()),
139            mapping: vec!["key=value".to_string()],
140        };
141
142        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
143        assert!(result.is_ok());
144    }
145
146    #[tokio::test]
147    async fn test_rules_delete() {
148        let server = MockServer::start().await;
149        let id = stormchaser_model::RuleId::new_v4();
150        Mock::given(method("DELETE"))
151            .and(path(format!("/api/v1/rules/{}", id)))
152            .and(header(reqwest::header::AUTHORIZATION, "Bearer test-token"))
153            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "deleted"})))
154            .mount(&server)
155            .await;
156
157        let client = ClientBuilder::new(reqwest::Client::new()).build();
158        let cmd = RuleCommands::Delete { id };
159
160        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
161        assert!(result.is_ok());
162    }
163}