Skip to main content

gitea_sdk_rs/options/
issue.rs

1// Copyright 2026 infinitete. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5//! Request option types for issue API endpoints.
6
7use crate::internal::request::urlencoding;
8use crate::pagination::{ListOptions, QueryEncode};
9use crate::types::enums::StateType;
10use crate::types::serde_helpers::nullable_rfc3339;
11use crate::{Deserialize, Serialize};
12use time::OffsetDateTime;
13
14// ── issue.go ─────────────────────────────────────────────────────
15
16/// ListIssueOption list issue options
17#[derive(Debug, Clone, Default)]
18/// Options for List Issue Option.
19pub struct ListIssueOption {
20    pub list_options: ListOptions,
21    pub state: Option<StateType>,
22    pub r#type: Option<crate::types::enums::IssueType>,
23    pub labels: Vec<String>,
24    pub milestones: Vec<String>,
25    pub key_word: String,
26    pub since: Option<OffsetDateTime>,
27    pub before: Option<OffsetDateTime>,
28    /// filter by created by username
29    pub created_by: String,
30    /// filter by assigned to username
31    pub assigned_by: String,
32    /// filter by username mentioned
33    pub mentioned_by: String,
34    /// filter by owner (only works on ListIssues on User)
35    pub owner: String,
36    /// filter by team (requires organization owner parameter)
37    pub team: String,
38}
39
40impl QueryEncode for ListIssueOption {
41    fn query_encode(&self) -> String {
42        let mut out = self.list_options.query_encode();
43
44        if let Some(ref state) = self.state {
45            out.push_str(&format!("&state={}", state.as_ref()));
46        }
47
48        if !self.labels.is_empty() {
49            out.push_str(&format!("&labels={}", self.labels.join(",")));
50        }
51
52        if !self.key_word.is_empty() {
53            out.push_str(&format!("&q={}", urlencoding(&self.key_word)));
54        }
55
56        if let Some(ref t) = self.r#type {
57            out.push_str(&format!("&type={}", t.as_ref()));
58        }
59
60        if !self.milestones.is_empty() {
61            out.push_str(&format!("&milestones={}", self.milestones.join(",")));
62        }
63
64        if let Some(since) = self.since {
65            let formatted = since
66                .format(&time::format_description::well_known::Rfc3339)
67                .unwrap_or_default();
68            out.push_str(&format!("&since={}", urlencoding(&formatted)));
69        }
70        if let Some(before) = self.before {
71            let formatted = before
72                .format(&time::format_description::well_known::Rfc3339)
73                .unwrap_or_default();
74            out.push_str(&format!("&before={}", urlencoding(&formatted)));
75        }
76
77        if !self.created_by.is_empty() {
78            out.push_str(&format!("&created_by={}", urlencoding(&self.created_by)));
79        }
80        if !self.assigned_by.is_empty() {
81            out.push_str(&format!("&assigned_by={}", urlencoding(&self.assigned_by)));
82        }
83        if !self.mentioned_by.is_empty() {
84            out.push_str(&format!(
85                "&mentioned_by={}",
86                urlencoding(&self.mentioned_by)
87            ));
88        }
89        if !self.owner.is_empty() {
90            out.push_str(&format!("&owner={}", urlencoding(&self.owner)));
91        }
92        if !self.team.is_empty() {
93            out.push_str(&format!("&team={}", urlencoding(&self.team)));
94        }
95
96        out
97    }
98}
99
100/// CreateIssueOption options to create one issue
101#[derive(Debug, Clone, Serialize, Deserialize)]
102/// Options for Create Issue Option.
103pub struct CreateIssueOption {
104    pub title: String,
105    pub body: String,
106    #[serde(default)]
107    pub r#ref: String,
108    #[serde(default)]
109    pub assignees: Vec<String>,
110    #[serde(
111        rename = "due_date",
112        default,
113        with = "nullable_rfc3339",
114        skip_serializing_if = "Option::is_none"
115    )]
116    pub deadline: Option<OffsetDateTime>,
117    /// milestone id
118    #[serde(default)]
119    pub milestone: i64,
120    /// list of label ids
121    #[serde(default)]
122    pub labels: Vec<i64>,
123    #[serde(default)]
124    pub closed: bool,
125}
126
127impl CreateIssueOption {
128    /// Validate the CreateIssueOption struct
129    pub fn validate(&self) -> crate::Result<()> {
130        if self.title.trim().is_empty() {
131            return Err(crate::Error::Validation("title is empty".to_string()));
132        }
133        Ok(())
134    }
135}
136
137/// EditIssueOption options for editing an issue
138#[derive(Debug, Clone, Serialize, Deserialize)]
139/// Options for Edit Issue Option.
140pub struct EditIssueOption {
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub title: Option<String>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub body: Option<String>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub r#ref: Option<String>,
147    #[serde(default)]
148    pub assignees: Vec<String>,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub milestone: Option<i64>,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub state: Option<StateType>,
153    #[serde(
154        rename = "due_date",
155        default,
156        with = "nullable_rfc3339",
157        skip_serializing_if = "Option::is_none"
158    )]
159    pub deadline: Option<OffsetDateTime>,
160    #[serde(
161        rename = "unset_due_date",
162        default,
163        skip_serializing_if = "Option::is_none"
164    )]
165    pub remove_deadline: Option<bool>,
166}
167
168impl EditIssueOption {
169    /// Validate the EditIssueOption struct
170    pub fn validate(&self) -> crate::Result<()> {
171        if let Some(ref title) = self.title
172            && title.trim().is_empty()
173        {
174            return Err(crate::Error::Validation("title is empty".to_string()));
175        }
176        Ok(())
177    }
178}
179
180// ── issue_comment.go ─────────────────────────────────────────────
181
182/// ListIssueCommentOptions list comment options
183#[derive(Debug, Clone, Default)]
184/// Options for List Issue Comment Option.
185pub struct ListIssueCommentOptions {
186    pub list_options: ListOptions,
187    pub since: Option<OffsetDateTime>,
188    pub before: Option<OffsetDateTime>,
189}
190
191impl QueryEncode for ListIssueCommentOptions {
192    fn query_encode(&self) -> String {
193        let mut out = self.list_options.query_encode();
194        if let Some(since) = self.since {
195            let formatted = since
196                .format(&time::format_description::well_known::Rfc3339)
197                .unwrap_or_default();
198            out.push_str(&format!("&since={}", urlencoding(&formatted)));
199        }
200        if let Some(before) = self.before {
201            let formatted = before
202                .format(&time::format_description::well_known::Rfc3339)
203                .unwrap_or_default();
204            out.push_str(&format!("&before={}", urlencoding(&formatted)));
205        }
206        out
207    }
208}
209
210/// CreateIssueCommentOption options for creating a comment on an issue
211#[derive(Debug, Clone, Serialize, Deserialize)]
212/// Options for Create Issue Comment Option.
213pub struct CreateIssueCommentOption {
214    pub body: String,
215}
216
217impl CreateIssueCommentOption {
218    /// Validate the CreateIssueCommentOption struct
219    pub fn validate(&self) -> crate::Result<()> {
220        if self.body.is_empty() {
221            return Err(crate::Error::Validation("body is empty".to_string()));
222        }
223        Ok(())
224    }
225}
226
227/// EditIssueCommentOption options for editing a comment
228#[derive(Debug, Clone, Serialize, Deserialize)]
229/// Options for Edit Issue Comment Option.
230pub struct EditIssueCommentOption {
231    pub body: String,
232}
233
234impl EditIssueCommentOption {
235    /// Validate the EditIssueCommentOption struct
236    pub fn validate(&self) -> crate::Result<()> {
237        if self.body.is_empty() {
238            return Err(crate::Error::Validation("body is empty".to_string()));
239        }
240        Ok(())
241    }
242}
243
244// ── issue_label.go ───────────────────────────────────────────────
245
246/// IssueLabelsOption a collection of labels
247#[derive(Debug, Clone, Serialize, Deserialize)]
248/// Options for Issue Labels Option.
249pub struct IssueLabelsOption {
250    /// list of label IDs
251    #[serde(default)]
252    pub labels: Vec<i64>,
253}
254
255// ── issue_milestone.go ───────────────────────────────────────────
256
257/// ListMilestoneOption list milestone options
258#[derive(Debug, Clone, Default)]
259/// Options for List Milestone Option.
260pub struct ListMilestoneOption {
261    pub list_options: ListOptions,
262    /// open, closed, all
263    pub state: Option<StateType>,
264    pub name: String,
265}
266
267impl QueryEncode for ListMilestoneOption {
268    fn query_encode(&self) -> String {
269        let mut out = self.list_options.query_encode();
270        if let Some(ref state) = self.state {
271            out.push_str(&format!("&state={}", state.as_ref()));
272        }
273        if !self.name.is_empty() {
274            out.push_str(&format!("&name={}", urlencoding(&self.name)));
275        }
276        out
277    }
278}
279
280/// CreateMilestoneOption options for creating a milestone
281#[derive(Debug, Clone, Serialize, Deserialize)]
282/// Options for Create Milestone Option.
283pub struct CreateMilestoneOption {
284    pub title: String,
285    #[serde(default)]
286    pub description: String,
287    pub state: StateType,
288    #[serde(
289        rename = "due_on",
290        default,
291        with = "nullable_rfc3339",
292        skip_serializing_if = "Option::is_none"
293    )]
294    pub deadline: Option<OffsetDateTime>,
295}
296
297impl CreateMilestoneOption {
298    /// Validate the CreateMilestoneOption struct
299    pub fn validate(&self) -> crate::Result<()> {
300        if self.title.trim().is_empty() {
301            return Err(crate::Error::Validation("title is empty".to_string()));
302        }
303        Ok(())
304    }
305}
306
307/// EditMilestoneOption options for editing a milestone
308#[derive(Debug, Clone, Default, Serialize, Deserialize)]
309/// Options for Edit Milestone Option.
310pub struct EditMilestoneOption {
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub title: Option<String>,
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub description: Option<String>,
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub state: Option<StateType>,
317    #[serde(
318        rename = "due_on",
319        default,
320        with = "nullable_rfc3339",
321        skip_serializing_if = "Option::is_none"
322    )]
323    pub deadline: Option<OffsetDateTime>,
324}
325
326impl EditMilestoneOption {
327    /// Validate the EditMilestoneOption struct
328    pub fn validate(&self) -> crate::Result<()> {
329        if let Some(ref title) = self.title
330            && title.trim().is_empty()
331        {
332            return Err(crate::Error::Validation("title is empty".to_string()));
333        }
334        Ok(())
335    }
336}
337
338// ── issue_reaction.go ────────────────────────────────────────────
339
340/// ListIssueReactionsOptions options for listing issue reactions
341#[derive(Debug, Clone, Default)]
342/// Options for List Issue Reactions Option.
343pub struct ListIssueReactionsOptions {
344    pub list_options: ListOptions,
345}
346
347impl QueryEncode for ListIssueReactionsOptions {
348    fn query_encode(&self) -> String {
349        self.list_options.query_encode()
350    }
351}
352
353// ── issue_subscription.go ────────────────────────────────────────
354
355/// ListIssueSubscribersOptions options for listing issue subscribers
356#[derive(Debug, Clone, Default)]
357/// Options for List Issue Subscribers Option.
358pub struct ListIssueSubscribersOptions {
359    pub list_options: ListOptions,
360}
361
362impl QueryEncode for ListIssueSubscribersOptions {
363    fn query_encode(&self) -> String {
364        self.list_options.query_encode()
365    }
366}
367
368// ── issue_stopwatch.go ───────────────────────────────────────────
369
370/// ListStopwatchesOptions options for listing stopwatches
371#[derive(Debug, Clone, Default)]
372/// Options for List Stopwatches Option.
373pub struct ListStopwatchesOptions {
374    pub list_options: ListOptions,
375}
376
377impl QueryEncode for ListStopwatchesOptions {
378    fn query_encode(&self) -> String {
379        self.list_options.query_encode()
380    }
381}
382
383// ── issue_tracked_time.go ────────────────────────────────────────
384
385/// ListTrackedTimesOptions options for listing repository's tracked times
386#[derive(Debug, Clone, Default)]
387/// Options for List Tracked Times Option.
388pub struct ListTrackedTimesOptions {
389    pub list_options: ListOptions,
390    pub since: Option<OffsetDateTime>,
391    pub before: Option<OffsetDateTime>,
392    /// User filter is only used by ListRepoTrackedTimes
393    pub user: String,
394}
395
396impl QueryEncode for ListTrackedTimesOptions {
397    fn query_encode(&self) -> String {
398        let mut out = self.list_options.query_encode();
399        if let Some(since) = self.since {
400            let formatted = since
401                .format(&time::format_description::well_known::Rfc3339)
402                .unwrap_or_default();
403            out.push_str(&format!("&since={}", urlencoding(&formatted)));
404        }
405        if let Some(before) = self.before {
406            let formatted = before
407                .format(&time::format_description::well_known::Rfc3339)
408                .unwrap_or_default();
409            out.push_str(&format!("&before={}", urlencoding(&formatted)));
410        }
411        if !self.user.is_empty() {
412            out.push_str(&format!("&user={}", urlencoding(&self.user)));
413        }
414        out
415    }
416}
417
418/// AddTimeOption options for adding time to an issue
419#[derive(Debug, Clone, Serialize, Deserialize)]
420/// Options for Add Time Option.
421pub struct AddTimeOption {
422    /// time in seconds
423    pub time: i64,
424    /// optional
425    #[serde(
426        default,
427        with = "nullable_rfc3339",
428        skip_serializing_if = "Option::is_none"
429    )]
430    pub created: Option<OffsetDateTime>,
431    /// optional
432    #[serde(default, rename = "user_name")]
433    pub user: String,
434}
435
436impl AddTimeOption {
437    /// Validate the AddTimeOption struct
438    pub fn validate(&self) -> crate::Result<()> {
439        if self.time == 0 {
440            return Err(crate::Error::Validation("no time to add".to_string()));
441        }
442        Ok(())
443    }
444}
445
446// ── issue_ext.go ─────────────────────────────────────────────────
447
448/// ListIssueBlocksOptions options for listing issue blocks
449#[derive(Debug, Clone, Default)]
450/// Options for List Issue Blocks Option.
451pub struct ListIssueBlocksOptions {
452    pub list_options: ListOptions,
453}
454
455impl QueryEncode for ListIssueBlocksOptions {
456    fn query_encode(&self) -> String {
457        self.list_options.query_encode()
458    }
459}
460
461/// ListIssueDependenciesOptions options for listing issue dependencies
462#[derive(Debug, Clone, Default)]
463/// Options for List Issue Dependencies Option.
464pub struct ListIssueDependenciesOptions {
465    pub list_options: ListOptions,
466}
467
468impl QueryEncode for ListIssueDependenciesOptions {
469    fn query_encode(&self) -> String {
470        self.list_options.query_encode()
471    }
472}
473
474/// LockIssueOption represents options for locking an issue
475#[derive(Debug, Clone, Serialize, Deserialize)]
476/// Options for Lock Issue Option.
477pub struct LockIssueOption {
478    #[serde(default, rename = "lock_reason")]
479    pub lock_reason: String,
480}
481
482/// EditDeadlineOption represents options for updating issue deadline
483#[derive(Debug, Clone, Serialize, Deserialize)]
484/// Options for Edit Deadline Option.
485pub struct EditDeadlineOption {
486    #[serde(
487        rename = "due_date",
488        default,
489        with = "nullable_rfc3339",
490        skip_serializing_if = "Option::is_none"
491    )]
492    pub deadline: Option<OffsetDateTime>,
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_create_issue_option_validate_success() {
501        let opt = CreateIssueOption {
502            title: "bug report".to_string(),
503            body: String::new(),
504            r#ref: String::new(),
505            assignees: Vec::new(),
506            deadline: None,
507            milestone: 0,
508            labels: Vec::new(),
509            closed: false,
510        };
511        assert!(opt.validate().is_ok());
512    }
513
514    #[test]
515    fn test_create_issue_option_validate_empty_title() {
516        let opt = CreateIssueOption {
517            title: String::new(),
518            body: String::new(),
519            r#ref: String::new(),
520            assignees: Vec::new(),
521            deadline: None,
522            milestone: 0,
523            labels: Vec::new(),
524            closed: false,
525        };
526        assert!(opt.validate().is_err());
527    }
528
529    #[test]
530    fn test_create_issue_option_validate_whitespace_title() {
531        let opt = CreateIssueOption {
532            title: "   ".to_string(),
533            body: String::new(),
534            r#ref: String::new(),
535            assignees: Vec::new(),
536            deadline: None,
537            milestone: 0,
538            labels: Vec::new(),
539            closed: false,
540        };
541        assert!(opt.validate().is_err());
542    }
543
544    #[test]
545    fn test_edit_issue_option_validate_success() {
546        let opt = EditIssueOption {
547            title: Some("new title".to_string()),
548            body: None,
549            r#ref: None,
550            assignees: Vec::new(),
551            milestone: None,
552            state: None,
553            deadline: None,
554            remove_deadline: None,
555        };
556        assert!(opt.validate().is_ok());
557    }
558
559    #[test]
560    fn test_edit_issue_option_validate_empty_title() {
561        let opt = EditIssueOption {
562            title: Some("   ".to_string()),
563            body: None,
564            r#ref: None,
565            assignees: Vec::new(),
566            milestone: None,
567            state: None,
568            deadline: None,
569            remove_deadline: None,
570        };
571        assert!(opt.validate().is_err());
572    }
573
574    #[test]
575    fn test_create_issue_comment_option_validate_success() {
576        let opt = CreateIssueCommentOption {
577            body: "comment".to_string(),
578        };
579        assert!(opt.validate().is_ok());
580    }
581
582    #[test]
583    fn test_create_issue_comment_option_validate_empty_body() {
584        let opt = CreateIssueCommentOption {
585            body: String::new(),
586        };
587        assert!(opt.validate().is_err());
588    }
589
590    #[test]
591    fn test_edit_issue_comment_option_validate_success() {
592        let opt = EditIssueCommentOption {
593            body: "updated comment".to_string(),
594        };
595        assert!(opt.validate().is_ok());
596    }
597
598    #[test]
599    fn test_edit_issue_comment_option_validate_empty_body() {
600        let opt = EditIssueCommentOption {
601            body: String::new(),
602        };
603        assert!(opt.validate().is_err());
604    }
605
606    #[test]
607    fn test_create_milestone_option_validate_success() {
608        let opt = CreateMilestoneOption {
609            title: "v1.0".to_string(),
610            description: String::new(),
611            state: StateType::Open,
612            deadline: None,
613        };
614        assert!(opt.validate().is_ok());
615    }
616
617    #[test]
618    fn test_create_milestone_option_validate_empty_title() {
619        let opt = CreateMilestoneOption {
620            title: String::new(),
621            description: String::new(),
622            state: StateType::Open,
623            deadline: None,
624        };
625        assert!(opt.validate().is_err());
626    }
627
628    #[test]
629    fn test_edit_milestone_option_validate_success() {
630        let opt = EditMilestoneOption {
631            title: Some("v2.0".to_string()),
632            ..Default::default()
633        };
634        assert!(opt.validate().is_ok());
635    }
636
637    #[test]
638    fn test_edit_milestone_option_validate_empty_title() {
639        let opt = EditMilestoneOption {
640            title: Some("   ".to_string()),
641            ..Default::default()
642        };
643        assert!(opt.validate().is_err());
644    }
645
646    #[test]
647    fn test_add_time_option_validate_success() {
648        let opt = AddTimeOption {
649            time: 3600,
650            created: None,
651            user: String::new(),
652        };
653        assert!(opt.validate().is_ok());
654    }
655
656    #[test]
657    fn test_add_time_option_validate_zero_time() {
658        let opt = AddTimeOption {
659            time: 0,
660            created: None,
661            user: String::new(),
662        };
663        assert!(opt.validate().is_err());
664    }
665}