Files
IdeA/crates/app-tauri/src/tickets.rs
Blomios 6da52c6a02 wip(tickets): backend createdBy (#5) + bulk status/priority/delete (#6)
Ajoute created_by sur IssueIndexEntry/TicketSummaryDto + filtre createdBy
(#5), et les use cases BulkUpdateIssueStatus/Priority + BulkDeleteIssues
avec les DTO/outils MCP idea_ticket_bulk_* associés (#6).

NON VERT : BulkDeleteIssues::execute référence BatchIssueResult::deleted(),
constructeur pas encore ajouté à BatchIssueResult (seuls ok/err existent) ->
`cargo check` échoue (E0599 dans application::issues::mod). Reste à
DevBackend avant toute QA/merge.

Branché sur feature/sdk-integration (dépend du commit #2 dans
orchestrator/mcp/server.rs et tools.rs) ; à rebaser sur develop une fois
feature/sdk-integration mergé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 18:28:39 +02:00

865 lines
28 KiB
Rust

//! Public ticket adapters (Tauri commands + MCP provider).
//!
//! Public wire names use `ticket`; the application/domain model remains `Issue`.
#![allow(missing_docs)]
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use tauri::State;
use application::{
AppError, AssignIssueAgentInput, AssignTicketToSprintInput, CloseTicketAssistantInput,
CreateSprintInput, DeleteIssueInput, DeleteSprintInput, LinkIssuesInput, ListIssuesInput,
ListSprintsInput, OpenProjectInput, OpenTicketAssistantInput, ReadIssueCarnetInput,
ReadIssueInput, RenameSprintInput, ReorderSprintsInput, UnassignTicketFromSprintInput,
UnlinkIssuesInput, UpdateIssueCarnetInput, UpdateIssueInput,
};
use domain::{IssueActor, IssueRef, Project};
use infrastructure::{TicketToolError, TicketToolProvider};
use crate::dto::*;
use crate::state::AppState;
#[derive(Clone)]
pub struct AppTicketToolProvider {
pub create: Arc<application::CreateIssue>,
pub read: Arc<application::ReadIssue>,
pub list: Arc<application::ListIssues>,
pub update: Arc<application::UpdateIssue>,
pub bulk_update_status: Arc<application::BulkUpdateIssueStatus>,
pub bulk_update_priority: Arc<application::BulkUpdateIssuePriority>,
pub read_carnet: Arc<application::ReadIssueCarnet>,
pub update_carnet: Arc<application::UpdateIssueCarnet>,
pub link: Arc<application::LinkIssues>,
pub unlink: Arc<application::UnlinkIssues>,
pub list_sprints: Arc<application::ListSprints>,
}
#[async_trait]
impl TicketToolProvider for AppTicketToolProvider {
async fn handle_ticket_tool(
&self,
project: &Project,
requester: &str,
name: &str,
arguments: Value,
) -> Result<Value, TicketToolError> {
let actor = actor_from_requester(requester);
let result = match name {
"idea_ticket_create" => {
let req = mcp_create_request(project, arguments)?;
let issue = self
.create
.execute(create_input(project.clone(), req, actor).map_err(dto_tool_error)?)
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(issue, None))
}
"idea_ticket_read" => {
let issue_ref = parse_json_ref(&arguments, "ref")?;
let include_carnet = arguments
.get("includeCarnet")
.and_then(Value::as_bool)
.unwrap_or(false);
let issue = self
.read
.execute(ReadIssueInput {
project: project.clone(),
issue_ref,
})
.await
.map_err(ticket_error)?
.issue;
let fallback_carnet = issue.carnet.as_str().to_owned();
let carnet = if include_carnet {
Some(
read_ticket_carnet_body_best_effort(
&*self.read_carnet,
project,
issue_ref,
fallback_carnet,
)
.await
.map_err(ticket_error)?,
)
} else {
None
};
let sprints = self
.list_sprints
.execute(ListSprintsInput {
project: project.clone(),
})
.await
.map_err(ticket_error)?
.sprints;
json!(TicketDto::from_issue_with_sprints(issue, carnet, &sprints))
}
"idea_ticket_list" => {
let req = mcp_list_request(project, arguments)?;
let mut rows = self
.list
.execute(ListIssuesInput {
project: project.clone(),
filter: req.filter,
})
.await
.map_err(ticket_error)?
.issues;
sort_ticket_rows(&mut rows, req.sort);
let sprints = self
.list_sprints
.execute(ListSprintsInput {
project: project.clone(),
})
.await
.map_err(ticket_error)?
.sprints;
let page = paginate_with_sprints(rows, req.limit, req.cursor, req.sort, &sprints)
.map_err(|e| TicketToolError::new("invalid", e.message))?;
json!(page)
}
"idea_sprint_list" => {
let rows = self
.list_sprints
.execute(ListSprintsInput {
project: project.clone(),
})
.await
.map_err(ticket_error)?
.sprints;
json!(SprintListDto::from(rows))
}
"idea_ticket_update" => {
let req = mcp_update_request(project, arguments)?;
let issue = self
.update
.execute(update_input(project.clone(), req, actor).map_err(dto_tool_error)?)
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(issue, None))
}
"idea_ticket_update_status" => {
let issue = self
.update
.execute(UpdateIssueInput {
project: project.clone(),
issue_ref: parse_json_ref(&arguments, "ref")?,
expected_version: parse_json_version(&arguments)?,
title: None,
description: None,
status: Some(parse_status(required_str(&arguments, "status")?)?),
priority: None,
assigned_agent_ids: None,
actor,
})
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(issue, None))
}
"idea_ticket_update_priority" => {
let issue = self
.update
.execute(UpdateIssueInput {
project: project.clone(),
issue_ref: parse_json_ref(&arguments, "ref")?,
expected_version: parse_json_version(&arguments)?,
title: None,
description: None,
status: None,
priority: Some(parse_priority(required_str(&arguments, "priority")?)?),
assigned_agent_ids: None,
actor,
})
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(issue, None))
}
"idea_ticket_bulk_update_status" => {
let req = mcp_bulk_update_status_request(project, arguments)?;
let out = self
.bulk_update_status
.execute(
bulk_update_status_input(project.clone(), req, actor)
.map_err(dto_tool_error)?,
)
.await;
json!(TicketBulkResultDto::from(out))
}
"idea_ticket_bulk_update_priority" => {
let req = mcp_bulk_update_priority_request(project, arguments)?;
let out = self
.bulk_update_priority
.execute(
bulk_update_priority_input(project.clone(), req, actor)
.map_err(dto_tool_error)?,
)
.await;
json!(TicketBulkResultDto::from(out))
}
"idea_ticket_read_carnet" => {
let carnet = self
.read_carnet
.execute(ReadIssueCarnetInput {
project: project.clone(),
issue_ref: parse_json_ref(&arguments, "ref")?,
})
.await
.map_err(ticket_error)?
.carnet;
json!(TicketCarnetDto::from(carnet))
}
"idea_ticket_update_carnet" => {
let issue_ref = parse_json_ref(&arguments, "ref")?;
self.update_carnet
.execute(UpdateIssueCarnetInput {
project: project.clone(),
issue_ref,
expected_version: parse_json_version(&arguments)?,
carnet: required_str(&arguments, "carnet")?.to_owned(),
actor,
})
.await
.map_err(ticket_error)?;
let issue = self
.read
.execute(ReadIssueInput {
project: project.clone(),
issue_ref,
})
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(
issue,
Some(read_carnet_body(&*self.read_carnet, project, issue_ref).await?)
))
}
"idea_ticket_link" => {
let issue = self
.link
.execute(LinkIssuesInput {
project: project.clone(),
issue_ref: parse_json_ref(&arguments, "ref")?,
target: parse_json_ref(&arguments, "targetRef")?,
kind: parse_link_kind(required_str(&arguments, "kind")?)?,
expected_version: parse_json_version(&arguments)?,
actor,
})
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(issue, None))
}
"idea_ticket_unlink" => {
let kind = arguments
.get("kind")
.and_then(Value::as_str)
.map(parse_link_kind)
.transpose()?;
let issue = self
.unlink
.execute(UnlinkIssuesInput {
project: project.clone(),
issue_ref: parse_json_ref(&arguments, "ref")?,
target: parse_json_ref(&arguments, "targetRef")?,
kind,
expected_version: parse_json_version(&arguments)?,
actor,
})
.await
.map_err(ticket_error)?
.issue;
json!(TicketDto::from_issue(issue, None))
}
_ => {
return Err(TicketToolError::new(
"unknownTool",
format!("unknown tool: {name}"),
))
}
};
Ok(result)
}
}
#[tauri::command]
pub async fn ticket_create(
request: TicketCreateRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue = state
.create_issue
.execute(create_input(project, request, IssueActor::User)?)
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_read(
request: TicketReadRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue_ref = parse_ref_dto(&request.r#ref)?;
let issue = state
.read_issue
.execute(ReadIssueInput {
project: project.clone(),
issue_ref,
})
.await
.map_err(ErrorDto::from)?
.issue;
let fallback_carnet = issue.carnet.as_str().to_owned();
let carnet = if request.include_carnet.unwrap_or(false) {
Some(
read_ticket_carnet_body_best_effort(
&state.read_issue_carnet,
&project,
issue_ref,
fallback_carnet,
)
.await
.map_err(ErrorDto::from)?,
)
} else {
None
};
Ok(TicketDto::from_issue(issue, carnet))
}
#[tauri::command]
pub async fn ticket_delete(
request: TicketDeleteRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
state
.delete_issue
.execute(DeleteIssueInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
})
.await
.map(|_| ())
.map_err(ErrorDto::from)
}
#[tauri::command]
pub async fn ticket_bulk_update_status(
request: TicketBulkUpdateStatusRequestDto,
state: State<'_, AppState>,
) -> Result<TicketBulkResultDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let out = state
.bulk_update_issue_status
.execute(bulk_update_status_input(
project,
request,
IssueActor::User,
)?)
.await;
Ok(TicketBulkResultDto::from(out))
}
#[tauri::command]
pub async fn ticket_bulk_update_priority(
request: TicketBulkUpdatePriorityRequestDto,
state: State<'_, AppState>,
) -> Result<TicketBulkResultDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let out = state
.bulk_update_issue_priority
.execute(bulk_update_priority_input(
project,
request,
IssueActor::User,
)?)
.await;
Ok(TicketBulkResultDto::from(out))
}
#[tauri::command]
pub async fn open_ticket_chat(
request: OpenTicketChatRequestDto,
state: State<'_, AppState>,
) -> Result<TicketChatDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let output = state
.open_ticket_assistant
.execute(OpenTicketAssistantInput {
project,
issue_ref: parse_ref_dto(&request.issue_ref)?,
profile_id: parse_profile_id_dto(&request.profile_id)?,
})
.await
.map_err(ErrorDto::from)?;
Ok(TicketChatDto {
session_id: output.session_id.to_string(),
requester: output.requester,
issue_ref: output.issue_ref.to_string(),
})
}
#[tauri::command]
pub async fn close_ticket_chat(
request: CloseTicketChatRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let _project = resolve_project(&state, &request.project_id).await?;
state
.close_ticket_assistant
.execute(CloseTicketAssistantInput {
issue_ref: parse_ref_dto(&request.issue_ref)?,
})
.await
.map_err(ErrorDto::from)
}
#[tauri::command]
pub async fn ticket_list(
request: TicketListRequestDto,
state: State<'_, AppState>,
) -> Result<TicketListDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let page = TicketListPageInput::from_request(request)?;
let mut rows = state
.list_issues
.execute(ListIssuesInput {
project,
filter: page.filter,
})
.await
.map_err(ErrorDto::from)?
.issues;
sort_ticket_rows(&mut rows, page.sort);
paginate(rows, page.limit, page.cursor, page.sort)
}
#[tauri::command]
pub async fn sprint_create(
request: SprintCreateRequestDto,
state: State<'_, AppState>,
) -> Result<SprintDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let sprint = state
.create_sprint
.execute(CreateSprintInput {
project,
name: request.name,
status: request
.status
.as_deref()
.map(parse_sprint_status_dto)
.transpose()?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.sprint;
Ok(SprintDto::from_sprint(sprint, 0))
}
#[tauri::command]
pub async fn sprint_list(
request: SprintListRequestDto,
state: State<'_, AppState>,
) -> Result<SprintListDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let rows = state
.list_sprints
.execute(ListSprintsInput { project })
.await
.map_err(ErrorDto::from)?
.sprints;
Ok(SprintListDto::from(rows))
}
#[tauri::command]
pub async fn sprint_rename(
request: SprintRenameRequestDto,
state: State<'_, AppState>,
) -> Result<SprintDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let sprint = state
.rename_sprint
.execute(RenameSprintInput {
project,
sprint_id: parse_sprint_id_dto(&request.sprint_id)?,
expected_version: sprint_version_dto(request.expected_version)?,
name: request.name,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.sprint;
Ok(SprintDto::from_sprint(sprint, 0))
}
#[tauri::command]
pub async fn sprint_reorder(
request: SprintReorderRequestDto,
state: State<'_, AppState>,
) -> Result<SprintListDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
state
.reorder_sprints
.execute(ReorderSprintsInput {
project: project.clone(),
ordered_ids: request
.ordered_ids
.iter()
.map(|id| parse_sprint_id_dto(id))
.collect::<Result<Vec<_>, _>>()?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?;
sprint_list(
SprintListRequestDto {
project_id: project.id.to_string(),
},
state,
)
.await
}
#[tauri::command]
pub async fn sprint_delete(
request: SprintDeleteRequestDto,
state: State<'_, AppState>,
) -> Result<(), ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
state
.delete_sprint
.execute(DeleteSprintInput {
project,
sprint_id: parse_sprint_id_dto(&request.sprint_id)?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)
}
#[tauri::command]
pub async fn ticket_assign_sprint(
request: TicketSprintAssignRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue = state
.assign_ticket_to_sprint
.execute(AssignTicketToSprintInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
sprint_id: parse_sprint_id_dto(&request.sprint_id)?,
expected_version: version_dto(request.expected_version)?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_unassign_sprint(
request: TicketSprintUnassignRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue = state
.unassign_ticket_from_sprint
.execute(UnassignTicketFromSprintInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
expected_version: version_dto(request.expected_version)?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_update(
request: TicketUpdateRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue = state
.update_issue
.execute(update_input(project, request, IssueActor::User)?)
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_read_carnet(
request: TicketReadRequestDto,
state: State<'_, AppState>,
) -> Result<TicketCarnetDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
state
.read_issue_carnet
.execute(ReadIssueCarnetInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
})
.await
.map(|out| TicketCarnetDto::from(out.carnet))
.map_err(ErrorDto::from)
}
#[tauri::command]
pub async fn ticket_update_carnet(
request: TicketUpdateCarnetRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue_ref = parse_ref_dto(&request.r#ref)?;
state
.update_issue_carnet
.execute(UpdateIssueCarnetInput {
project: project.clone(),
issue_ref,
expected_version: version_dto(request.expected_version)?,
carnet: request.carnet,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?;
let issue = state
.read_issue
.execute(ReadIssueInput { project, issue_ref })
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_link(
request: TicketLinkCommandRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue = state
.link_issues
.execute(LinkIssuesInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
target: parse_ref_dto(&request.target_ref)?,
kind: parse_link_kind_dto(&request.kind)?,
expected_version: version_dto(request.expected_version)?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_unlink(
request: TicketUnlinkCommandRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let issue = state
.unlink_issues
.execute(UnlinkIssuesInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
target: parse_ref_dto(&request.target_ref)?,
kind: request
.kind
.as_deref()
.map(parse_link_kind_dto)
.transpose()?,
expected_version: version_dto(request.expected_version)?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
#[tauri::command]
pub async fn ticket_assign(
request: TicketAssignRequestDto,
state: State<'_, AppState>,
) -> Result<TicketDto, ErrorDto> {
let project = resolve_project(&state, &request.project_id).await?;
let agent_id = parse_agent_id_dto(&request.agent_id)?;
let issue = state
.assign_issue_agent
.execute(AssignIssueAgentInput {
project,
issue_ref: parse_ref_dto(&request.r#ref)?,
agent_id,
assigned: request.assigned,
expected_version: version_dto(request.expected_version)?,
actor: IssueActor::User,
})
.await
.map_err(ErrorDto::from)?
.issue;
Ok(TicketDto::from_issue(issue, None))
}
async fn resolve_project(state: &AppState, project_id: &str) -> Result<Project, ErrorDto> {
let id = parse_project_id(project_id)?;
state
.open_project
.execute(OpenProjectInput { project_id: id })
.await
.map(|out| out.project)
.map_err(ErrorDto::from)
}
async fn read_carnet_body(
read_carnet: &application::ReadIssueCarnet,
project: &Project,
issue_ref: IssueRef,
) -> Result<String, TicketToolError> {
read_carnet
.execute(ReadIssueCarnetInput {
project: project.clone(),
issue_ref,
})
.await
.map(|out| out.carnet.carnet.as_str().to_owned())
.map_err(ticket_error)
}
async fn read_ticket_carnet_body_best_effort(
read_carnet: &application::ReadIssueCarnet,
project: &Project,
issue_ref: IssueRef,
fallback_carnet: String,
) -> Result<String, AppError> {
match read_carnet
.execute(ReadIssueCarnetInput {
project: project.clone(),
issue_ref,
})
.await
{
Ok(out) => Ok(out.carnet.carnet.as_str().to_owned()),
Err(AppError::NotFound(_)) => Ok(fallback_carnet),
Err(err) => Err(err),
}
}
fn ticket_error(err: AppError) -> TicketToolError {
let message = err.to_string();
let code = if message.contains("issue version conflict") {
"versionConflict"
} else {
match err.code() {
"NOT_FOUND" => "notFound",
"INVALID" => "invalid",
"STORE" => "store",
"FILESYSTEM" => "filesystem",
_ => "internal",
}
};
TicketToolError::new(code, message)
}
fn dto_tool_error(err: ErrorDto) -> TicketToolError {
let code = match err.code.as_str() {
"NOT_FOUND" => "notFound",
"INVALID" => "invalid",
"STORE" => "store",
"FILESYSTEM" => "filesystem",
_ => "internal",
};
TicketToolError::new(code, err.message)
}
fn mcp_create_request(
project: &Project,
arguments: Value,
) -> Result<TicketCreateRequestDto, TicketToolError> {
let mut req: TicketCreateRequestDto = serde_json::from_value(arguments)
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
req.project_id = project.id.to_string();
Ok(req)
}
fn mcp_update_request(
project: &Project,
arguments: Value,
) -> Result<TicketUpdateRequestDto, TicketToolError> {
let mut req: TicketUpdateRequestDto = serde_json::from_value(arguments)
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
req.project_id = project.id.to_string();
Ok(req)
}
fn mcp_list_request(
project: &Project,
arguments: Value,
) -> Result<TicketListPageInput, TicketToolError> {
let mut req: TicketListRequestDto = serde_json::from_value(arguments)
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
req.project_id = project.id.to_string();
TicketListPageInput::from_request(req).map_err(|e| TicketToolError::new("invalid", e.message))
}
fn mcp_bulk_update_status_request(
project: &Project,
arguments: Value,
) -> Result<TicketBulkUpdateStatusRequestDto, TicketToolError> {
let mut req: TicketBulkUpdateStatusRequestDto = serde_json::from_value(arguments)
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
req.project_id = project.id.to_string();
Ok(req)
}
fn mcp_bulk_update_priority_request(
project: &Project,
arguments: Value,
) -> Result<TicketBulkUpdatePriorityRequestDto, TicketToolError> {
let mut req: TicketBulkUpdatePriorityRequestDto = serde_json::from_value(arguments)
.map_err(|e| TicketToolError::new("invalid", e.to_string()))?;
req.project_id = project.id.to_string();
Ok(req)
}
fn parse_json_ref(arguments: &Value, key: &str) -> Result<IssueRef, TicketToolError> {
required_str(arguments, key).and_then(|raw| parse_ref_dto(raw).map_err(dto_tool_error))
}
fn parse_json_version(arguments: &Value) -> Result<domain::IssueVersion, TicketToolError> {
let version = arguments
.get("expectedVersion")
.and_then(Value::as_u64)
.ok_or_else(|| TicketToolError::new("invalid", "missing expectedVersion"))?;
version_dto(version).map_err(dto_tool_error)
}
fn required_str<'a>(arguments: &'a Value, key: &str) -> Result<&'a str, TicketToolError> {
arguments
.get(key)
.and_then(Value::as_str)
.ok_or_else(|| TicketToolError::new("invalid", format!("missing {key}")))
}