refactor(trello): rewrite trello module and rename commands

This commit is contained in:
Paul Campbell 2024-12-09 16:58:20 +00:00
parent f71f8c5467
commit 90912509f4
26 changed files with 455 additions and 625 deletions

View file

@ -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. As part of building the import server, I'm including the following commands the exercise each operation invovled.
- [x] trello member get - [x] trello member get - includes list of boards
- [ ] trello board get (was list/stack list) - [x] trello board get - includes list of stacks
- [ ] trello stack get (was card list) - [x] trello stack get - includes list of cards
- [ ] trello card get
- [ ] trello attachment get - [ ] trello attachment get
- [ ] nextcloud deck get (was board list) - [ ] nextcloud deck get (was board list)
- [ ] nextcloud board get (was stack list) - [ ] nextcloud board get (was stack list)

View file

@ -1,18 +1,12 @@
// //
use color_eyre::eyre::{OptionExt as _, Result}; 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<()> { pub(crate) async fn run(ctx: FullCtx) -> Result<()> {
// test trello by getting a list of the boards for the user // test trello by getting a list of the boards for the user
p!(ctx.prt, ">> Testing Trello details..."); p!(ctx.prt, ">> Testing Trello details...");
let boards = crate::trello::api::members::get_boards_that_member_belongs_to( let boards = ctx.trello_client().boards().await.result?;
&ctx.cfg.trello,
&ctx.net,
&ctx.prt,
)
.await
.result?;
p!(ctx.prt, "<<< Trello Credentials: OKAY"); p!(ctx.prt, "<<< Trello Credentials: OKAY");
let board_name = &ctx.cfg.trello.board_name; let board_name = &ctx.cfg.trello.board_name;
p!(ctx.prt, ">> Trello Board: {board_name}"); p!(ctx.prt, ">> Trello Board: {board_name}");

View file

@ -1,7 +1,6 @@
[trello] [trello]
api_key = "" api_key = ""
api_secret = "" api_secret = ""
user = ""
board_name = "" board_name = ""
[nextcloud] [nextcloud]

View file

@ -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
}
}
]
}

View file

@ -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<TrelloBoard> {
// 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<TrelloBoard> {
fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard> {
self.iter().find(|b| &b.name == board_name)
}
}

View file

@ -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<TrelloCard> {
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,
// }

View file

@ -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(())
}

View file

@ -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<TrelloCard> {
let card = net
.get(url(f!("/cards/{trello_card_id}")))
.headers(auth.into())
.send()
.await?
.json()
.await?;
Ok(card)
}

View file

@ -1,4 +0,0 @@
mod create;
mod delete;
mod get;
mod update;

View file

@ -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<TrelloCard> {
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 }
}
}

View file

@ -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<Vec<TrelloList>> {
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<Vec<TrelloCard>> {
// 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
// }

View file

@ -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<Vec<TrelloBoard>> {
APIResult::new(
net.get(url("/members/me/boards?lists=open"))
.headers(TrelloAuth::from(cfg).into())
.header("Accept", "application/json")
.send()
.await,
prt,
)
.await
}

View file

@ -1,9 +0,0 @@
//
pub(crate) mod boards;
// pub(crate) mod cards;
pub(crate) mod lists;
pub(crate) mod members;
#[cfg(test)]
mod tests;

View file

@ -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,
// }
// }

View file

@ -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

40
src/trello/board.rs Normal file
View file

@ -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(())
}
}
}
}

View file

@ -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(())
}

View file

