trello/
card.rs

1use crate::client::TrelloClient;
2use crate::formatting::header;
3use crate::label::Label;
4use crate::trello_error::TrelloError;
5use crate::trello_object::{Renderable, TrelloObject};
6
7use chrono::{DateTime, Utc};
8use colored::Colorize;
9use serde::Deserialize;
10use std::str::FromStr;
11
12type Result<T> = std::result::Result<T, TrelloError>;
13
14// https://developer.atlassian.com/cloud/trello/guides/rest-api/object-definitions/#card-object
15#[derive(Deserialize, Debug, Eq, PartialEq, Clone)]
16#[serde(rename_all = "camelCase")]
17pub struct Card {
18    pub id: String,
19    pub name: String,
20    pub desc: String,
21    pub closed: bool,
22    pub url: String,
23    pub labels: Option<Vec<Label>>,
24    pub due: Option<DateTime<Utc>>,
25}
26
27impl TrelloObject for Card {
28    fn get_type() -> String {
29        String::from("Card")
30    }
31
32    fn get_name(&self) -> &str {
33        &self.name
34    }
35
36    fn get_fields() -> &'static [&'static str] {
37        &["id", "name", "desc", "labels", "closed", "due", "url"]
38    }
39}
40
41impl Renderable for Card {
42    fn render(&self, headers: bool) -> String {
43        match headers {
44            true => [header(&self.name, "=").as_str(), &self.desc].join("\n"),
45            false => self.desc.clone(),
46        }
47    }
48
49    fn simple_render(&self) -> String {
50        let mut lformat: Vec<String> = vec![];
51
52        if self.closed {
53            lformat.push("[Closed]".red().to_string());
54        }
55
56        lformat.push(String::from(&self.name));
57
58        if self.desc != "" {
59            lformat.push("[...]".dimmed().to_string());
60        }
61
62        if let Some(labels) = &self.labels {
63            for l in labels {
64                lformat.push(l.simple_render());
65            }
66        }
67
68        // trim end in case there is no data presented by lformat
69        lformat.join(" ").trim_end().to_string()
70    }
71}
72
73#[derive(Debug, PartialEq, Eq)]
74pub struct CardContents {
75    pub name: String,
76    pub desc: String,
77}
78
79impl FromStr for CardContents {
80    type Err = TrelloError;
81
82    /// Takes a buffer of contents that represent a Card render and parses
83    /// it into a CardContents structure. This is similar to a deserialization process
84    /// except this is quite unstructured and is not very strict in order to allow
85    /// the user to more easily edit card contents.
86    /// ```
87    /// # fn main() -> Result<(), trello::TrelloError> {
88    /// let buffer = "Hello World\n===\nThis is my card";
89    /// let card_contents: trello::CardContents = buffer.parse()?;
90    ///
91    /// assert_eq!(
92    ///     card_contents,
93    ///     trello::CardContents {
94    ///         name: String::from("Hello World"),
95    ///         desc: String::from("This is my card"),
96    ///     },
97    /// );
98    /// # Ok(())
99    /// # }
100    /// ```
101    /// Invalid data will result in an appropriate error being returned.
102    fn from_str(value: &str) -> Result<CardContents> {
103        // this is guaranteed to give at least one result
104        let mut contents = value.split('\n').collect::<Vec<&str>>();
105        trace!("{:?}", contents);
106
107        // first line should *always* be the name of the card
108        let mut name = vec![contents.remove(0)];
109
110        // continue generating the name until we find a line entirely composed of '='
111        // we cannot calculate header() here because we allow the user the benefit of not
112        // having to add or remove characters in case the name grows or shrinks in size
113        let mut found = false;
114        while !contents.is_empty() {
115            let line = contents.remove(0);
116
117            if line.chars().take_while(|c| c == &'=').collect::<String>() != line {
118                name.push(line);
119            } else {
120                found = true;
121                break;
122            }
123        }
124
125        if !found {
126            return Err(TrelloError::CardParse(
127                "Unable to find name delimiter '===='".to_owned(),
128            ));
129        }
130
131        let name = name.join("\n");
132        // The rest of the contents is assumed to be the description
133        let desc = contents.join("\n");
134
135        Ok(CardContents { name, desc })
136    }
137}
138
139impl Card {
140    pub fn new(
141        id: &str,
142        name: &str,
143        desc: &str,
144        labels: Option<Vec<Label>>,
145        url: &str,
146        due: Option<DateTime<Utc>>,
147    ) -> Card {
148        Card {
149            id: String::from(id),
150            name: String::from(name),
151            desc: String::from(desc),
152            url: String::from(url),
153            labels,
154            due,
155            closed: false,
156        }
157    }
158
159    pub fn get(client: &TrelloClient, card_id: &str) -> Result<Card> {
160        let url = client
161            .config
162            .get_trello_url(&format!("/1/cards/{}", card_id), &[])?;
163
164        Ok(client.client.get(url).send()?.error_for_status()?.json()?)
165    }
166
167    pub fn create(client: &TrelloClient, list_id: &str, card: &Card) -> Result<Card> {
168        let url = client.config.get_trello_url("/1/cards/", &[])?;
169
170        let params: [(&str, &str); 3] = [
171            ("name", &card.name),
172            ("desc", &card.desc),
173            ("idList", list_id),
174        ];
175
176        Ok(client
177            .client
178            .post(url)
179            .form(&params)
180            .send()?
181            .error_for_status()?
182            .json()?)
183    }
184
185    pub fn open(client: &TrelloClient, card_id: &str) -> Result<Card> {
186        let url = client
187            .config
188            .get_trello_url(&format!("/1/cards/{}", &card_id), &[])?;
189
190        let params = [("closed", "false")];
191
192        Ok(client
193            .client
194            .put(url)
195            .form(&params)
196            .send()?
197            .error_for_status()?
198            .json()?)
199    }
200
201    pub fn update(client: &TrelloClient, card: &Card) -> Result<Card> {
202        let url = client
203            .config
204            .get_trello_url(&format!("/1/cards/{}/", &card.id), &[])?;
205
206        let params = [
207            ("name", &card.name),
208            ("desc", &card.desc),
209            ("closed", &card.closed.to_string()),
210        ];
211
212        Ok(client
213            .client
214            .put(url)
215            .form(&params)
216            .send()?
217            .error_for_status()?
218            .json()?)
219    }
220
221    // Moves a card to the list with the specified id
222    pub fn change_list(client: &TrelloClient, card_id: &str, list_id: &str) -> Result<()> {
223        let url = client
224            .config
225            .get_trello_url(&format!("/1/cards/{}/", card_id), &[])?;
226
227        let params = [("idList", list_id)];
228
229        client
230            .client
231            .put(url)
232            .form(&params)
233            .send()?
234            .error_for_status()?;
235
236        Ok(())
237    }
238
239    pub fn get_all(client: &TrelloClient, list_id: &str) -> Result<Vec<Card>> {
240        let url = client.config.get_trello_url(
241            &format!("/1/lists/{}/cards/", list_id),
242            &[("fields", &Card::get_fields().join(","))],
243        )?;
244        Ok(client.client.get(url).send()?.error_for_status()?.json()?)
245    }
246}