Skip to main content

devboy_jira/
types.rs

1//! Jira API response types.
2//!
3//! These types represent the raw JSON responses from Jira API v2/v3.
4//! They are deserialized and then mapped to unified types.
5
6use serde::{Deserialize, Serialize};
7
8// =============================================================================
9// User
10// =============================================================================
11
12#[derive(Debug, Clone, Deserialize)]
13pub struct JiraUser {
14    /// Account ID (Cloud only)
15    #[serde(default, rename = "accountId")]
16    pub account_id: Option<String>,
17    /// Username (Self-Hosted only)
18    #[serde(default)]
19    pub name: Option<String>,
20    #[serde(default, rename = "displayName")]
21    pub display_name: Option<String>,
22    #[serde(default, rename = "emailAddress")]
23    pub email_address: Option<String>,
24}
25
26// =============================================================================
27// Issue
28// =============================================================================
29
30#[derive(Debug, Clone, Deserialize)]
31pub struct JiraIssue {
32    pub id: String,
33    /// Issue key (e.g., "PROJ-123")
34    pub key: String,
35    pub fields: JiraIssueFields,
36}
37
38#[derive(Debug, Clone, Deserialize)]
39pub struct JiraIssueFields {
40    #[serde(default)]
41    pub summary: Option<String>,
42    /// Description — plain text (v2) or ADF document (v3)
43    #[serde(default)]
44    pub description: Option<serde_json::Value>,
45    #[serde(default)]
46    pub status: Option<JiraStatus>,
47    #[serde(default)]
48    pub priority: Option<JiraPriority>,
49    #[serde(default)]
50    pub assignee: Option<JiraUser>,
51    /// Reporter (author)
52    #[serde(default)]
53    pub reporter: Option<JiraUser>,
54    #[serde(default)]
55    pub labels: Vec<String>,
56    /// Created timestamp
57    #[serde(default)]
58    pub created: Option<String>,
59    /// Updated timestamp
60    #[serde(default)]
61    pub updated: Option<String>,
62    /// Parent issue (for subtasks)
63    #[serde(default)]
64    pub parent: Option<Box<JiraIssue>>,
65    /// Subtasks / child issues
66    #[serde(default)]
67    pub subtasks: Vec<JiraIssue>,
68    #[serde(default)]
69    pub issuelinks: Vec<JiraIssueLink>,
70    /// Attachments on the issue (present when the caller requests
71    /// `fields=attachment` or uses `fields=*all`).
72    #[serde(default)]
73    pub attachment: Vec<JiraAttachment>,
74}
75
76/// Jira attachment as returned inside `fields.attachment`.
77#[derive(Debug, Clone, Deserialize)]
78pub struct JiraAttachment {
79    /// Attachment id (numeric string).
80    pub id: String,
81    #[serde(default)]
82    pub filename: Option<String>,
83    /// Direct download URL (`content` in the Jira API).
84    #[serde(default)]
85    pub content: Option<String>,
86    #[serde(default)]
87    pub size: Option<u64>,
88    #[serde(default, rename = "mimeType")]
89    pub mime_type: Option<String>,
90    /// Creation timestamp (ISO 8601).
91    #[serde(default)]
92    pub created: Option<String>,
93    /// Author — Jira uses `author` inside the attachment object.
94    #[serde(default)]
95    pub author: Option<JiraUser>,
96}
97
98#[derive(Debug, Clone, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct JiraStatus {
101    pub name: String,
102    /// Status category (new, indeterminate, done)
103    #[serde(default)]
104    pub status_category: Option<JiraStatusCategory>,
105}
106
107#[derive(Debug, Clone, Deserialize)]
108pub struct JiraStatusCategory {
109    /// Category key: "new", "indeterminate", "done"
110    pub key: String,
111}
112
113#[derive(Debug, Clone, Deserialize)]
114pub struct JiraPriority {
115    pub name: String,
116}
117
118// =============================================================================
119// Issue Links
120// =============================================================================
121
122#[derive(Debug, Clone, Deserialize)]
123pub struct JiraIssueLink {
124    /// Link ID
125    #[serde(default)]
126    pub id: Option<String>,
127    #[serde(rename = "type")]
128    pub link_type: JiraIssueLinkType,
129    /// Inward issue (e.g., "is blocked by" this issue)
130    #[serde(default, rename = "inwardIssue")]
131    pub inward_issue: Option<Box<JiraIssue>>,
132    /// Outward issue (e.g., "blocks" this issue)
133    #[serde(default, rename = "outwardIssue")]
134    pub outward_issue: Option<Box<JiraIssue>>,
135}
136
137#[derive(Debug, Clone, Deserialize)]
138pub struct JiraIssueLinkType {
139    /// Link type name (e.g., "Blocks", "Relates")
140    pub name: String,
141    /// Inward description (e.g., "is blocked by")
142    #[serde(default)]
143    pub inward: Option<String>,
144    /// Outward description (e.g., "blocks")
145    #[serde(default)]
146    pub outward: Option<String>,
147}
148
149// =============================================================================
150// Search Response
151// =============================================================================
152
153/// Search response from Self-Hosted Jira (API v2, GET /search).
154#[derive(Debug, Clone, Deserialize)]
155pub struct JiraSearchResponse {
156    pub issues: Vec<JiraIssue>,
157    /// Starting index
158    #[serde(default, rename = "startAt")]
159    pub start_at: Option<u32>,
160    /// Max results per page
161    #[serde(default, rename = "maxResults")]
162    pub max_results: Option<u32>,
163    /// Total number of results
164    #[serde(default)]
165    pub total: Option<u32>,
166}
167
168/// Search response from Jira Cloud (API v3, GET /search/jql).
169#[derive(Debug, Clone, Deserialize)]
170pub struct JiraCloudSearchResponse {
171    pub issues: Vec<JiraIssue>,
172    #[serde(default, rename = "nextPageToken")]
173    pub next_page_token: Option<String>,
174}
175
176// =============================================================================
177// Comment
178// =============================================================================
179
180/// Jira comment representation.
181#[derive(Debug, Clone, Deserialize)]
182pub struct JiraComment {
183    /// Comment ID
184    pub id: String,
185    /// Comment body — plain text (v2) or ADF document (v3)
186    #[serde(default)]
187    pub body: Option<serde_json::Value>,
188    /// Comment author
189    #[serde(default)]
190    pub author: Option<JiraUser>,
191    /// Created timestamp
192    #[serde(default)]
193    pub created: Option<String>,
194    /// Updated timestamp
195    #[serde(default)]
196    pub updated: Option<String>,
197}
198
199/// Response from GET /issue/{key}/comment.
200#[derive(Debug, Clone, Deserialize)]
201pub struct JiraCommentsResponse {
202    pub comments: Vec<JiraComment>,
203}
204
205// =============================================================================
206// Transitions
207// =============================================================================
208
209/// Jira transition representation.
210#[derive(Debug, Clone, Deserialize)]
211pub struct JiraTransition {
212    /// Transition ID
213    pub id: String,
214    /// Transition name
215    pub name: String,
216    /// Target status
217    pub to: JiraStatus,
218}
219
220/// Response from GET /issue/{key}/transitions.
221#[derive(Debug, Clone, Deserialize)]
222pub struct JiraTransitionsResponse {
223    /// Available transitions
224    pub transitions: Vec<JiraTransition>,
225}
226
227// =============================================================================
228// Create/Update types
229// =============================================================================
230
231/// Request body for creating an issue.
232#[derive(Debug, Clone, Serialize)]
233pub struct CreateIssuePayload {
234    /// Issue fields
235    pub fields: CreateIssueFields,
236}
237
238/// Fields for creating an issue.
239#[derive(Debug, Clone, Serialize)]
240pub struct CreateIssueFields {
241    pub project: ProjectKey,
242    /// Summary (title)
243    pub summary: String,
244    /// Issue type
245    pub issuetype: IssueType,
246    /// Description — plain text (v2) or ADF (v3)
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub description: Option<serde_json::Value>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub labels: Option<Vec<String>>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub priority: Option<PriorityName>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub assignee: Option<serde_json::Value>,
255    /// Components (issue #197).
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub components: Option<Vec<ComponentRef>>,
258    /// Parent issue reference. Required by Jira when `issuetype` is a
259    /// sub-task or any "is_subtask" hierarchical type — the API rejects
260    /// the request with a 400 otherwise (issue #214). Serialised as
261    /// `{ "key": "PROJ-1" }`, matching Jira's `fields.parent` shape.
262    /// Uses [`IssueKeyRef`] (not [`ProjectKey`]) because the value is an
263    /// **issue** key (e.g. `PROJ-1`), not a project key (e.g. `PROJ`).
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub parent: Option<IssueKeyRef>,
266}
267
268/// Component reference used in create/update issue payloads (issue #197).
269///
270/// We serialise by **name** (`{ "name": "..." }`) to line up with the
271/// Jira schema enricher, which enumerates component *names* in tool
272/// schemas (`JiraComponent.name`). Jira accepts either `{id}` or `{name}`
273/// in `fields.components`, so name-based references work across Cloud +
274/// Self-Hosted without forcing callers to resolve ids first.
275///
276/// This is addressed in Copilot review on PR #205.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct ComponentRef {
279    pub name: String,
280}
281
282/// Project key reference.
283#[derive(Debug, Clone, Serialize)]
284pub struct ProjectKey {
285    /// Project key (e.g., "PROJ")
286    pub key: String,
287}
288
289/// Issue type reference.
290#[derive(Debug, Clone, Serialize)]
291pub struct IssueType {
292    /// Issue type name
293    pub name: String,
294}
295
296/// Priority name reference.
297#[derive(Debug, Clone, Serialize)]
298pub struct PriorityName {
299    /// Priority name
300    pub name: String,
301}
302
303/// Request body for updating an issue.
304#[derive(Debug, Clone, Serialize)]
305pub struct UpdateIssuePayload {
306    /// Issue fields to update
307    pub fields: UpdateIssueFields,
308}
309
310/// Fields for updating an issue.
311#[derive(Debug, Clone, Serialize, Default)]
312pub struct UpdateIssueFields {
313    /// Summary (title)
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub summary: Option<String>,
316    /// Description — plain text (v2) or ADF (v3)
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub description: Option<serde_json::Value>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub labels: Option<Vec<String>>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub priority: Option<PriorityName>,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub assignee: Option<serde_json::Value>,
325    /// Components (issue #197). `None` means do not touch.
326    /// `Some(vec![])` clears all components.
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub components: Option<Vec<ComponentRef>>,
329}
330
331/// Request body for transitioning an issue.
332#[derive(Debug, Clone, Serialize)]
333pub struct TransitionPayload {
334    /// Transition to execute
335    pub transition: TransitionId,
336}
337
338/// Transition ID reference.
339#[derive(Debug, Clone, Serialize)]
340pub struct TransitionId {
341    /// Transition ID
342    pub id: String,
343}
344
345/// Response from POST /issue (create issue).
346#[derive(Debug, Clone, Deserialize)]
347pub struct CreateIssueResponse {
348    /// Issue ID
349    pub id: String,
350    /// Issue key (e.g., "PROJ-123")
351    pub key: String,
352}
353
354/// Request body for adding a comment.
355#[derive(Debug, Clone, Serialize)]
356pub struct AddCommentPayload {
357    /// Comment body — plain text (v2) or ADF (v3)
358    pub body: serde_json::Value,
359}
360
361// =============================================================================
362// Project Statuses
363// =============================================================================
364
365/// Response from GET /project/{key}/statuses.
366/// Returns statuses grouped by issue type.
367#[derive(Debug, Clone, Deserialize)]
368pub struct JiraIssueTypeStatuses {
369    /// Issue type name (e.g., "Task", "Bug")
370    #[serde(default)]
371    pub name: Option<String>,
372    /// Statuses available for this issue type
373    #[serde(default)]
374    pub statuses: Vec<JiraProjectStatus>,
375}
376
377/// A status within a project, including its category.
378#[derive(Debug, Clone, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct JiraProjectStatus {
381    /// Status name
382    pub name: String,
383    /// Status ID
384    #[serde(default)]
385    pub id: Option<String>,
386    #[serde(default)]
387    pub status_category: Option<JiraStatusCategory>,
388}
389
390// =============================================================================
391// Issue Link types
392// =============================================================================
393
394/// Request body for creating an issue link.
395#[derive(Debug, Clone, Serialize)]
396#[serde(rename_all = "camelCase")]
397pub struct CreateIssueLinkPayload {
398    #[serde(rename = "type")]
399    pub link_type: IssueLinkTypeName,
400    /// Inward issue (target)
401    pub inward_issue: IssueKeyRef,
402    /// Outward issue (source)
403    pub outward_issue: IssueKeyRef,
404}
405
406/// Issue link type name reference.
407#[derive(Debug, Clone, Serialize)]
408pub struct IssueLinkTypeName {
409    /// Link type name (e.g., "Blocks", "Relates")
410    pub name: String,
411}
412
413/// Issue key reference for linking.
414#[derive(Debug, Clone, Serialize)]
415pub struct IssueKeyRef {
416    /// Issue key (e.g., "PROJ-123")
417    pub key: String,
418}
419
420// =============================================================================
421// Jira Structure Plugin API types (/rest/structure/2.0/)
422// =============================================================================
423
424/// Structure info from GET /rest/structure/2.0/structure
425#[derive(Debug, Clone, Deserialize)]
426pub struct JiraStructure {
427    pub id: u64,
428    pub name: String,
429    #[serde(default)]
430    pub description: Option<String>,
431}
432
433/// Response from GET /rest/structure/2.0/structure
434#[derive(Debug, Clone, Deserialize)]
435pub struct JiraStructureListResponse {
436    pub structures: Vec<JiraStructure>,
437}
438
439/// A single row in the forest (compact format from API)
440#[derive(Debug, Clone, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub struct JiraForestRow {
443    pub id: u64,
444    #[serde(default)]
445    pub item_id: Option<String>,
446    #[serde(default)]
447    pub item_type: Option<String>,
448}
449
450/// Forest response from POST /rest/structure/2.0/forest/{id}/spec
451#[derive(Debug, Clone, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct JiraForestResponse {
454    pub version: u64,
455    #[serde(default)]
456    pub rows: Vec<JiraForestRow>,
457    #[serde(default)]
458    pub depths: Vec<u32>,
459    #[serde(default)]
460    pub total_count: Option<u64>,
461}
462
463/// Response from forest modification operations (add/move)
464#[derive(Debug, Clone, Deserialize)]
465pub struct JiraForestModifyResponse {
466    pub version: u64,
467    #[serde(default)]
468    pub rows: Vec<JiraForestRow>,
469}
470
471/// Structure view from /rest/structure/2.0/view
472#[derive(Debug, Clone, Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct JiraStructureView {
475    pub id: u64,
476    pub name: String,
477    /// Owning structure id. Left non-optional and **without**
478    /// `#[serde(default)]` so a missing / renamed field from the API
479    /// fails loudly rather than silently deserialising to `0` — the
480    /// caller uses this id for cross-structure scope checks.
481    pub structure_id: u64,
482    #[serde(default)]
483    pub columns: Vec<JiraStructureViewColumn>,
484    #[serde(default)]
485    pub group_by: Option<String>,
486    #[serde(default)]
487    pub sort_by: Option<String>,
488    #[serde(default)]
489    pub filter: Option<String>,
490}
491
492/// Column definition in a structure view
493#[derive(Debug, Clone, Deserialize, Serialize)]
494pub struct JiraStructureViewColumn {
495    #[serde(default)]
496    pub id: Option<String>,
497    #[serde(default)]
498    pub field: Option<String>,
499    #[serde(default)]
500    pub formula: Option<String>,
501    #[serde(default)]
502    pub width: Option<u32>,
503}
504
505/// Response from GET /rest/structure/2.0/view?structureId={id}
506#[derive(Debug, Clone, Deserialize)]
507pub struct JiraStructureViewListResponse {
508    pub views: Vec<JiraStructureView>,
509}
510
511/// Batch value response from POST /rest/structure/2.0/value
512#[derive(Debug, Clone, Deserialize)]
513#[serde(rename_all = "camelCase")]
514pub struct JiraStructureValueEntry {
515    pub row_id: u64,
516    #[serde(default)]
517    pub column_id: Option<String>,
518    #[serde(default)]
519    pub value: serde_json::Value,
520}
521
522/// Response from POST /rest/structure/2.0/value
523#[derive(Debug, Clone, Deserialize)]
524pub struct JiraStructureValuesResponse {
525    pub values: Vec<JiraStructureValueEntry>,
526}
527
528// =============================================================================
529// Jira Project Versions (issue #238) — /rest/api/2/version + /project/{key}/versions
530// =============================================================================
531
532/// Version DTO returned by the Jira REST API.
533///
534/// Field set is the union of Cloud (v3) and Server/DC (v2) — both use
535/// the same path family. `issuesStatusForFixVersion` only appears when
536/// the caller passes `?expand=issuesstatus`.
537#[derive(Debug, Clone, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct JiraVersionDto {
540    pub id: String,
541    pub name: String,
542    /// Project key (e.g., "PROJ"). Server returns this directly; Cloud
543    /// returns `projectId` separately, so we accept either.
544    #[serde(default)]
545    pub project: Option<String>,
546    #[serde(default)]
547    pub project_id: Option<u64>,
548    #[serde(default)]
549    pub description: Option<String>,
550    #[serde(default)]
551    pub start_date: Option<String>,
552    #[serde(default)]
553    pub release_date: Option<String>,
554    #[serde(default)]
555    pub released: bool,
556    #[serde(default)]
557    pub archived: bool,
558    /// Computed by the server: true when releaseDate is in the past
559    /// and the version is still unreleased.
560    #[serde(default)]
561    pub overdue: Option<bool>,
562    /// Returned only with `?expand=issuesstatus` (Cloud) — keys are
563    /// status categories (`unmapped`, `toDo`, `inProgress`, `done`).
564    #[serde(default)]
565    pub issues_status_for_fix_version: Option<JiraVersionIssueStatusCounts>,
566    /// Server/DC returns this directly on the version DTO. Callers that
567    /// need a total fixed-issue count have to hit
568    /// `/version/{id}/relatedIssueCounts` separately.
569    #[serde(default)]
570    pub issues_unresolved_count: Option<u32>,
571}
572
573/// Issue counts by status category (Cloud `?expand=issuesstatus`).
574#[derive(Debug, Clone, Deserialize, Default)]
575#[serde(rename_all = "camelCase")]
576pub struct JiraVersionIssueStatusCounts {
577    #[serde(default)]
578    pub unmapped: u32,
579    #[serde(default)]
580    pub to_do: u32,
581    #[serde(default)]
582    pub in_progress: u32,
583    #[serde(default)]
584    pub done: u32,
585}
586
587impl JiraVersionIssueStatusCounts {
588    pub fn total(&self) -> u32 {
589        self.unmapped
590            .saturating_add(self.to_do)
591            .saturating_add(self.in_progress)
592            .saturating_add(self.done)
593    }
594}
595
596/// POST /rest/api/2/version payload.
597///
598/// `project` and `project_id` are mutually-exclusive routing keys —
599/// callers should fill exactly one (Server/DC accepts both, Cloud
600/// historically prefers `projectId`). Optional date / state fields
601/// are skipped when `None` so creation defaults match the UI.
602#[derive(Debug, Clone, Serialize, Default)]
603#[serde(rename_all = "camelCase")]
604pub struct CreateVersionPayload {
605    pub name: String,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub project: Option<String>,
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub project_id: Option<u64>,
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub description: Option<String>,
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub start_date: Option<String>,
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub release_date: Option<String>,
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub released: Option<bool>,
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub archived: Option<bool>,
620}
621
622/// PUT /rest/api/2/version/{id} payload — partial update; only fields
623/// explicitly set are sent (`#[serde(skip_serializing_if = "Option::is_none")]`),
624/// so unspecified fields are preserved server-side.
625#[derive(Debug, Clone, Serialize, Default)]
626#[serde(rename_all = "camelCase")]
627pub struct UpdateVersionPayload {
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub name: Option<String>,
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub description: Option<String>,
632    #[serde(skip_serializing_if = "Option::is_none")]
633    pub start_date: Option<String>,
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub release_date: Option<String>,
636    #[serde(skip_serializing_if = "Option::is_none")]
637    pub released: Option<bool>,
638    #[serde(skip_serializing_if = "Option::is_none")]
639    pub archived: Option<bool>,
640}