ironflow_api/routes/
get_workflow.rs1use std::collections::HashSet;
4
5use axum::extract::{Path, State};
6use axum::response::IntoResponse;
7use ironflow_auth::extractor::Authenticated;
8use serde::Serialize;
9
10use crate::error::ApiError;
11use crate::response::ok;
12use crate::state::AppState;
13
14#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
16#[derive(Debug, Serialize)]
17pub struct SubWorkflowDetail {
18 pub name: String,
20 pub description: String,
22 pub source_code: Option<String>,
24}
25
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
28#[derive(Debug, Serialize)]
29pub struct WorkflowDetailResponse {
30 pub name: String,
32 pub description: String,
34 pub source_code: Option<String>,
36 pub sub_workflows: Vec<SubWorkflowDetail>,
38}
39
40#[cfg_attr(
46 feature = "openapi",
47 utoipa::path(
48 get,
49 path = "/api/v1/workflows/{name}",
50 tags = ["workflows"],
51 params(("name" = String, Path, description = "Workflow name")),
52 responses(
53 (status = 200, description = "Workflow details", body = WorkflowDetailResponse),
54 (status = 401, description = "Unauthorized"),
55 (status = 404, description = "Workflow not found")
56 ),
57 security(("Bearer" = []))
58 )
59)]
60pub async fn get_workflow(
61 _auth: Authenticated,
62 State(state): State<AppState>,
63 Path(name): Path<String>,
64) -> Result<impl IntoResponse, ApiError> {
65 let info = state
66 .engine
67 .handler_info(&name)
68 .ok_or_else(|| ApiError::WorkflowNotFound(name.clone()))?;
69
70 let mut sub_workflows = Vec::new();
71 let mut visited = HashSet::new();
72 visited.insert(name.clone());
73 collect_sub_workflows(
74 &state,
75 &info.sub_workflows,
76 &mut sub_workflows,
77 &mut visited,
78 5,
79 );
80
81 Ok(ok(WorkflowDetailResponse {
82 name,
83 description: info.description,
84 source_code: info.source_code,
85 sub_workflows,
86 }))
87}
88
89fn collect_sub_workflows(
90 state: &AppState,
91 names: &[String],
92 result: &mut Vec<SubWorkflowDetail>,
93 visited: &mut HashSet<String>,
94 depth: usize,
95) {
96 if depth == 0 {
97 return;
98 }
99 for sub_name in names {
100 if !visited.insert(sub_name.clone()) {
101 continue;
102 }
103 if let Some(sub_info) = state.engine.handler_info(sub_name) {
104 collect_sub_workflows(state, &sub_info.sub_workflows, result, visited, depth - 1);
105 result.push(SubWorkflowDetail {
106 name: sub_name.clone(),
107 description: sub_info.description,
108 source_code: sub_info.source_code,
109 });
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use axum::Router;
117 use axum::body::Body;
118 use axum::http::{Request, StatusCode};
119 use axum::routing::get;
120 use http_body_util::BodyExt;
121 use ironflow_auth::jwt::AccessToken;
122 use ironflow_core::providers::claude::ClaudeCodeProvider;
123 use ironflow_engine::context::WorkflowContext;
124 use ironflow_engine::engine::Engine;
125 use ironflow_engine::handler::{HandlerFuture, WorkflowHandler};
126 use ironflow_engine::notify::Event;
127 use ironflow_store::api_key_store::ApiKeyStore;
128 use ironflow_store::memory::InMemoryStore;
129 use serde_json::Value as JsonValue;
130 use std::sync::Arc;
131 use tokio::sync::broadcast;
132 use tower::ServiceExt;
133 use uuid::Uuid;
134
135 use super::*;
136
137 struct DescribedWorkflow;
138 impl WorkflowHandler for DescribedWorkflow {
139 fn name(&self) -> &str {
140 "my-workflow"
141 }
142 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
143 Box::pin(async move { Ok(()) })
144 }
145 }
146
147 fn test_state() -> AppState {
148 let store = Arc::new(InMemoryStore::new());
149 let user_store: Arc<dyn ironflow_store::user_store::UserStore> =
150 Arc::new(InMemoryStore::new());
151 let api_key_store: Arc<dyn ApiKeyStore> = Arc::new(InMemoryStore::new());
152 let provider = Arc::new(ClaudeCodeProvider::new());
153 let mut engine = Engine::new(store.clone(), provider);
154 engine.register(DescribedWorkflow).unwrap();
155 let jwt_config = Arc::new(ironflow_auth::jwt::JwtConfig {
156 secret: "test-secret".to_string(),
157 access_token_ttl_secs: 900,
158 refresh_token_ttl_secs: 604800,
159 cookie_domain: None,
160 cookie_secure: false,
161 });
162 let (event_sender, _) = broadcast::channel::<Event>(1);
163 AppState::new(
164 store,
165 user_store,
166 api_key_store,
167 Arc::new(engine),
168 jwt_config,
169 "test-worker-token".to_string(),
170 event_sender,
171 )
172 }
173
174 fn make_auth_header(state: &AppState) -> String {
175 let user_id = Uuid::now_v7();
176 let token = AccessToken::for_user(user_id, "testuser", false, &state.jwt_config).unwrap();
177 format!("Bearer {}", token.0)
178 }
179
180 #[tokio::test]
181 async fn get_workflow_found() {
182 let state = test_state();
183 let auth_header = make_auth_header(&state);
184 let app = Router::new()
185 .route("/{name}", get(get_workflow))
186 .with_state(state);
187
188 let req = Request::builder()
189 .uri("/my-workflow")
190 .header("authorization", auth_header)
191 .body(Body::empty())
192 .unwrap();
193
194 let resp = app.oneshot(req).await.unwrap();
195 assert_eq!(resp.status(), StatusCode::OK);
196
197 let body = resp.into_body().collect().await.unwrap().to_bytes();
198 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
199 assert_eq!(json_val["data"]["name"], "my-workflow");
200 }
201
202 #[tokio::test]
203 async fn get_workflow_not_found() {
204 let state = test_state();
205 let auth_header = make_auth_header(&state);
206 let app = Router::new()
207 .route("/{name}", get(get_workflow))
208 .with_state(state);
209
210 let req = Request::builder()
211 .uri("/nonexistent")
212 .header("authorization", auth_header)
213 .body(Body::empty())
214 .unwrap();
215
216 let resp = app.oneshot(req).await.unwrap();
217 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
218 }
219}