diff --git a/README.md b/README.md index 6eb1b4c..2178135 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,9 @@ trello-to-deck check As part of building the import server, I'm including the following commands the exercise each operation invovled. -- [x] trello member get -- [ ] trello board get (was list/stack list) -- [ ] trello stack get (was card list) -- [ ] trello card get +- [x] trello member get - includes list of boards +- [x] trello board get - includes list of stacks +- [x] trello stack get - includes list of cards - [ ] trello attachment get - [ ] nextcloud deck get (was board list) - [ ] nextcloud board get (was stack list) diff --git a/src/check.rs b/src/check.rs index aa6619f..fd5342c 100644 --- a/src/check.rs +++ b/src/check.rs @@ -1,18 +1,12 @@ // use color_eyre::eyre::{OptionExt as _, Result}; -use crate::{f, p, trello::api::boards::TrelloBoards as _, FullCtx}; +use crate::{f, p, trello::model::board::TrelloBoards as _, FullCtx}; pub(crate) async fn run(ctx: FullCtx) -> Result<()> { // test trello by getting a list of the boards for the user p!(ctx.prt, ">> Testing Trello details..."); - let boards = crate::trello::api::members::get_boards_that_member_belongs_to( - &ctx.cfg.trello, - &ctx.net, - &ctx.prt, - ) - .await - .result?; + let boards = ctx.trello_client().boards().await.result?; p!(ctx.prt, "<<< Trello Credentials: OKAY"); let board_name = &ctx.cfg.trello.board_name; p!(ctx.prt, ">> Trello Board: {board_name}"); diff --git a/src/default-config.toml b/src/default-config.toml index 3b73a8f..78969cd 100644 --- a/src/default-config.toml +++ b/src/default-config.toml @@ -1,7 +1,6 @@ [trello] api_key = "" api_secret = "" -user = "" board_name = "" [nextcloud] diff --git a/src/tests/responses/trello-board-get.json b/src/tests/responses/trello-board-get.json new file mode 100644 index 0000000..94d938a --- /dev/null +++ b/src/tests/responses/trello-board-get.json @@ -0,0 +1,193 @@ +{ + "id": "65ad94865aed24f70ecdce4b", + "name": "Tasyn Kanban", + "desc": "Use this simple Kanban template to keep the engineering team on the same page and moving through work fluidly. \n\n1. Break down the roadmap by adding tasks as cards to the **Backlog** list. \n\n2. Move the cards one-by-one through **Design** as they becomes more fleshed out. *Pro tip:* You can enable Power-ups for your favorite design tools like [Figma](https://trello.com/power-ups/59b2e7611e6ece0b35eac16a/figma) or [Invision](https://trello.com/power-ups/596f2cb2d279152540b2bb31), in order to easily link and view designs without switching context.\n\n3. When a card is fully specced out and designs are attached, move it to **To Do** for engineers to pick up. \n\n4. Engineers move cards to **Doing** and assign themselves to the cards, so the whole team stays informed of who is working on what.\n\n5. Cards then move through **Code Review** when they're ready for a second set of eyes. The team can set a **List Limit** (with the List Limit Power-up) on the number of cards in Code Review, as a visual indicator for when the team needs to prioritize reviews rather than picking up new work. \n\n6. Once cards move through **Testing** and eventually ship to production, move them to **Done** and celebrate!\n", + "descData": null, + "closed": false, + "idOrganization": "60ae034415aa230ab2ef596d", + "idEnterprise": null, + "pinned": false, + "url": "https://trello.com/b/pKSkfnfK/tasyn-kanban", + "shortUrl": "https://trello.com/b/pKSkfnfK", + "prefs": { + "permissionLevel": "org", + "hideVotes": false, + "voting": "disabled", + "comments": "members", + "invitations": "members", + "selfJoin": false, + "cardCovers": true, + "cardCounts": false, + "isTemplate": false, + "cardAging": "regular", + "calendarFeedEnabled": false, + "hiddenPluginBoardButtons": [], + "switcherViews": [ + { + "viewType": "Board", + "enabled": true + }, + { + "viewType": "Table", + "enabled": true + }, + { + "viewType": "Calendar", + "enabled": false + }, + { + "viewType": "Dashboard", + "enabled": false + }, + { + "viewType": "Timeline", + "enabled": false + }, + { + "viewType": "Map", + "enabled": false + } + ], + "background": "5dfa855f31b76a80318febaf", + "backgroundColor": null, + "backgroundImage": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/original/d71b9370b4cba91634ae4ffe331fb59a/photo-1576502200916-3808e07386a5", + "backgroundTile": false, + "backgroundBrightness": "light", + "sharedSourceUrl": "https://images.unsplash.com/photo-1576502200916-3808e07386a5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjcwNjZ9&w=2560&h=2048&q=90", + "backgroundImageScaled": [ + { + "width": 140, + "height": 94, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/140x94/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 256, + "height": 172, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/256x172/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 480, + "height": 322, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/480x322/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 960, + "height": 644, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/960x644/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 1024, + "height": 687, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/1024x687/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 1280, + "height": 859, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/1280x859/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 1920, + "height": 1288, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/1920x1288/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 2048, + "height": 1374, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2048x1374/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 2386, + "height": 1600, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/2386x1600/47f09f0e3910259568294477d0bdedac/photo-1576502200916-3808e07386a5.jpg" + }, + { + "width": 2560, + "height": 1717, + "url": "https://trello-backgrounds.s3.amazonaws.com/SharedBackground/original/d71b9370b4cba91634ae4ffe331fb59a/photo-1576502200916-3808e07386a5" + } + ], + "backgroundBottomColor": "#068faa", + "backgroundTopColor": "#ddaba7", + "canBePublic": true, + "canBeEnterprise": true, + "canBeOrg": true, + "canBePrivate": true, + "canInvite": true + }, + "labelNames": { + "green": "", + "yellow": "", + "orange": "", + "red": "", + "purple": "", + "blue": "", + "sky": "", + "lime": "", + "pink": "", + "black": "", + "green_dark": "", + "yellow_dark": "", + "orange_dark": "", + "red_dark": "", + "purple_dark": "", + "blue_dark": "", + "sky_dark": "", + "lime_dark": "", + "pink_dark": "", + "black_dark": "", + "green_light": "", + "yellow_light": "", + "orange_light": "", + "red_light": "", + "purple_light": "", + "blue_light": "", + "sky_light": "", + "lime_light": "", + "pink_light": "", + "black_light": "" + }, + "lists": [ + { + "id": "65ad94865aed24f70ecdce4c", + "name": "Backlog", + "closed": false, + "color": null, + "idBoard": "65ad94865aed24f70ecdce4b", + "pos": 65535, + "subscribed": false, + "softLimit": null, + "type": null, + "datasource": { + "filter": false + } + }, + { + "id": "65ad94865aed24f70ecdce4e", + "name": "To Do", + "closed": false, + "color": null, + "idBoard": "65ad94865aed24f70ecdce4b", + "pos": 196607, + "subscribed": false, + "softLimit": null, + "type": null, + "datasource": { + "filter": false + } + }, + { + "id": "65ad94865aed24f70ecdce52", + "name": "Done 🎉", + "closed": false, + "color": null, + "idBoard": "65ad94865aed24f70ecdce4b", + "pos": 393215, + "subscribed": false, + "softLimit": null, + "type": null, + "datasource": { + "filter": false + } + } + ] +} diff --git a/src/trello/api/boards.rs b/src/trello/api/boards.rs deleted file mode 100644 index 185b606..0000000 --- a/src/trello/api/boards.rs +++ /dev/null @@ -1,40 +0,0 @@ -// -use crate::trello::model::board::TrelloBoard; -// use color_eyre::Result; -// use kxio::net::Net; -// -// use crate::{ -// f, -// trello::{ -// model::{TrelloAuth, TrelloBoardId}, -// url, -// }, -// }; -use crate::trello::model::TrelloBoardName; - -// pub(crate) async fn get_board( -// auth: &TrelloAuth, -// board_id: &TrelloBoardId, -// net: &Net, -// ) -> Result { -// let board = net -// .get(url(f!( -// "/boards/{}/?fields=name&lists=all&list_fields=all&cards=all&card_fields=all", -// **board_id -// ))) -// .headers(auth.into()) -// .send() -// .await? -// .json() -// .await?; -// Ok(board) -// } - -pub(crate) trait TrelloBoards { - fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard>; -} -impl TrelloBoards for Vec { - fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard> { - self.iter().find(|b| &b.name == board_name) - } -} diff --git a/src/trello/api/cards/create.rs b/src/trello/api/cards/create.rs deleted file mode 100644 index 98917fb..0000000 --- a/src/trello/api/cards/create.rs +++ /dev/null @@ -1,61 +0,0 @@ -// - -use color_eyre::Result; -use kxio::net::Net; -use serde_json::json; - -use crate::trello::{ - types::{NewTrelloCard, TrelloAuth, TrelloCard}, - url, -}; - -/// -/// POST /cards -/// -/// Query Pparameters -/// -/// - name string -/// The name for the card -/// - desc string -/// The description of the card -/// - pos string -/// The position of the card. top, bottom, or a positive number. -/// - due string -/// A due date for the card. A date -/// - start string -/// A start date for the card. A date, or null -/// - dueComplete bool -/// If the due date has been marked complete -/// - idList string REQUIRED -/// The ID of the list the card should be added to -/// - idMembers string -/// A comma-separated list of memberIds to add to the card -/// - idLabels string -/// A comma-separated list of labelIds to add to the card -pub async fn create_card( - auth: &TrelloAuth, - new_card: NewTrelloCard, - net: &Net, -) -> Result { - let card = net - .post(url("/cards")) - .headers(auth.into()) - .body( - json!({ - "idList": new_card.list_id(), - "name": new_card.name(), - "pos": "bottom", - }) - .to_string(), - ) - .send() - .await? - .json() - .await?; - Ok(card) -} -// #[derive(Debug, serde::Serialize)] -// pub struct CreateCardRequest { -// name: String, -// pos: String, -// } diff --git a/src/trello/api/cards/delete.rs b/src/trello/api/cards/delete.rs deleted file mode 100644 index 8d87194..0000000 --- a/src/trello/api/cards/delete.rs +++ /dev/null @@ -1,21 +0,0 @@ -// - -use color_eyre::Result; -use kxio::net::Net; - -use crate::{ - f, - trello::{types::TrelloAuth, url, TrelloCardId}, -}; - -pub async fn delete_card( - auth: &TrelloAuth, - remote_task_id: &TrelloCardId, - net: &Net, -) -> Result<()> { - net.delete(url(f!("/cards/{remote_task_id}"))) - .headers(auth.into()) - .send() - .await?; - Ok(()) -} diff --git a/src/trello/api/cards/get.rs b/src/trello/api/cards/get.rs deleted file mode 100644 index 510174a..0000000 --- a/src/trello/api/cards/get.rs +++ /dev/null @@ -1,96 +0,0 @@ -// - -use color_eyre::Result; -use kxio::net::Net; - -use crate::{ - f, - trello::{ - types::{TrelloAuth, TrelloCard}, - url, TrelloCardId, - }, -}; - -/// Get a Card -/// -/// https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-get -/// -/// GET /cards/{id} -/// -/// Get a card by its ID -/// -/// Request -/// -/// Path parameters -/// -/// id TrelloID REQUIRED -/// -/// Query parameters -/// -/// - fields string -/// Which fields to return. -/// Default: `all` -/// `all` or a comma-separated list of fields. -/// Defaults: `badges, checkItemStates, closed, dateLastActivity, desc, descData, -/// due, start, email, idBoard, idChecklists, idLabels, idList, idMembers, idShort, idAttachmentCover, -/// manualCoverAttachment, labels, name, pos, shortUrl, url` -/// -/// - actions string -/// See the [Actions Nested Resource](https://developer.atlassian.com/cloud/trello/guides/rest-api/nested-resources/#actions-nested-resource) -/// -/// - attachments oneOf [string, boolean] -/// true, false, or cover -/// Default: false -/// -/// - attachment_fields string -/// `all` or a comma-separated list of attachment fields -/// Default: `all` -/// -/// - members boolean -/// Whether to return member objects for members on the card -/// Default: false -/// -/// - member_fields string -/// `all` or a comma-separated list of member fields. Defaults: `avatarHash, fullName, initials, username` -/// -/// - checkItemStates boolean -/// Whether to return checkItemState objects for checklists on the card -/// Default: false -/// -/// - checklists string -/// Whether to return the checklists on the card. all or none -/// Default: none -/// -/// - checklist_fields string -/// `all` or a comma-separated list of idBoard,idCard,name,pos -/// Default: `all` -/// -/// - board boolean -/// Whether to return the board object the card is on -/// Default: false -/// -/// - board_fields string -/// `all` or a comma-separated list of board fields. Defaults: `name, desc, descData, closed, idOrganization, pinned, url, prefs` -/// -/// - list boolean -/// See the [Lists Nested Resource](https://developer.atlassian.com/cloud/trello/guides/rest-api/nested-resources/#lists-nested-resource) -/// -/// Responses -/// -/// 200 OK Success -/// -/// application/json -pub async fn get_card( - auth: &TrelloAuth, - trello_card_id: &TrelloCardId, - net: &Net, -) -> Result { - let card = net - .get(url(f!("/cards/{trello_card_id}"))) - .headers(auth.into()) - .send() - .await? - .json() - .await?; - Ok(card) -} diff --git a/src/trello/api/cards/mod.rs b/src/trello/api/cards/mod.rs deleted file mode 100644 index 9a75c31..0000000 --- a/src/trello/api/cards/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod create; -mod delete; -mod get; -mod update; \ No newline at end of file diff --git a/src/trello/api/cards/update.rs b/src/trello/api/cards/update.rs deleted file mode 100644 index fca63fb..0000000 --- a/src/trello/api/cards/update.rs +++ /dev/null @@ -1,49 +0,0 @@ -// - -use color_eyre::Result; -use kxio::net::Net; -use serde_json::json; - -use crate::{ - f, - trello::{ - types::{TrelloAuth, TrelloCard}, - url, TrelloCardId, TrelloCardName, TrelloListId, - }, -}; - -pub async fn update_card( - auth: &TrelloAuth, - card_update: TrelloCardUpdate, - net: &Net, -) -> Result { - let net_url = url(f!("/cards/{}", card_update.id)); - let card = net - .put(net_url) - .headers(auth.into()) - .body( - json!({ - "idList": card_update.id_list, - "name": card_update.name, - }) - .to_string(), - ) - .send() - .await? - .json() - .await?; - Ok(card) -} - -#[derive(Debug, PartialEq, Eq, serde::Serialize)] -pub struct TrelloCardUpdate { - id: TrelloCardId, - name: TrelloCardName, - #[serde(rename = "idList")] - id_list: TrelloListId, -} -impl TrelloCardUpdate { - pub const fn new(id: TrelloCardId, name: TrelloCardName, id_list: TrelloListId) -> Self { - Self { id, name, id_list } - } -} diff --git a/src/trello/api/lists.rs b/src/trello/api/lists.rs deleted file mode 100644 index a56a2ef..0000000 --- a/src/trello/api/lists.rs +++ /dev/null @@ -1,72 +0,0 @@ -// - -use kxio::{net::Net, print::Printer}; - -use crate::{ - api_result::APIResult, - f, - trello::{ - model::{auth::TrelloAuth, list::TrelloList, TrelloBoardId}, - url, TrelloConfig, - }, -}; - -/// Get Lists in a Board -/// -/// GET /boards/{id}/lists -/// -/// List all lists in a board -pub(crate) async fn get_board_lists( - cfg: &TrelloConfig, - board_id: &TrelloBoardId, - net: &Net, - prt: &Printer, -) -> APIResult> { - APIResult::new( - net.get(url(f!("/boards/{}/lists", board_id))) - .headers(TrelloAuth::from(cfg).into()) - .header("accept", "application/json") - .header("content-type", "application/json") - .send() - .await, - prt, - ) - .await -} - -// /// Get Cards in a List -// /// -// /// https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-cards-get -// /// -// /// GET /lists/{id}/cards -// /// -// /// List the cards in a list -// /// -// /// Request -// /// -// /// Path parameters -// /// -// /// id TrelloID REQUIRED -// /// -// /// Responses -// /// -// /// 200 OK Success -// /// -// /// application/json -// pub(crate) async fn get_lists_cards<'cfg>( -// cfg: &TrelloConfig, -// list: &TrelloList, -// net: &Net, -// prt: &Printer, -// ) -> APIResult> { -// APIResult::new( -// net.get(url(f!("/lists/{}/cards", list.id))) -// .headers(TrelloAuth::from(cfg).into()) -// .header("accept", "application/json") -// .header("content-type", "application/json") -// .send() -// .await, -// prt, -// ) -// .await -// } diff --git a/src/trello/api/members.rs b/src/trello/api/members.rs deleted file mode 100644 index 03cdf98..0000000 --- a/src/trello/api/members.rs +++ /dev/null @@ -1,55 +0,0 @@ -// -use kxio::{net::Net, print::Printer}; - -use crate::{ - api_result::APIResult, - trello::{ - model::{auth::TrelloAuth, board::TrelloBoard}, - url, TrelloConfig, - }, -}; - -/// Get lists from named board that Member belongs to -/// -/// Get Boards that Member belongs to -/// https://developer.atlassian.com/cloud/trello/rest/api-group-members/#api-members-id-boards-get -/// /members/{id}/boards -/// -/// Lists the boards that the user is a member of. -/// -/// Request -/// -/// Path parameters -/// -/// - id TrelloID REQUIRED -/// -/// -/// Query parameters -/// -/// - fields string -/// Default: all -/// Valid values: id, name, desc, descData, closed, idMemberCreator, idOrganization, pinned, url, shortUrl, prefs, labelNames, starred, limits, memberships, enterpriseOwned -/// -/// - lists string -/// Which lists to include with the boards. One of: all, closed, none, open -/// Default: none -/// Valid values: all, closed, none, open -/// -/// curl --request GET \ -/// --url "https://api.trello.com/1/members/$TRELLO_USERNAME/boards?key=$TRELLO_KEY&token=$TRELLO_SECRET&lists=open" \ -/// --header 'Accept: application/json' -pub(crate) async fn get_boards_that_member_belongs_to( - cfg: &TrelloConfig, - net: &Net, - prt: &Printer, -) -> APIResult> { - APIResult::new( - net.get(url("/members/me/boards?lists=open")) - .headers(TrelloAuth::from(cfg).into()) - .header("Accept", "application/json") - .send() - .await, - prt, - ) - .await -} diff --git a/src/trello/api/mod.rs b/src/trello/api/mod.rs deleted file mode 100644 index 4726c47..0000000 --- a/src/trello/api/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// - -pub(crate) mod boards; -// pub(crate) mod cards; -pub(crate) mod lists; -pub(crate) mod members; - -#[cfg(test)] -mod tests; diff --git a/src/trello/api/tests/given.rs b/src/trello/api/tests/given.rs deleted file mode 100644 index f24709a..0000000 --- a/src/trello/api/tests/given.rs +++ /dev/null @@ -1,17 +0,0 @@ -// -use kxio::{net::MockNet, print::Printer}; - -pub(crate) fn a_network() -> MockNet { - kxio::net::mock() -} - -pub(crate) fn a_printer() -> Printer { - kxio::print::test() -} - -// pub(crate) fn an_auth<'cfg>(cfg: &'cfg TrelloConfig) -> TrelloAuth<'cfg> { -// TrelloAuth { -// api_key: &cfg.api_key, -// api_secret: &cfg.api_secret, -// } -// } diff --git a/src/trello/api/tests/mod.rs b/src/trello/api/tests/mod.rs deleted file mode 100644 index 5c2d72b..0000000 --- a/src/trello/api/tests/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -// -use std::collections::HashMap; - -use kxio::net::StatusCode; -use serde_json::json; - -use crate::{ - s, - trello::{ - api::members::get_boards_that_member_belongs_to, model::board::TrelloBoard, TrelloConfig, - }, -}; - -mod given; - -type TestResult = color_eyre::Result<()>; - -mod members { - use super::*; - - #[tokio::test] - async fn get_member_boards() -> TestResult { - //given - let net = given::a_network(); - let prt = given::a_printer(); - let trello_config = TrelloConfig { - api_key: s!("foo").into(), - api_secret: s!("bar").into(), - board_name: s!("board-name").into(), - }; - - net.on() - .get("https://api.trello.com/1/members/me/boards?lists=open") - .headers(HashMap::from([ - ( - s!("authorization"), - s!("OAuth oauth_consumer_key=\"foo\", oauth_token=\"bar\""), - ), - (s!("accept"), s!("application/json")), - ])) - .respond(StatusCode::OK) - .body(s!(json!([ - {"id": "1", "name": "board-name", "lists":[]} - ])))?; - - //when - let result = get_boards_that_member_belongs_to(&trello_config, &net.into(), &prt) - .await - .result?; - - assert_eq!( - result, - vec![TrelloBoard { - id: s!("1").into(), - name: s!("board-name").into(), - lists: vec![] - }] - ); - - //then - Ok(()) - } -} -// TODO: boards -// TODO: lists -// TODO: cards diff --git a/src/trello/board.rs b/src/trello/board.rs new file mode 100644 index 0000000..be57dfc --- /dev/null +++ b/src/trello/board.rs @@ -0,0 +1,40 @@ +// +use clap::Parser; + +use crate::execute::Execute; +use crate::{p, FullCtx}; + +use super::model::TrelloBoardId; + +#[derive(Parser, Debug)] +pub(crate) enum TrelloBoardCommand { + Get { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + + board_id: String, + }, +} + +impl Execute for TrelloBoardCommand { + async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { + match self { + Self::Get { dump, board_id } => { + let api_result = ctx + .trello_client() + .board(&TrelloBoardId::new(board_id)) + .await; + if dump { + p!(ctx.prt, "{}", api_result.text); + } else { + let mut lists = api_result.result?.lists; + lists.sort_by(|a, b| a.name.cmp(&b.name)); + lists.into_iter().for_each(|list| { + p!(ctx.prt, "{}:{}", list.id, list.name); + }); + } + Ok(()) + } + } + } +} diff --git a/src/trello/boards.rs b/src/trello/boards.rs deleted file mode 100644 index 38c9633..0000000 --- a/src/trello/boards.rs +++ /dev/null @@ -1,35 +0,0 @@ -// -use clap::Parser; - -use crate::execute::Execute; -use crate::{p, FullCtx}; - -#[derive(Parser, Debug)] -pub(crate) enum TrelloBoardCommand { - List { - #[clap(long, action = clap::ArgAction::SetTrue)] - dump: bool, - }, -} - -impl Execute for TrelloBoardCommand { - async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { - match self { - Self::List { dump } => list(ctx, dump).await, - } - } -} - -pub(crate) async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> { - let api_result = ctx.trello_client().boards(&ctx.cfg.trello).await; - if dump { - p!(ctx.prt, "{}", api_result.text); - } else { - let mut boards = api_result.result?; - boards.sort_by(|a, b| a.name.cmp(&b.name)); - boards.into_iter().for_each(|board| { - p!(ctx.prt, "{}:{}", board.id, board.name); - }); - } - Ok(()) -} diff --git a/src/trello/client.rs b/src/trello/client.rs index 7e36a7b..433fc55 100644 --- a/src/trello/client.rs +++ b/src/trello/client.rs @@ -1,25 +1,92 @@ // -use crate::api_result::APIResult; -use crate::trello::model::list::TrelloList; -use crate::trello::TrelloConfig; -use crate::trello::{api::lists, model::board::TrelloBoard}; -use crate::FullCtx; +use std::collections::HashMap; -use super::model::TrelloBoardId; +use kxio::net::{Net, ReqBuilder}; + +use super::model::{TrelloBoardId, TrelloListId}; +use crate::trello::model::card::TrelloShortCard; +use crate::{api_result::APIResult, f, s, trello::model::board::TrelloBoard, FullCtx}; pub(crate) struct TrelloClient<'ctx> { ctx: &'ctx FullCtx, } impl<'ctx> TrelloClient<'ctx> { - pub(crate) async fn boards(&self, cfg: &TrelloConfig) -> APIResult> { - super::api::members::get_boards_that_member_belongs_to(cfg, &self.ctx.net, &self.ctx.prt) + fn url(&self, path: impl Into) -> String { + let path = path.into(); + assert!(path.starts_with("/")); + f!("https://api.trello.com/1{path}") + } + + fn common_headers(&self) -> HashMap { + let api_key = &self.ctx.cfg.trello.api_key; + let api_secret = &self.ctx.cfg.trello.api_secret; + HashMap::from([ + (s!("accept"), s!("application/json")), + (s!("content-type"), s!("application/json")), + ( + s!("Authorization"), + f!(r#"OAuth oauth_consumer_key="{api_key}", oauth_token="{api_secret}""#,), + ), + ]) + } + + async fn request serde::Deserialize<'a>>( + &self, + url: impl Into, + custom: fn(&Net, String) -> ReqBuilder, + ) -> APIResult { + APIResult::new( + custom(&self.ctx.net, self.url(url)) + .headers(self.common_headers()) + .send() + .await, + &self.ctx.prt, + ) + .await + } +} + +impl<'ctx> TrelloClient<'ctx> { + // https://developer.atlassian.com/cloud/trello/rest/api-group-members/#api-members-id-boards-get + pub(crate) async fn boards(&self) -> APIResult> { + self.request("/members/me/boards?lists=open", |net, url| net.get(url)) .await } - pub(crate) async fn lists(&self, board_id: &TrelloBoardId) -> APIResult> { - lists::get_board_lists(&self.ctx.cfg.trello, board_id, &self.ctx.net, &self.ctx.prt).await + // https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-get + pub(crate) async fn board(&self, board_id: &TrelloBoardId) -> APIResult { + self.request(f!("/boards/{board_id}?lists=open"), |net, url| net.get(url)) + .await } + + // https://developer.atlassian.com/cloud/trello/rest/api-group-lists/#api-lists-id-cards-get + pub(crate) async fn list_cards( + &self, + list_id: &TrelloListId, + ) -> APIResult> { + self.request(f!("/lists/{list_id}/cards"), |net, url| net.get(url)) + .await + } + + // // https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-get + // pub(crate) async fn card(&self, card_id: &TrelloCardId) -> APIResult{ + // self.request(f!("/cards/{card_id}?attachments=true"), |net, url| net.get(url)) + // .await + // } + + // // https://developer.atlassian.com/cloud/trello/rest/api-group-cards/#api-cards-id-attachments-idattachment-get + // pub(crate) async fn card_attachment( + // &self, + // card_id: &TrelloCardId, + // attachment_id: &TrelloAttachmentId, + // ) -> APIResult { + // self.request( + // f!("/cards/{card_id}/attachments/{attachment_id}"), + // |net, url| net.get(url), + // ) + // .await + // } } impl TrelloClient<'_> { diff --git a/src/trello/member.rs b/src/trello/member.rs index e5f1b16..591ac3d 100644 --- a/src/trello/member.rs +++ b/src/trello/member.rs @@ -16,7 +16,7 @@ impl Execute for TrelloMemberCommand { async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { match self { Self::Get { dump } => { - let api_result = ctx.trello_client().boards(&ctx.cfg.trello).await; + let api_result = ctx.trello_client().boards().await; if dump { p!(ctx.prt, "{}", api_result.text); } else { diff --git a/src/trello/mod.rs b/src/trello/mod.rs index 27986eb..eb5fdbe 100644 --- a/src/trello/mod.rs +++ b/src/trello/mod.rs @@ -1,6 +1,21 @@ // -pub(crate) mod api; -pub(crate) mod boards; +use clap::Parser; + +use crate::{ + execute::Execute, + trello::{ + board::TrelloBoardCommand, + member::TrelloMemberCommand, + model::{ + auth::{TrelloApiKey, TrelloApiSecret}, + TrelloBoardName, + }, + stack::TrelloStackCommand, + }, + FullCtx, +}; + +pub(crate) mod board; pub(crate) mod client; pub(crate) mod member; pub(crate) mod model; @@ -9,26 +24,14 @@ pub(crate) mod stack; #[cfg(test)] mod tests; -use crate::execute::Execute; -use crate::trello::member::TrelloMemberCommand; -use crate::trello::model::auth::{TrelloApiKey, TrelloApiSecret}; -use crate::trello::model::TrelloBoardName; -use crate::trello::stack::TrelloStackCommand; -use crate::{f, FullCtx}; - -use clap::Parser; - -pub(crate) fn url(path: impl Into) -> String { - let path = path.into(); - assert!(path.starts_with("/")); - f!("https://api.trello.com/1{path}") -} - #[derive(Parser, Debug)] pub(crate) enum TrelloCommand { #[clap(subcommand)] Member(TrelloMemberCommand), + #[clap(subcommand)] + Board(TrelloBoardCommand), + #[clap(subcommand)] Stack(TrelloStackCommand), } @@ -37,6 +40,7 @@ impl Execute for TrelloCommand { async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { match self { Self::Member(cmd) => cmd.execute(ctx).await, + Self::Board(cmd) => cmd.execute(ctx).await, Self::Stack(cmd) => cmd.execute(ctx).await, } } diff --git a/src/trello/model/auth.rs b/src/trello/model/auth.rs index 3617062..1761616 100644 --- a/src/trello/model/auth.rs +++ b/src/trello/model/auth.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; - // use derive_more::derive::Display; diff --git a/src/trello/model/board.rs b/src/trello/model/board.rs index d81edfd..cd44b0f 100644 --- a/src/trello/model/board.rs +++ b/src/trello/model/board.rs @@ -9,3 +9,12 @@ pub(crate) struct TrelloBoard { pub(crate) name: TrelloBoardName, pub(crate) lists: Vec, } + +pub(crate) trait TrelloBoards { + fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard>; +} +impl TrelloBoards for Vec { + fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard> { + self.iter().find(|b| &b.name == board_name) + } +} diff --git a/src/trello/model/card.rs b/src/trello/model/card.rs index b8ac1ea..26887d9 100644 --- a/src/trello/model/card.rs +++ b/src/trello/model/card.rs @@ -1,5 +1,8 @@ // -use super::{TrelloCardId, TrelloCardName, TrelloListId}; +use super::{ + TrelloAttachmentId, TrelloCardDescription, TrelloCardDue, TrelloCardId, TrelloCardName, + TrelloCardPosition, TrelloLabelId, TrelloListId, +}; #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] pub(crate) struct TrelloCard { @@ -8,3 +11,27 @@ pub(crate) struct TrelloCard { #[serde(rename = "idList")] pub(crate) id_list: TrelloListId, } + +#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub(crate) struct TrelloShortCard { + pub(crate) id: TrelloCardId, + pub(crate) name: TrelloCardName, + pub(crate) desc: TrelloCardDescription, + pub(crate) due: Option, // format date + #[serde(rename = "idAttachmentCover")] + pub(crate) id_attachment_cover: Option, + pub(crate) labels: Vec, + pub(crate) pos: TrelloCardPosition, +} + +#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub(crate) struct TrelloLongCard { + pub(crate) id: TrelloCardId, + pub(crate) name: TrelloCardName, + pub(crate) desc: TrelloCardDescription, + pub(crate) due: Option, // format date + #[serde(rename = "idAttachmentCover")] + pub(crate) id_attachment_cover: Option, + pub(crate) labels: Vec, + pub(crate) pos: TrelloCardPosition, +} diff --git a/src/trello/model/mod.rs b/src/trello/model/mod.rs index e89fa27..c87b82d 100644 --- a/src/trello/model/mod.rs +++ b/src/trello/model/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod list; // mod new_card; use derive_more::derive::Display; +use serde::{Deserialize, Serialize}; use crate::newtype; @@ -21,3 +22,27 @@ newtype!( ); newtype!(TrelloCardId, String, Display, "Card ID"); newtype!(TrelloCardName, String, Display, "Card Name"); +newtype!(TrelloCardDescription, String, Display, "Card Description"); +newtype!(TrelloCardDue, String, Display, "Card Due"); +newtype!(TrelloCardPosition, i64, Display, "Card Position"); +newtype!(TrelloAttachmentId, String, Display, "Card Attachment ID"); +newtype!(TrelloLabelId, String, Display, "Label ID"); + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct TrelloAttachment { + pub(crate) id: String, // "5abbe4b7ddc1b351ef961414", + pub(crate) bytes: String, + date: String, //"2018-10-17T19:10:14.808Z", + #[serde(rename = "edgeColor")] + edge_color: String, //"yellow", + #[serde(rename = "idMember")] + id_member: String, //"5abbe4b7ddc1b351ef961414", + #[serde(rename = "isUpload")] + is_upload: bool, //false, + #[serde(rename = "mimeType")] + mime_type: String, //"", + pub(crate) name: String, //"Deprecation Extension Notice", + previews: Vec, //[], + url: String, //"https://admin.typeform.com/form/RzExEM/share#/link", + pos: i64, //1638 +} diff --git a/src/trello/stack.rs b/src/trello/stack.rs index ad5c480..7191f7b 100644 --- a/src/trello/stack.rs +++ b/src/trello/stack.rs @@ -4,41 +4,36 @@ use color_eyre::Result; use crate::{execute::Execute, p, FullCtx}; +use super::model::TrelloListId; + #[derive(Parser, Debug)] pub(crate) enum TrelloStackCommand { - /// List all stacks (lists) in the board - List { + Get { #[clap(long, action = clap::ArgAction::SetTrue)] dump: bool, + + list_id: String, }, } impl Execute for TrelloStackCommand { async fn execute(self, ctx: FullCtx) -> Result<()> { match self { - Self::List { dump } => list(ctx, dump).await, + Self::Get { dump, list_id } => { + let api_result = ctx + .trello_client() + .list_cards(&TrelloListId::new(list_id)) + .await; + if dump { + p!(ctx.prt, "{}", api_result.text); + } else { + let cards = api_result.result?; + for card in cards { + p!(ctx.prt, "{}:{}", card.id, card.name); + } + } + Ok(()) + } } } } - -async fn list(ctx: FullCtx, dump: bool) -> std::result::Result<(), color_eyre::eyre::Error> { - let client = ctx.trello_client(); - let cfg = &ctx.cfg.trello; - let board = client - .boards(cfg) - .await - .result? - .into_iter() - .find(|b| b.name == cfg.board_name) - .ok_or_else(|| color_eyre::eyre::eyre!("Board not found"))?; - let api_result = client.lists(&board.id).await; - if dump { - p!(ctx.prt, "{}", api_result.text); - } else { - let lists = api_result.result?; - for list in lists { - p!(ctx.prt, "{}", list.name); - } - } - Ok(()) -} diff --git a/src/trello/tests.rs b/src/trello/tests.rs index 60399c1..b15229a 100644 --- a/src/trello/tests.rs +++ b/src/trello/tests.rs @@ -2,9 +2,10 @@ use kxio::net::StatusCode; use crate::{ + s, tests::given, trello::{ - api::boards::TrelloBoards as _, + model::board::TrelloBoards as _, model::{ board::TrelloBoard, list::TrelloList, TrelloBoardId, TrelloBoardName, TrelloListId, TrelloListName, @@ -48,39 +49,42 @@ mod commands { } } - mod stack { + #[tokio::test] + async fn get() { + //given + let mock_net = kxio::net::mock(); - use super::*; + mock_net + .on() + .get("https://api.trello.com/1/boards/65ad94865aed24f70ecdce4b") + .query("lists", "open") + .header( + "authorization", + "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"", + ) + .header("accept", "application/json") + .header("content-type", "application/json") + .respond(StatusCode::OK) + .body(include_str!("../tests/responses/trello-board-get.json")) + .expect("mock request"); - #[tokio::test] - async fn list() { - //given - let mock_net = kxio::net::mock(); + // let fs = given::a_filesystem(); + let ctx = given::a_full_context(mock_net); - mock_net - .on() - .get("https://api.trello.com/1/boards/123/lists") - .header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"") - .header("accept", "application/json") - .header("content-type", "application/json") - .respond(StatusCode::OK) - .body(include_str!("../tests/responses/trello-stack-list.json")) - .expect("mock request"); + //when + let result = ctx + .trello_client() + .board(&TrelloBoardId::new("65ad94865aed24f70ecdce4b")) + .await + .result + .expect("board"); - // let fs = given::a_filesystem(); - let ctx = given::a_full_context(mock_net); - - //when - let result = ctx - .trello_client() - .lists(&TrelloBoardId::new("123")) - .await - .result - .expect("get stacks"); - - assert_eq!( - result, - vec![ + assert_eq!( + result, + TrelloBoard { + id: TrelloBoardId::new(crate::s!("65ad94865aed24f70ecdce4b")), + name: TrelloBoardName::new(s!("Tasyn Kanban")), + lists: vec![ TrelloList { id: TrelloListId::new("65ad94865aed24f70ecdce4c"), name: TrelloListName::new("Backlog") @@ -94,7 +98,7 @@ mod commands { name: TrelloListName::new("Done 🎉") } ] - ); - } + } + ); } }