@ -1,25 +1,92 @@
// //
use crate::api_result::APIResult; use std::collections::HashMap;
use crate::trello::model::list::TrelloList;
use crate::trello::TrelloConfig;
use crate::trello::{api::lists, model::board::TrelloBoard};
use crate::FullCtx;
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> { pub(crate) struct TrelloClient<'ctx> {
ctx: &'ctx FullCtx, ctx: &'ctx FullCtx,
} }
impl<'ctx> TrelloClient<'ctx> { impl<'ctx> TrelloClient<'ctx> {
pub(crate) async fn boards(&self, cfg: &TrelloConfig) -> APIResult<Vec<TrelloBoard>> { fn url(&self, path: impl Into<String>) -> String {
super::api::members::get_boards_that_member_belongs_to(cfg, &self.ctx.net, &self.ctx.prt) let path = path.into();
assert!(path.starts_with("/"));
f!("https://api.trello.com/1{path}")
}
fn common_headers(&self) -> HashMap<String, String> {
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<T: for<'a> serde::Deserialize<'a>>(
&self,
url: impl Into<String>,
custom: fn(&Net, String) -> ReqBuilder,
) -> APIResult<T> {
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<Vec<TrelloBoard>> {
self.request("/members/me/boards?lists=open", |net, url| net.get(url))
.await .await
} }
pub(crate) async fn lists(&self, board_id: &TrelloBoardId) -> APIResult<Vec<TrelloList>> { // https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-get
lists::get_board_lists(&self.ctx.cfg.trello, board_id, &self.ctx.net, &self.ctx.prt).await pub(crate) async fn board(&self, board_id: &TrelloBoardId) -> APIResult<TrelloBoard> {
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<Vec<TrelloShortCard>> {
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<TrelloShortCard>{
// 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<TrelloAttachment> {
// self.request(
// f!("/cards/{card_id}/attachments/{attachment_id}"),
// |net, url| net.get(url),
// )
// .await
// }
} }
impl TrelloClient<'_> { impl TrelloClient<'_> {

View file

@ -16,7 +16,7 @@ impl Execute for TrelloMemberCommand {
async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> {
match self { match self {
Self::Get { dump } => { Self::Get { dump } => {
let api_result = ctx.trello_client().boards(&ctx.cfg.trello).await; let api_result = ctx.trello_client().boards().await;
if dump { if dump {
p!(ctx.prt, "{}", api_result.text); p!(ctx.prt, "{}", api_result.text);
} else { } else {

View file

@ -1,6 +1,21 @@
// //
pub(crate) mod api; use clap::Parser;
pub(crate) mod boards;
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 client;
pub(crate) mod member; pub(crate) mod member;
pub(crate) mod model; pub(crate) mod model;
@ -9,26 +24,14 @@ pub(crate) mod stack;
#[cfg(test)] #[cfg(test)]
mod tests; 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>) -> String {
let path = path.into();
assert!(path.starts_with("/"));
f!("https://api.trello.com/1{path}")
}
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub(crate) enum TrelloCommand { pub(crate) enum TrelloCommand {
#[clap(subcommand)] #[clap(subcommand)]
Member(TrelloMemberCommand), Member(TrelloMemberCommand),
#[clap(subcommand)]
Board(TrelloBoardCommand),
#[clap(subcommand)] #[clap(subcommand)]
Stack(TrelloStackCommand), Stack(TrelloStackCommand),
} }
@ -37,6 +40,7 @@ impl Execute for TrelloCommand {
async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> { async fn execute(self, ctx: FullCtx) -> color_eyre::Result<()> {
match self { match self {
Self::Member(cmd) => cmd.execute(ctx).await, Self::Member(cmd) => cmd.execute(ctx).await,
Self::Board(cmd) => cmd.execute(ctx).await,
Self::Stack(cmd) => cmd.execute(ctx).await, Self::Stack(cmd) => cmd.execute(ctx).await,
} }
} }

View file

@ -1,5 +1,4 @@
use std::collections::HashMap; use std::collections::HashMap;
// //
use derive_more::derive::Display; use derive_more::derive::Display;

View file

@ -9,3 +9,12 @@ pub(crate) struct TrelloBoard {
pub(crate) name: TrelloBoardName, pub(crate) name: TrelloBoardName,
pub(crate) lists: Vec<TrelloList>, pub(crate) lists: Vec<TrelloList>,
} }
pub(crate) trait TrelloBoards {
fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard>;
}
impl TrelloBoards for Vec<TrelloBoard> {
fn find_by_name(&self, board_name: &TrelloBoardName) -> Option<&TrelloBoard> {
self.iter().find(|b| &b.name == board_name)
}
}

View file

@ -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)] #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub(crate) struct TrelloCard { pub(crate) struct TrelloCard {
@ -8,3 +11,27 @@ pub(crate) struct TrelloCard {
#[serde(rename = "idList")] #[serde(rename = "idList")]
pub(crate) id_list: TrelloListId, 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<TrelloCardDue>, // format date
#[serde(rename = "idAttachmentCover")]
pub(crate) id_attachment_cover: Option<TrelloAttachmentId>,
pub(crate) labels: Vec<TrelloLabelId>,
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<TrelloCardDue>, // format date
#[serde(rename = "idAttachmentCover")]
pub(crate) id_attachment_cover: Option<TrelloAttachmentId>,
pub(crate) labels: Vec<TrelloLabelId>,
pub(crate) pos: TrelloCardPosition,
}

View file

@ -5,6 +5,7 @@ pub(crate) mod list;
// mod new_card; // mod new_card;
use derive_more::derive::Display; use derive_more::derive::Display;
use serde::{Deserialize, Serialize};
use crate::newtype; use crate::newtype;
@ -21,3 +22,27 @@ newtype!(
); );
newtype!(TrelloCardId, String, Display, "Card ID"); newtype!(TrelloCardId, String, Display, "Card ID");
newtype!(TrelloCardName, String, Display, "Card Name"); 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<String>, //[],
url: String, //"https://admin.typeform.com/form/RzExEM/share#/link",
pos: i64, //1638
}

View file

@ -4,41 +4,36 @@ use color_eyre::Result;
use crate::{execute::Execute, p, FullCtx}; use crate::{execute::Execute, p, FullCtx};
use super::model::TrelloListId;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
pub(crate) enum TrelloStackCommand { pub(crate) enum TrelloStackCommand {
/// List all stacks (lists) in the board Get {
List {
#[clap(long, action = clap::ArgAction::SetTrue)] #[clap(long, action = clap::ArgAction::SetTrue)]
dump: bool, dump: bool,
list_id: String,
}, },
} }
impl Execute for TrelloStackCommand { impl Execute for TrelloStackCommand {
async fn execute(self, ctx: FullCtx) -> Result<()> { async fn execute(self, ctx: FullCtx) -> Result<()> {
match self { 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;
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 { if dump {
p!(ctx.prt, "{}", api_result.text); p!(ctx.prt, "{}", api_result.text);
} else { } else {
let lists = api_result.result?; let cards = api_result.result?;
for list in lists { for card in cards {
p!(ctx.prt, "{}", list.name); p!(ctx.prt, "{}:{}", card.id, card.name);
} }
} }
Ok(()) Ok(())
} }
}
}
}

View file

@ -2,9 +2,10 @@
use kxio::net::StatusCode; use kxio::net::StatusCode;
use crate::{ use crate::{
s,
tests::given, tests::given,
trello::{ trello::{
api::boards::TrelloBoards as _, model::board::TrelloBoards as _,
model::{ model::{
board::TrelloBoard, list::TrelloList, TrelloBoardId, TrelloBoardName, TrelloListId, board::TrelloBoard, list::TrelloList, TrelloBoardId, TrelloBoardName, TrelloListId,
TrelloListName, TrelloListName,
@ -48,23 +49,23 @@ mod commands {
} }
} }
mod stack {
use super::*;
#[tokio::test] #[tokio::test]
async fn list() { async fn get() {
//given //given
let mock_net = kxio::net::mock(); let mock_net = kxio::net::mock();
mock_net mock_net
.on() .on()
.get("https://api.trello.com/1/boards/123/lists") .get("https://api.trello.com/1/boards/65ad94865aed24f70ecdce4b")
.header("authorization", "OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"") .query("lists", "open")
.header(
"authorization",
"OAuth oauth_consumer_key=\"trello-api-key\", oauth_token=\"trello-api-secret\"",
)
.header("accept", "application/json") .header("accept", "application/json")
.header("content-type", "application/json") .header("content-type", "application/json")
.respond(StatusCode::OK) .respond(StatusCode::OK)
.body(include_str!("../tests/responses/trello-stack-list.json")) .body(include_str!("../tests/responses/trello-board-get.json"))
.expect("mock request"); .expect("mock request");
// let fs = given::a_filesystem(); // let fs = given::a_filesystem();
@ -73,14 +74,17 @@ mod commands {
//when //when
let result = ctx let result = ctx
.trello_client() .trello_client()
.lists(&TrelloBoardId::new("123")) .board(&TrelloBoardId::new("65ad94865aed24f70ecdce4b"))
.await .await
.result .result
.expect("get stacks"); .expect("board");
assert_eq!( assert_eq!(
result, result,
vec![ TrelloBoard {
id: TrelloBoardId::new(crate::s!("65ad94865aed24f70ecdce4b")),
name: TrelloBoardName::new(s!("Tasyn Kanban")),
lists: vec![
TrelloList { TrelloList {
id: TrelloListId::new("65ad94865aed24f70ecdce4c"), id: TrelloListId::new("65ad94865aed24f70ecdce4c"),
name: TrelloListName::new("Backlog") name: TrelloListName::new("Backlog")
@ -94,7 +98,7 @@ mod commands {
name: TrelloListName::new("Done 🎉") name: TrelloListName::new("Done 🎉")
} }
] ]
}
); );
} }
} }
}