gitlab/api/projects/issues/
create.rs1use std::collections::BTreeSet;
8
9use chrono::{DateTime, NaiveDate, Utc};
10use derive_builder::Builder;
11
12use crate::api::common::{CommaSeparatedList, NameOrId};
13use crate::api::endpoint_prelude::*;
14use crate::api::issues::IssueType;
15
16#[derive(Debug, Builder, Clone)]
18#[builder(setter(strip_option))]
19pub struct CreateIssue<'a> {
20 #[builder(setter(into))]
22 project: NameOrId<'a>,
23 #[builder(setter(into))]
31 title: Cow<'a, str>,
32
33 #[builder(default)]
37 iid: Option<u64>,
38 #[builder(setter(into), default)]
40 description: Option<Cow<'a, str>>,
41 #[builder(default)]
43 confidential: Option<bool>,
44 #[builder(setter(name = "_assignee_ids"), default, private)]
46 assignee_ids: BTreeSet<u64>,
47 #[builder(default)]
49 milestone_id: Option<u64>,
50 #[builder(setter(name = "_labels"), default, private)]
52 labels: Option<CommaSeparatedList<Cow<'a, str>>>,
53 #[builder(default)]
57 created_at: Option<DateTime<Utc>>,
58 #[builder(default)]
60 due_date: Option<NaiveDate>,
61 #[builder(default)]
65 merge_request_to_resolve_discussions_of: Option<u64>,
66 #[builder(setter(into), default)]
68 discussion_to_resolve: Option<Cow<'a, str>>,
69 #[builder(default)]
71 weight: Option<u64>,
72 #[builder(default)]
74 epic_id: Option<u64>,
75 #[builder(default)]
77 issue_type: Option<IssueType>,
78}
79
80impl<'a> CreateIssue<'a> {
81 pub fn builder() -> CreateIssueBuilder<'a> {
83 CreateIssueBuilder::default()
84 }
85}
86
87impl<'a> CreateIssueBuilder<'a> {
88 pub fn assignee_id(&mut self, assignee: u64) -> &mut Self {
90 self.assignee_ids
91 .get_or_insert_with(BTreeSet::new)
92 .insert(assignee);
93 self
94 }
95
96 pub fn assignee_ids<I>(&mut self, iter: I) -> &mut Self
98 where
99 I: Iterator<Item = u64>,
100 {
101 self.assignee_ids
102 .get_or_insert_with(BTreeSet::new)
103 .extend(iter);
104 self
105 }
106
107 pub fn label<L>(&mut self, label: L) -> &mut Self
109 where
110 L: Into<Cow<'a, str>>,
111 {
112 self.labels
113 .get_or_insert(None)
114 .get_or_insert_with(CommaSeparatedList::new)
115 .push(label.into());
116 self
117 }
118
119 pub fn labels<I, L>(&mut self, iter: I) -> &mut Self
121 where
122 I: IntoIterator<Item = L>,
123 L: Into<Cow<'a, str>>,
124 {
125 self.labels
126 .get_or_insert(None)
127 .get_or_insert_with(CommaSeparatedList::new)
128 .extend(iter.into_iter().map(Into::into));
129 self
130 }
131}
132
133impl Endpoint for CreateIssue<'_> {
134 fn method(&self) -> Method {
135 Method::POST
136 }
137
138 fn endpoint(&self) -> Cow<'static, str> {
139 format!("projects/{}/issues", self.project).into()
140 }
141
142 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
143 let mut params = FormParams::default();
144
145 if !self.title.is_empty() || self.merge_request_to_resolve_discussions_of.is_none() {
146 params.push("title", &self.title);
147 }
148
149 params
150 .push_opt("iid", self.iid)
151 .push_opt("description", self.description.as_ref())
152 .push_opt("confidential", self.confidential)
153 .extend(
154 self.assignee_ids
155 .iter()
156 .map(|&value| ("assignee_ids[]", value)),
157 )
158 .push_opt("milestone_id", self.milestone_id)
159 .push_opt("labels", self.labels.as_ref())
160 .push_opt("created_at", self.created_at)
161 .push_opt("due_date", self.due_date)
162 .push_opt(
163 "merge_request_to_resolve_discussions_of",
164 self.merge_request_to_resolve_discussions_of,
165 )
166 .push_opt("discussion_to_resolve", self.discussion_to_resolve.as_ref())
167 .push_opt("weight", self.weight)
168 .push_opt("epic_id", self.epic_id)
169 .push_opt("issue_type", self.issue_type);
170
171 params.into_body()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use chrono::{NaiveDate, TimeZone, Utc};
178 use http::Method;
179
180 use crate::api::issues::IssueType;
181 use crate::api::projects::issues::{CreateIssue, CreateIssueBuilderError};
182 use crate::api::{self, Query};
183 use crate::test::client::{ExpectedUrl, SingleTestClient};
184
185 #[test]
186 fn project_and_title_are_necessary() {
187 let err = CreateIssue::builder().build().unwrap_err();
188 crate::test::assert_missing_field!(err, CreateIssueBuilderError, "project");
189 }
190
191 #[test]
192 fn project_is_necessary() {
193 let err = CreateIssue::builder().title("title").build().unwrap_err();
194 crate::test::assert_missing_field!(err, CreateIssueBuilderError, "project");
195 }
196
197 #[test]
198 fn title_is_necessary() {
199 let err = CreateIssue::builder().project(1).build().unwrap_err();
200 crate::test::assert_missing_field!(err, CreateIssueBuilderError, "title");
201 }
202
203 #[test]
204 fn project_and_title_are_sufficient() {
205 CreateIssue::builder()
206 .project(1)
207 .title("title")
208 .build()
209 .unwrap();
210 }
211
212 #[test]
213 fn endpoint() {
214 let endpoint = ExpectedUrl::builder()
215 .method(Method::POST)
216 .endpoint("projects/simple%2Fproject/issues")
217 .content_type("application/x-www-form-urlencoded")
218 .body_str("title=title+of+issue")
219 .build()
220 .unwrap();
221 let client = SingleTestClient::new_raw(endpoint, "");
222
223 let endpoint = CreateIssue::builder()
224 .project("simple/project")
225 .title("title of issue")
226 .build()
227 .unwrap();
228 api::ignore(endpoint).query(&client).unwrap();
229 }
230
231 #[test]
232 fn endpoint_iid() {
233 let endpoint = ExpectedUrl::builder()
234 .method(Method::POST)
235 .endpoint("projects/simple%2Fproject/issues")
236 .content_type("application/x-www-form-urlencoded")
237 .body_str(concat!("title=title", "&iid=1"))
238 .build()
239 .unwrap();
240 let client = SingleTestClient::new_raw(endpoint, "");
241
242 let endpoint = CreateIssue::builder()
243 .project("simple/project")
244 .title("title")
245 .iid(1)
246 .build()
247 .unwrap();
248 api::ignore(endpoint).query(&client).unwrap();
249 }
250
251 #[test]
252 fn endpoint_description() {
253 let endpoint = ExpectedUrl::builder()
254 .method(Method::POST)
255 .endpoint("projects/simple%2Fproject/issues")
256 .content_type("application/x-www-form-urlencoded")
257 .body_str(concat!("title=title", "&description=description"))
258 .build()
259 .unwrap();
260 let client = SingleTestClient::new_raw(endpoint, "");
261
262 let endpoint = CreateIssue::builder()
263 .project("simple/project")
264 .title("title")
265 .description("description")
266 .build()
267 .unwrap();
268 api::ignore(endpoint).query(&client).unwrap();
269 }
270
271 #[test]
272 fn endpoint_confidential() {
273 let endpoint = ExpectedUrl::builder()
274 .method(Method::POST)
275 .endpoint("projects/simple%2Fproject/issues")
276 .content_type("application/x-www-form-urlencoded")
277 .body_str(concat!("title=title", "&confidential=true"))
278 .build()
279 .unwrap();
280 let client = SingleTestClient::new_raw(endpoint, "");
281
282 let endpoint = CreateIssue::builder()
283 .project("simple/project")
284 .title("title")
285 .confidential(true)
286 .build()
287 .unwrap();
288 api::ignore(endpoint).query(&client).unwrap();
289 }
290
291 #[test]
292 fn endpoint_assignee_ids() {
293 let endpoint = ExpectedUrl::builder()
294 .method(Method::POST)
295 .endpoint("projects/simple%2Fproject/issues")
296 .content_type("application/x-www-form-urlencoded")
297 .body_str(concat!(
298 "title=title",
299 "&assignee_ids%5B%5D=1",
300 "&assignee_ids%5B%5D=2",
301 ))
302 .build()
303 .unwrap();
304 let client = SingleTestClient::new_raw(endpoint, "");
305
306 let endpoint = CreateIssue::builder()
307 .project("simple/project")
308 .title("title")
309 .assignee_id(1)
310 .assignee_ids([1, 2].iter().copied())
311 .build()
312 .unwrap();
313 api::ignore(endpoint).query(&client).unwrap();
314 }
315
316 #[test]
317 fn endpoint_milestone_id() {
318 let endpoint = ExpectedUrl::builder()
319 .method(Method::POST)
320 .endpoint("projects/simple%2Fproject/issues")
321 .content_type("application/x-www-form-urlencoded")
322 .body_str(concat!("title=title", "&milestone_id=1"))
323 .build()
324 .unwrap();
325 let client = SingleTestClient::new_raw(endpoint, "");
326
327 let endpoint = CreateIssue::builder()
328 .project("simple/project")
329 .title("title")
330 .milestone_id(1)
331 .build()
332 .unwrap();
333 api::ignore(endpoint).query(&client).unwrap();
334 }
335
336 #[test]
337 fn endpoint_labels() {
338 let endpoint = ExpectedUrl::builder()
339 .method(Method::POST)
340 .endpoint("projects/simple%2Fproject/issues")
341 .content_type("application/x-www-form-urlencoded")
342 .body_str(concat!("title=title", "&labels=label"))
343 .build()
344 .unwrap();
345 let client = SingleTestClient::new_raw(endpoint, "");
346
347 let endpoint = CreateIssue::builder()
348 .project("simple/project")
349 .title("title")
350 .label("label")
351 .build()
352 .unwrap();
353 api::ignore(endpoint).query(&client).unwrap();
354 }
355
356 #[test]
357 fn endpoint_labels_multiple() {
358 let endpoint = ExpectedUrl::builder()
359 .method(Method::POST)
360 .endpoint("projects/simple%2Fproject/issues")
361 .content_type("application/x-www-form-urlencoded")
362 .body_str(concat!("title=title", "&labels=label1%2Clabel2"))
363 .build()
364 .unwrap();
365 let client = SingleTestClient::new_raw(endpoint, "");
366
367 let endpoint = CreateIssue::builder()
368 .project("simple/project")
369 .title("title")
370 .labels(["label1", "label2"].iter().copied())
371 .build()
372 .unwrap();
373 api::ignore(endpoint).query(&client).unwrap();
374 }
375
376 #[test]
377 fn endpoint_created_at() {
378 let endpoint = ExpectedUrl::builder()
379 .method(Method::POST)
380 .endpoint("projects/simple%2Fproject/issues")
381 .content_type("application/x-www-form-urlencoded")
382 .body_str(concat!(
383 "title=title",
384 "&created_at=2020-01-01T00%3A00%3A00Z",
385 ))
386 .build()
387 .unwrap();
388 let client = SingleTestClient::new_raw(endpoint, "");
389
390 let endpoint = CreateIssue::builder()
391 .project("simple/project")
392 .title("title")
393 .created_at(Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap())
394 .build()
395 .unwrap();
396 api::ignore(endpoint).query(&client).unwrap();
397 }
398
399 #[test]
400 fn endpoint_due_date() {
401 let endpoint = ExpectedUrl::builder()
402 .method(Method::POST)
403 .endpoint("projects/simple%2Fproject/issues")
404 .content_type("application/x-www-form-urlencoded")
405 .body_str(concat!("title=title", "&due_date=2020-01-01"))
406 .build()
407 .unwrap();
408 let client = SingleTestClient::new_raw(endpoint, "");
409
410 let endpoint = CreateIssue::builder()
411 .project("simple/project")
412 .title("title")
413 .due_date(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap())
414 .build()
415 .unwrap();
416 api::ignore(endpoint).query(&client).unwrap();
417 }
418
419 #[test]
420 fn endpoint_merge_request_to_resolve_discussions_of() {
421 let endpoint = ExpectedUrl::builder()
422 .method(Method::POST)
423 .endpoint("projects/simple%2Fproject/issues")
424 .content_type("application/x-www-form-urlencoded")
425 .body_str(concat!(
426 "title=title",
427 "&merge_request_to_resolve_discussions_of=1",
428 ))
429 .build()
430 .unwrap();
431 let client = SingleTestClient::new_raw(endpoint, "");
432
433 let endpoint = CreateIssue::builder()
434 .project("simple/project")
435 .title("title")
436 .merge_request_to_resolve_discussions_of(1)
437 .build()
438 .unwrap();
439 api::ignore(endpoint).query(&client).unwrap();
440 }
441
442 #[test]
443 fn endpoint_merge_request_to_resolve_discussions_of_no_title() {
444 let endpoint = ExpectedUrl::builder()
445 .method(Method::POST)
446 .endpoint("projects/simple%2Fproject/issues")
447 .content_type("application/x-www-form-urlencoded")
448 .body_str("merge_request_to_resolve_discussions_of=1")
449 .build()
450 .unwrap();
451 let client = SingleTestClient::new_raw(endpoint, "");
452
453 let endpoint = CreateIssue::builder()
454 .project("simple/project")
455 .title("") .merge_request_to_resolve_discussions_of(1)
457 .build()
458 .unwrap();
459 api::ignore(endpoint).query(&client).unwrap();
460 }
461
462 #[test]
463 fn endpoint_discussion_to_resolve() {
464 let endpoint = ExpectedUrl::builder()
465 .method(Method::POST)
466 .endpoint("projects/simple%2Fproject/issues")
467 .content_type("application/x-www-form-urlencoded")
468 .body_str(concat!("title=title", "&discussion_to_resolve=deadbeef"))
469 .build()
470 .unwrap();
471 let client = SingleTestClient::new_raw(endpoint, "");
472
473 let endpoint = CreateIssue::builder()
474 .project("simple/project")
475 .title("title")
476 .discussion_to_resolve("deadbeef")
477 .build()
478 .unwrap();
479 api::ignore(endpoint).query(&client).unwrap();
480 }
481
482 #[test]
483 fn endpoint_weight() {
484 let endpoint = ExpectedUrl::builder()
485 .method(Method::POST)
486 .endpoint("projects/simple%2Fproject/issues")
487 .content_type("application/x-www-form-urlencoded")
488 .body_str(concat!("title=title", "&weight=1"))
489 .build()
490 .unwrap();
491 let client = SingleTestClient::new_raw(endpoint, "");
492
493 let endpoint = CreateIssue::builder()
494 .project("simple/project")
495 .title("title")
496 .weight(1)
497 .build()
498 .unwrap();
499 api::ignore(endpoint).query(&client).unwrap();
500 }
501
502 #[test]
503 fn endpoint_epic_id() {
504 let endpoint = ExpectedUrl::builder()
505 .method(Method::POST)
506 .endpoint("projects/simple%2Fproject/issues")
507 .content_type("application/x-www-form-urlencoded")
508 .body_str(concat!("title=title", "&epic_id=1"))
509 .build()
510 .unwrap();
511 let client = SingleTestClient::new_raw(endpoint, "");
512
513 let endpoint = CreateIssue::builder()
514 .project("simple/project")
515 .title("title")
516 .epic_id(1)
517 .build()
518 .unwrap();
519 api::ignore(endpoint).query(&client).unwrap();
520 }
521
522 #[test]
523 fn endpoint_issue_type() {
524 let endpoint = ExpectedUrl::builder()
525 .method(Method::POST)
526 .endpoint("projects/simple%2Fproject/issues")
527 .content_type("application/x-www-form-urlencoded")
528 .body_str(concat!("title=title", "&issue_type=test_case"))
529 .build()
530 .unwrap();
531 let client = SingleTestClient::new_raw(endpoint, "");
532
533 let endpoint = CreateIssue::builder()
534 .project("simple/project")
535 .title("title")
536 .issue_type(IssueType::TestCase)
537 .build()
538 .unwrap();
539 api::ignore(endpoint).query(&client).unwrap();
540 }
541}