gitlab/api/projects/releases/
create.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use crate::api::common::NameOrId;
8use crate::api::endpoint_prelude::*;
9use chrono::{DateTime, Utc};
10use derive_builder::Builder;
11use serde_json::json;
12
13use crate::api::projects::releases::links::LinkType;
14
15/// Asset link.
16///
17/// Used to create permalinks for your release.
18#[derive(Debug, Builder, Clone)]
19#[builder(setter(strip_option))]
20pub struct CreateReleaseAssetLinks<'a> {
21    /// The name of the link.
22    ///
23    /// Link names must be unique in the release.
24    #[builder(setter(into))]
25    name: Cow<'a, str>,
26
27    /// The URL of the link.
28    ///
29    /// Link URLs must be unique in the release.
30    #[builder(setter(into))]
31    url: Cow<'a, str>,
32
33    /// Optional path for a direct asset link.
34    #[builder(setter(into), default)]
35    direct_asset_path: Option<Cow<'a, str>>,
36
37    /// The type of the link: other, runbook, image, package.
38    ///
39    /// Defaults to other.
40    #[builder(default)]
41    link_type: Option<LinkType>,
42}
43
44impl<'a> CreateReleaseAssetLinks<'a> {
45    /// Create a builder for the endpoint.
46    pub fn builder() -> CreateReleaseAssetLinksBuilder<'a> {
47        CreateReleaseAssetLinksBuilder::default()
48    }
49
50    fn as_json(&self) -> serde_json::Value {
51        JsonParams::clean(json!({
52            "name": self.name,
53            "url": self.url,
54            "direct_asset_path": self.direct_asset_path,
55            "link_type": self.link_type.map(|lt| lt.as_str()),
56        }))
57    }
58}
59
60/// Creates a release.
61///
62/// Developer level access to the project is required to create a release.
63#[derive(Debug, Builder, Clone)]
64#[builder(setter(strip_option))]
65pub struct CreateRelease<'a> {
66    /// The project to query for the packages.
67    #[builder(setter(into))]
68    project: NameOrId<'a>,
69
70    /// The name of the link.
71    ///
72    /// Link names must be unique in the release.
73    #[builder(setter(into), default)]
74    name: Option<Cow<'a, str>>,
75
76    /// The tag associated with the release.
77    #[builder(setter(into))]
78    tag_name: Cow<'a, str>,
79
80    /// Message to use if creating a new annotated tag.
81    #[builder(setter(into), default)]
82    tag_message: Option<Cow<'a, str>>,
83
84    /// The description of the release.
85    ///
86    /// You can use Markdown.
87    #[builder(setter(into), default)]
88    description: Option<Cow<'a, str>>,
89
90    /// If a tag specified in tag_name doesn’t exist,
91    /// the release is created from ref and tagged with tag_name.
92    ///
93    /// It can be a commit SHA, another tag name, or a branch name.
94    #[builder(setter(into), default)]
95    ref_sha: Option<Cow<'a, str>>,
96
97    /// The title of each milestone the release is associated with.
98    #[builder(setter(name = "_milestones"), default, private)]
99    milestones: Vec<Cow<'a, str>>,
100
101    /// An array of assets links.
102    #[builder(setter(name = "_assets"), default, private)]
103    assets: Vec<CreateReleaseAssetLinks<'a>>,
104
105    /// Date and time for the release.
106    ///
107    /// Defaults to the current time.
108    /// Only provide this field if creating an upcoming
109    /// or historical release.
110    #[builder(default)]
111    released_at: Option<DateTime<Utc>>,
112}
113
114impl<'a> CreateRelease<'a> {
115    /// Create a builder for the endpoint.
116    pub fn builder() -> CreateReleaseBuilder<'a> {
117        CreateReleaseBuilder::default()
118    }
119
120    /// Creates a JSON string of the data for the endpoint
121    fn as_json(&self) -> serde_json::Value {
122        JsonParams::clean(json!({
123            "name": self.name,
124            "tag_name": self.tag_name,
125            "tag_message": self.tag_message,
126            "description": self.description,
127            "ref": self.ref_sha,
128            "milestones": self.milestones,
129            "released_at": self.released_at,
130            "assets": JsonParams::clean(json!({
131                "links": self.assets
132                    .iter()
133                    .map(|a| a.as_json())
134                    .collect::<Vec<_>>()
135            })),
136        }))
137    }
138}
139
140impl<'a> CreateReleaseBuilder<'a> {
141    /// The title of a milestone the release is associated with.
142    pub fn milestone<M>(&mut self, milestone: M) -> &mut Self
143    where
144        M: Into<Cow<'a, str>>,
145    {
146        self.milestones
147            .get_or_insert_with(Vec::new)
148            .push(milestone.into());
149        self
150    }
151
152    /// The title of milestones the release is associated with.
153    pub fn milestones<I, M>(&mut self, milestones: I) -> &mut Self
154    where
155        I: Iterator<Item = M>,
156        M: Into<Cow<'a, str>>,
157    {
158        self.milestones
159            .get_or_insert_with(Vec::new)
160            .extend(milestones.map(Into::into));
161        self
162    }
163
164    /// A link to an asset in the release.
165    pub fn asset(&mut self, asset: CreateReleaseAssetLinks<'a>) -> &mut Self {
166        self.assets.get_or_insert_with(Vec::new).push(asset);
167        self
168    }
169
170    /// An iterator over links to assets in the release.
171    pub fn assets<I>(&mut self, assets: I) -> &mut Self
172    where
173        I: Iterator<Item = CreateReleaseAssetLinks<'a>>,
174    {
175        self.assets.get_or_insert_with(Vec::new).extend(assets);
176        self
177    }
178}
179
180impl Endpoint for CreateRelease<'_> {
181    fn method(&self) -> Method {
182        Method::POST
183    }
184
185    fn endpoint(&self) -> Cow<'static, str> {
186        format!("projects/{}/releases", self.project).into()
187    }
188
189    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
190        JsonParams::into_body(&self.as_json())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use chrono::DateTime;
197    use http::Method;
198
199    use crate::{
200        api::{
201            self,
202            projects::releases::{CreateReleaseAssetLinks, CreateReleaseAssetLinksBuilderError},
203            Query,
204        },
205        test::client::{ExpectedUrl, SingleTestClient},
206    };
207
208    use super::{CreateRelease, CreateReleaseBuilderError};
209
210    #[test]
211    fn assets_url_is_needed() {
212        let err = CreateReleaseAssetLinks::builder()
213            .name("test")
214            .build()
215            .unwrap_err();
216
217        crate::test::assert_missing_field!(err, CreateReleaseAssetLinksBuilderError, "url");
218    }
219
220    #[test]
221    fn assets_name_is_needed() {
222        let err = CreateReleaseAssetLinks::builder()
223            .url("https://img.com/img.png")
224            .build()
225            .unwrap_err();
226
227        crate::test::assert_missing_field!(err, CreateReleaseAssetLinksBuilderError, "name");
228    }
229
230    #[test]
231    fn assets_required_parameter_are_sufficient() {
232        CreateReleaseAssetLinks::builder()
233            .name("test")
234            .url("https://test.com/")
235            .build()
236            .unwrap();
237    }
238
239    #[test]
240    fn project_is_needed() {
241        let err = CreateRelease::builder()
242            .tag_name("1.2.3")
243            .build()
244            .unwrap_err();
245
246        crate::test::assert_missing_field!(err, CreateReleaseBuilderError, "project");
247    }
248
249    #[test]
250    fn tag_name_is_needed() {
251        let err = CreateRelease::builder().project(123).build().unwrap_err();
252
253        crate::test::assert_missing_field!(err, CreateReleaseBuilderError, "tag_name");
254    }
255
256    #[test]
257    fn project_and_tag_name_are_sufficient() {
258        CreateRelease::builder()
259            .project(1)
260            .tag_name("1.2.3")
261            .build()
262            .unwrap();
263    }
264
265    #[test]
266    fn endpoint() {
267        let endpoint = ExpectedUrl::builder()
268            .method(Method::POST)
269            .endpoint("projects/1337/releases")
270            .content_type("application/json")
271            .body_str("{\"tag_name\":\"1.2.3\"}")
272            .build()
273            .unwrap();
274        let client = SingleTestClient::new_raw(endpoint, "");
275
276        let endpoint = CreateRelease::builder()
277            .project(1337)
278            .tag_name("1.2.3")
279            .build()
280            .unwrap();
281        api::ignore(endpoint).query(&client).unwrap();
282    }
283
284    #[test]
285    fn endpoint_name() {
286        let endpoint = ExpectedUrl::builder()
287            .method(Method::POST)
288            .endpoint("projects/1337/releases")
289            .content_type("application/json")
290            .body_str(concat!(
291                "{",
292                "\"name\":\"Test\",",
293                "\"tag_name\":\"1.2.3\"",
294                "}",
295            ))
296            .build()
297            .unwrap();
298        let client = SingleTestClient::new_raw(endpoint, "");
299
300        let endpoint = CreateRelease::builder()
301            .project(1337)
302            .tag_name("1.2.3")
303            .name("Test")
304            .build()
305            .unwrap();
306        api::ignore(endpoint).query(&client).unwrap();
307    }
308
309    #[test]
310    fn endpoint_tag_message() {
311        let endpoint = ExpectedUrl::builder()
312            .method(Method::POST)
313            .endpoint("projects/1337/releases")
314            .content_type("application/json")
315            .body_str(concat!(
316                "{",
317                "\"tag_message\":\"Test message\",",
318                "\"tag_name\":\"1.2.3\"",
319                "}",
320            ))
321            .build()
322            .unwrap();
323        let client = SingleTestClient::new_raw(endpoint, "");
324
325        let endpoint = CreateRelease::builder()
326            .project(1337)
327            .tag_name("1.2.3")
328            .tag_message("Test message")
329            .build()
330            .unwrap();
331        api::ignore(endpoint).query(&client).unwrap();
332    }
333
334    #[test]
335    fn endpoint_description() {
336        let endpoint = ExpectedUrl::builder()
337            .method(Method::POST)
338            .endpoint("projects/1337/releases")
339            .content_type("application/json")
340            .body_str(concat!(
341                "{",
342                "\"description\":\"Test description\",",
343                "\"tag_name\":\"1.2.3\"",
344                "}",
345            ))
346            .build()
347            .unwrap();
348        let client = SingleTestClient::new_raw(endpoint, "");
349
350        let endpoint = CreateRelease::builder()
351            .project(1337)
352            .tag_name("1.2.3")
353            .description("Test description")
354            .build()
355            .unwrap();
356        api::ignore(endpoint).query(&client).unwrap();
357    }
358
359    #[test]
360    fn endpoint_ref() {
361        let endpoint = ExpectedUrl::builder()
362            .method(Method::POST)
363            .endpoint("projects/1337/releases")
364            .content_type("application/json")
365            .body_str(concat!(
366                "{",
367                "\"ref\":\"abfc1234\",",
368                "\"tag_name\":\"1.2.3\"",
369                "}",
370            ))
371            .build()
372            .unwrap();
373        let client = SingleTestClient::new_raw(endpoint, "");
374
375        let endpoint = CreateRelease::builder()
376            .project(1337)
377            .tag_name("1.2.3")
378            .ref_sha("abfc1234")
379            .build()
380            .unwrap();
381        api::ignore(endpoint).query(&client).unwrap();
382    }
383
384    #[test]
385    fn endpoint_milestones() {
386        let endpoint = ExpectedUrl::builder()
387            .method(Method::POST)
388            .endpoint("projects/1337/releases")
389            .content_type("application/json")
390            .body_str(concat!(
391                "{",
392                "\"milestones\":[",
393                "\"milestone_1\",",
394                "\"milestone_2\",",
395                "\"milestone_3\"",
396                "],",
397                "\"tag_name\":\"1.2.3\"",
398                "}",
399            ))
400            .build()
401            .unwrap();
402        let client = SingleTestClient::new_raw(endpoint, "");
403
404        let endpoint = CreateRelease::builder()
405            .project(1337)
406            .tag_name("1.2.3")
407            .milestones(["milestone_1", "milestone_2"].iter().copied())
408            .milestone("milestone_3")
409            .build()
410            .unwrap();
411        api::ignore(endpoint).query(&client).unwrap();
412    }
413
414    #[test]
415    fn endpoint_released_at() {
416        let endpoint = ExpectedUrl::builder()
417            .method(Method::POST)
418            .endpoint("projects/1337/releases")
419            .content_type("application/json")
420            .body_str(concat!(
421                "{",
422                "\"released_at\":\"2023-12-16T12:00:00Z\",",
423                "\"tag_name\":\"1.2.3\"",
424                "}",
425            ))
426            .build()
427            .unwrap();
428        let client = SingleTestClient::new_raw(endpoint, "");
429
430        let endpoint = CreateRelease::builder()
431            .project(1337)
432            .tag_name("1.2.3")
433            .released_at(
434                DateTime::parse_from_rfc3339("2023-12-16T12:00:00Z")
435                    .unwrap()
436                    .into(),
437            )
438            .build()
439            .unwrap();
440        api::ignore(endpoint).query(&client).unwrap();
441    }
442
443    #[test]
444    fn endpoint_assets() {
445        let endpoint = ExpectedUrl::builder()
446            .method(Method::POST)
447            .endpoint("projects/1337/releases")
448            .content_type("application/json")
449            .body_str(concat!(
450                "{",
451                "\"assets\":",
452                "{",
453                "\"links\":",
454                "[",
455                "{",
456                "\"name\":\"Test url 1\",",
457                "\"url\":\"https://test.com/test-1.zip\"",
458                "},",
459                "{",
460                "\"name\":\"Test url 2\",",
461                "\"url\":\"https://test.com/test-2.zip\"",
462                "},",
463                "{",
464                "\"name\":\"Test url 3\",",
465                "\"url\":\"https://test.com/test-3.zip\"",
466                "}",
467                "]",
468                "},",
469                "\"tag_name\":\"1.2.3\"",
470                "}",
471            ))
472            .build()
473            .unwrap();
474        let client = SingleTestClient::new_raw(endpoint, "");
475
476        let endpoint = CreateRelease::builder()
477            .project(1337)
478            .tag_name("1.2.3")
479            .asset(
480                CreateReleaseAssetLinks::builder()
481                    .name("Test url 1")
482                    .url("https://test.com/test-1.zip")
483                    .build()
484                    .unwrap(),
485            )
486            .assets(
487                [
488                    CreateReleaseAssetLinks::builder()
489                        .name("Test url 2")
490                        .url("https://test.com/test-2.zip")
491                        .build()
492                        .unwrap(),
493                    CreateReleaseAssetLinks::builder()
494                        .name("Test url 3")
495                        .url("https://test.com/test-3.zip")
496                        .build()
497                        .unwrap(),
498                ]
499                .into_iter(),
500            )
501            .build()
502            .unwrap();
503        api::ignore(endpoint).query(&client).unwrap();
504    }
505
506    #[test]
507    fn endpoint_assets_direct_asset_path() {
508        let endpoint = ExpectedUrl::builder()
509            .method(Method::POST)
510            .endpoint("projects/1337/releases")
511            .content_type("application/json")
512            .body_str(concat!(
513                "{",
514                "\"assets\":",
515                "{",
516                "\"links\":",
517                "[",
518                "{",
519                "\"direct_asset_path\":\"bin/test.zip\",",
520                "\"name\":\"Test url\",",
521                "\"url\":\"https://test.com/test.zip\"",
522                "}",
523                "]",
524                "},",
525                "\"tag_name\":\"1.2.3\"",
526                "}",
527            ))
528            .build()
529            .unwrap();
530        let client = SingleTestClient::new_raw(endpoint, "");
531
532        let endpoint = CreateRelease::builder()
533            .project(1337)
534            .tag_name("1.2.3")
535            .asset(
536                CreateReleaseAssetLinks::builder()
537                    .name("Test url")
538                    .url("https://test.com/test.zip")
539                    .direct_asset_path("bin/test.zip")
540                    .build()
541                    .unwrap(),
542            )
543            .build()
544            .unwrap();
545        api::ignore(endpoint).query(&client).unwrap();
546    }
547
548    #[test]
549    fn endpoint_assets_link_type() {
550        let endpoint = ExpectedUrl::builder()
551            .method(Method::POST)
552            .endpoint("projects/1337/releases")
553            .content_type("application/json")
554            .body_str(concat!(
555                "{",
556                "\"assets\":",
557                "{",
558                "\"links\":",
559                "[",
560                "{",
561                "\"link_type\":\"other\",",
562                "\"name\":\"Test url\",",
563                "\"url\":\"https://test.com/test.zip\"",
564                "}",
565                "]",
566                "},",
567                "\"tag_name\":\"1.2.3\"",
568                "}",
569            ))
570            .build()
571            .unwrap();
572        let client = SingleTestClient::new_raw(endpoint, "");
573
574        let endpoint = CreateRelease::builder()
575            .project(1337)
576            .tag_name("1.2.3")
577            .asset(
578                CreateReleaseAssetLinks::builder()
579                    .name("Test url")
580                    .url("https://test.com/test.zip")
581                    .link_type(api::projects::releases::links::LinkType::Other)
582                    .build()
583                    .unwrap(),
584            )
585            .build()
586            .unwrap();
587        api::ignore(endpoint).query(&client).unwrap();
588    }
589}