Skip to content

Commit 1dac259

Browse files
refactor: reduce complexity of execute_safe_output in src/execute.rs (#307)
1 parent 8c7646d commit 1dac259

18 files changed

Lines changed: 127 additions & 185 deletions

src/execute.rs

Lines changed: 61 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use anyhow::{Result, bail};
77
use log::{debug, error, info, warn};
8+
use serde::de::DeserializeOwned;
89
use serde_json::Value;
910
use std::collections::HashMap;
1011
use std::path::Path;
@@ -213,6 +214,35 @@ pub async fn execute_safe_outputs(
213214
Ok(results)
214215
}
215216

217+
/// Parse a JSON entry as `T` and run it through `execute_sanitized`.
218+
///
219+
/// This is the common path for all tools that implement `Executor`. The tool name
220+
/// is used only for the error message so callers don't have to repeat it.
221+
async fn dispatch_tool<T>(
222+
tool_name: &str,
223+
entry: &Value,
224+
ctx: &ExecutionContext,
225+
) -> Result<ExecutionResult>
226+
where
227+
T: DeserializeOwned + Executor,
228+
{
229+
debug!("Parsing {} payload", tool_name);
230+
let mut output: T = serde_json::from_value(entry.clone())
231+
.map_err(|e| anyhow::anyhow!("Failed to parse {}: {}", tool_name, e))?;
232+
output.execute_sanitized(ctx).await
233+
}
234+
235+
macro_rules! dispatch_executor_tools {
236+
($tool_name:expr, $entry:expr, $ctx:expr, { $($name:literal => $ty:ty),+ $(,)? }) => {
237+
match $tool_name {
238+
$(
239+
$name => dispatch_tool::<$ty>($tool_name, $entry, $ctx).await.map(Some),
240+
)+
241+
_ => Ok(None),
242+
}
243+
};
244+
}
245+
216246
/// Execute a single safe output entry, returning the tool name and result
217247
pub async fn execute_safe_output(
218248
entry: &Value,
@@ -226,191 +256,49 @@ pub async fn execute_safe_output(
226256

227257
debug!("Dispatching tool: {}", tool_name);
228258

229-
// Dispatch based on tool name
230-
let result = match tool_name {
231-
"create-work-item" => {
232-
debug!("Parsing create-work-item payload");
233-
let mut output: CreateWorkItemResult = serde_json::from_value(entry.clone())
234-
.map_err(|e| anyhow::anyhow!("Failed to parse create-work-item: {}", e))?;
235-
debug!(
236-
"create-work-item: title='{}', description length={}",
237-
output.title,
238-
output.description.len()
239-
);
240-
output.execute_sanitized(ctx).await?
241-
}
242-
"comment-on-work-item" => {
243-
debug!("Parsing comment-on-work-item payload");
244-
let mut output: CommentOnWorkItemResult = serde_json::from_value(entry.clone())
245-
.map_err(|e| anyhow::anyhow!("Failed to parse comment-on-work-item: {}", e))?;
246-
debug!(
247-
"comment-on-work-item: work_item_id={}, body length={}",
248-
output.work_item_id,
249-
output.body.len()
250-
);
251-
output.execute_sanitized(ctx).await?
252-
}
253-
"update-work-item" => {
254-
debug!("Parsing update-work-item payload");
255-
let mut output: UpdateWorkItemResult = serde_json::from_value(entry.clone())
256-
.map_err(|e| anyhow::anyhow!("Failed to parse update-work-item: {}", e))?;
257-
debug!("update-work-item: id={}", output.id);
258-
output.execute_sanitized(ctx).await?
259-
}
260-
"create-pull-request" => {
261-
debug!("Parsing create-pull-request payload");
262-
let mut output: CreatePrResult = serde_json::from_value(entry.clone())
263-
.map_err(|e| anyhow::anyhow!("Failed to parse create-pull-request: {}", e))?;
264-
debug!(
265-
"create-pull-request: title='{}', repo='{}', branch='{}', patch='{}'",
266-
output.title, output.repository, output.source_branch, output.patch_file
267-
);
268-
output.execute_sanitized(ctx).await?
269-
}
270-
"update-wiki-page" => {
271-
debug!("Parsing update-wiki-page payload");
272-
let mut output: UpdateWikiPageResult = serde_json::from_value(entry.clone())
273-
.map_err(|e| anyhow::anyhow!("Failed to parse update-wiki-page: {}", e))?;
274-
debug!(
275-
"update-wiki-page: path='{}', content length={}",
276-
output.path,
277-
output.content.len()
278-
);
279-
output.execute_sanitized(ctx).await?
280-
}
281-
"create-wiki-page" => {
282-
debug!("Parsing create-wiki-page payload");
283-
let mut output: CreateWikiPageResult = serde_json::from_value(entry.clone())
284-
.map_err(|e| anyhow::anyhow!("Failed to parse create-wiki-page: {}", e))?;
285-
debug!(
286-
"create-wiki-page: path='{}', content length={}",
287-
output.path,
288-
output.content.len()
289-
);
290-
output.execute_sanitized(ctx).await?
291-
}
292-
"add-pr-comment" => {
293-
debug!("Parsing add-pr-comment payload");
294-
let mut output: AddPrCommentResult = serde_json::from_value(entry.clone())
295-
.map_err(|e| anyhow::anyhow!("Failed to parse add-pr-comment: {}", e))?;
296-
debug!(
297-
"add-pr-comment: pr_id={}, content length={}",
298-
output.pull_request_id,
299-
output.content.len()
300-
);
301-
output.execute_sanitized(ctx).await?
302-
}
303-
"link-work-items" => {
304-
debug!("Parsing link-work-items payload");
305-
let mut output: LinkWorkItemsResult = serde_json::from_value(entry.clone())
306-
.map_err(|e| anyhow::anyhow!("Failed to parse link-work-items: {}", e))?;
307-
debug!(
308-
"link-work-items: source={}, target={}, type={}",
309-
output.source_id, output.target_id, output.link_type
310-
);
311-
output.execute_sanitized(ctx).await?
312-
}
313-
"queue-build" => {
314-
debug!("Parsing queue-build payload");
315-
let mut output: QueueBuildResult = serde_json::from_value(entry.clone())
316-
.map_err(|e| anyhow::anyhow!("Failed to parse queue-build: {}", e))?;
317-
debug!("queue-build: pipeline_id={}", output.pipeline_id);
318-
output.execute_sanitized(ctx).await?
319-
}
320-
"create-git-tag" => {
321-
debug!("Parsing create-git-tag payload");
322-
let mut output: CreateGitTagResult = serde_json::from_value(entry.clone())
323-
.map_err(|e| anyhow::anyhow!("Failed to parse create-git-tag: {}", e))?;
324-
debug!("create-git-tag: tag_name='{}'", output.tag_name);
325-
output.execute_sanitized(ctx).await?
326-
}
327-
"add-build-tag" => {
328-
debug!("Parsing add-build-tag payload");
329-
let mut output: AddBuildTagResult = serde_json::from_value(entry.clone())
330-
.map_err(|e| anyhow::anyhow!("Failed to parse add-build-tag: {}", e))?;
331-
debug!("add-build-tag: build_id={}, tag='{}'", output.build_id, output.tag);
332-
output.execute_sanitized(ctx).await?
333-
}
334-
"create-branch" => {
335-
debug!("Parsing create-branch payload");
336-
let mut output: CreateBranchResult = serde_json::from_value(entry.clone())
337-
.map_err(|e| anyhow::anyhow!("Failed to parse create-branch: {}", e))?;
338-
debug!("create-branch: branch_name='{}'", output.branch_name);
339-
output.execute_sanitized(ctx).await?
340-
}
341-
"update-pr" => {
342-
debug!("Parsing update-pr payload");
343-
let mut output: UpdatePrResult = serde_json::from_value(entry.clone())
344-
.map_err(|e| anyhow::anyhow!("Failed to parse update-pr: {}", e))?;
345-
debug!(
346-
"update-pr: pr_id={}, operation='{}'",
347-
output.pull_request_id, output.operation
348-
);
349-
output.execute_sanitized(ctx).await?
350-
}
351-
"upload-attachment" => {
352-
debug!("Parsing upload-attachment payload");
353-
let mut output: UploadAttachmentResult = serde_json::from_value(entry.clone())
354-
.map_err(|e| anyhow::anyhow!("Failed to parse upload-attachment: {}", e))?;
355-
debug!(
356-
"upload-attachment: work_item_id={}, file_path='{}'",
357-
output.work_item_id, output.file_path
358-
);
359-
output.execute_sanitized(ctx).await?
360-
}
361-
"submit-pr-review" => {
362-
debug!("Parsing submit-pr-review payload");
363-
let mut output: SubmitPrReviewResult = serde_json::from_value(entry.clone())
364-
.map_err(|e| anyhow::anyhow!("Failed to parse submit-pr-review: {}", e))?;
365-
debug!(
366-
"submit-pr-review: pr_id={}, event='{}'",
367-
output.pull_request_id, output.event
368-
);
369-
output.execute_sanitized(ctx).await?
370-
}
371-
"reply-to-pr-review-comment" => {
372-
debug!("Parsing reply-to-pr-review-comment payload");
373-
let mut output: ReplyToPrCommentResult = serde_json::from_value(entry.clone())
374-
.map_err(|e| anyhow::anyhow!("Failed to parse reply-to-pr-review-comment: {}", e))?;
375-
debug!(
376-
"reply-to-pr-review-comment: pr_id={}, thread_id={}",
377-
output.pull_request_id, output.thread_id
378-
);
379-
output.execute_sanitized(ctx).await?
380-
}
381-
"resolve-pr-thread" => {
382-
debug!("Parsing resolve-pr-thread payload");
383-
let mut output: ResolvePrThreadResult = serde_json::from_value(entry.clone())
384-
.map_err(|e| anyhow::anyhow!("Failed to parse resolve-pr-thread: {}", e))?;
385-
debug!(
386-
"resolve-pr-thread: pr_id={}, thread_id={}, status='{}'",
387-
output.pull_request_id, output.thread_id, output.status
388-
);
389-
output.execute_sanitized(ctx).await?
390-
}
391-
"noop" => {
392-
debug!("Skipping noop entry");
393-
ExecutionResult::success("Skipped informational output: noop")
259+
// Dispatch based on tool name. All standard tools go through `dispatch_tool` which
260+
// handles deserialization and sanitized execution uniformly. Special cases (informational
261+
// outputs and report-incomplete) are handled inline.
262+
let result = if let Some(dispatched_result) = dispatch_executor_tools!(tool_name, entry, ctx, {
263+
"create-work-item" => CreateWorkItemResult,
264+
"comment-on-work-item" => CommentOnWorkItemResult,
265+
"update-work-item" => UpdateWorkItemResult,
266+
"create-pull-request" => CreatePrResult,
267+
"update-wiki-page" => UpdateWikiPageResult,
268+
"create-wiki-page" => CreateWikiPageResult,
269+
"add-pr-comment" => AddPrCommentResult,
270+
"link-work-items" => LinkWorkItemsResult,
271+
"queue-build" => QueueBuildResult,
272+
"create-git-tag" => CreateGitTagResult,
273+
"add-build-tag" => AddBuildTagResult,
274+
"create-branch" => CreateBranchResult,
275+
"update-pr" => UpdatePrResult,
276+
"upload-attachment" => UploadAttachmentResult,
277+
"submit-pr-review" => SubmitPrReviewResult,
278+
"reply-to-pr-review-comment" => ReplyToPrCommentResult,
279+
"resolve-pr-thread" => ResolvePrThreadResult,
280+
})? {
281+
dispatched_result
282+
} else {
283+
match tool_name {
284+
// Informational outputs — no side effects, always succeed
285+
"noop" | "missing-tool" | "missing-data" => {
286+
debug!("Skipping informational entry: {}", tool_name);
287+
ExecutionResult::success(format!("Skipped informational output: {}", tool_name))
394288
}
289+
// report-incomplete does not implement Executor; Stage 3 surfaces its reason as a failure
395290
"report-incomplete" => {
396291
let mut output: ReportIncompleteResult = serde_json::from_value(entry.clone())
397292
.map_err(|e| anyhow::anyhow!("Failed to parse report-incomplete: {}", e))?;
398293
output.sanitize_content_fields();
399294
debug!("report-incomplete: {}", output.reason);
400295
ExecutionResult::failure(format!("Agent reported task incomplete: {}", output.reason))
401296
}
402-
"missing-tool" => {
403-
debug!("Skipping missing-tool entry");
404-
ExecutionResult::success("Skipped informational output: missing-tool")
405-
}
406-
"missing-data" => {
407-
debug!("Skipping missing-data entry");
408-
ExecutionResult::success("Skipped informational output: missing-data")
409-
}
410297
other => {
411298
error!("Unknown tool type: {}", other);
412299
bail!("Unknown tool type: {}. No executor registered.", other)
413300
}
301+
}
414302
};
415303

416304
Ok((tool_name.to_string(), result))

src/safeoutputs/add_build_tag.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl Executor for AddBuildTagResult {
112112

113113
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
114114
info!("Adding tag '{}' to build #{}", self.tag, self.build_id);
115+
debug!("add-build-tag: build_id={}, tag='{}'", self.build_id, self.tag);
115116

116117
// 1. Extract required context
117118
let org_url = ctx

src/safeoutputs/add_pr_comment.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ impl Executor for AddPrCommentResult {
208208
self.pull_request_id,
209209
self.content.len()
210210
);
211+
debug!(
212+
"add-pr-comment: pr_id={}, content length={}",
213+
self.pull_request_id,
214+
self.content.len()
215+
);
211216

212217
let org_url = ctx
213218
.ado_org_url
@@ -625,4 +630,3 @@ allowed-statuses:
625630
);
626631
}
627632
}
628-

src/safeoutputs/comment_on_work_item.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,19 @@ impl Executor for CommentOnWorkItemResult {
170170
format!("comment on work item #{}", self.work_item_id)
171171
}
172172

173-
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
174-
info!(
175-
"Commenting on work item #{}: {} chars",
176-
self.work_item_id,
177-
self.body.len()
178-
);
179-
180-
let org_url = ctx
173+
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
174+
info!(
175+
"Commenting on work item #{}: {} chars",
176+
self.work_item_id,
177+
self.body.len()
178+
);
179+
debug!(
180+
"comment-on-work-item: work_item_id={}, body length={}",
181+
self.work_item_id,
182+
self.body.len()
183+
);
184+
185+
let org_url = ctx
181186
.ado_org_url
182187
.as_ref()
183188
.context("AZURE_DEVOPS_ORG_URL not set")?;

src/safeoutputs/create_branch.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ impl Executor for CreateBranchResult {
201201

202202
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
203203
info!("Creating branch: '{}'", self.branch_name);
204+
debug!("create-branch: branch_name='{}'", self.branch_name);
204205

205206
let org_url = ctx
206207
.ado_org_url

src/safeoutputs/create_git_tag.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ impl Executor for CreateGitTagResult {
244244

245245
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
246246
info!("Creating git tag: '{}'", self.tag_name);
247+
debug!("create-git-tag: tag_name='{}'", self.tag_name);
247248

248249
let org_url = ctx
249250
.ado_org_url

src/safeoutputs/create_pr.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,10 @@ impl Executor for CreatePrResult {
541541
"Creating PR: '{}' in repository '{}'",
542542
self.title, self.repository
543543
);
544+
debug!(
545+
"create-pull-request: title='{}', repo='{}', branch='{}', patch='{}'",
546+
self.title, self.repository, self.source_branch, self.patch_file
547+
);
544548
debug!("PR description length: {} chars", self.description.len());
545549
debug!("Source branch: {}", self.source_branch);
546550
debug!("Patch file: {}", self.patch_file);

src/safeoutputs/create_wiki_page.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ impl Executor for CreateWikiPageResult {
192192

193193
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
194194
info!("Creating wiki page: '{}'", self.path);
195+
debug!(
196+
"create-wiki-page: path='{}', content length={}",
197+
self.path,
198+
self.content.len()
199+
);
195200
debug!("Content length: {} chars", self.content.len());
196201

197202
let org_url = ctx
@@ -922,4 +927,3 @@ wiki-name: "MyProject.wiki"
922927
assert_eq!(encoded, "MyProject.wiki");
923928
}
924929
}
925-

src/safeoutputs/create_work_item.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ impl Executor for CreateWorkItemResult {
243243

244244
async fn execute_impl(&self, ctx: &ExecutionContext) -> anyhow::Result<ExecutionResult> {
245245
info!("Creating work item: '{}'", self.title);
246+
debug!(
247+
"create-work-item: title='{}', description length={}",
248+
self.title,
249+
self.description.len()
250+
);
246251
debug!("Description length: {} chars", self.description.len());
247252

248253
let org_url = ctx
@@ -538,4 +543,3 @@ tags:
538543
assert_eq!(config.tags, vec!["my-tag"]);
539544
}
540545
}
541-

src/safeoutputs/link_work_items.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ impl Executor for LinkWorkItemsResult {
145145
"Linking work item #{} -> #{} ({})",
146146
self.source_id, self.target_id, self.link_type
147147
);
148+
debug!(
149+
"link-work-items: source={}, target={}, type={}",
150+
self.source_id, self.target_id, self.link_type
151+
);
148152

149153
let org_url = ctx
150154
.ado_org_url

0 commit comments

Comments
 (0)