feat(nextcloud): add basics of nextcloud config model client
Some checks failed
Test / build (map[name:nightly]) (push) Successful in 2m51s
Test / build (map[name:stable]) (push) Successful in 6m36s
Release Please / Release-plz (push) Failing after 16s

This commit is contained in:
Paul Campbell 2024-11-30 11:30:36 +00:00
parent c8f9780226
commit 327e33a7e3
11 changed files with 884 additions and 14 deletions

View file

@ -4,7 +4,6 @@ use kxio::net::Response;
use crate::{e, s}; use crate::{e, s};
pub struct APIResult<T> { pub struct APIResult<T> {
pub text: String,
pub result: Result<T, kxio::net::Error>, pub result: Result<T, kxio::net::Error>,
} }
@ -21,12 +20,9 @@ impl<T: for<'a> serde::Deserialize<'a>> APIResult<T> {
e e
}) })
.map_err(kxio::net::Error::from); .map_err(kxio::net::Error::from);
Self { text, result } Self { result }
} }
Err(e) => Self { Err(e) => Self { result: Err(e) },
text: s!(""),
result: Err(e),
},
} }
} }
} }

View file

@ -1,13 +1,35 @@
// //
use color_eyre::Result; use color_eyre::Result;
use crate::{f, s, Ctx, NAME}; use crate::{
f,
nextcloud::model::{NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername},
s, Ctx, NAME,
};
#[derive(Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)] #[derive(Clone, Debug, derive_more::From, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)]
pub struct TrelloConfig { pub(crate) struct TrelloConfig {
pub api_key: String, pub(crate) api_key: String,
pub api_secret: String, pub(crate) api_secret: String,
pub board_name: String, pub(crate) board_name: String,
}
#[derive(
Clone,
Debug,
derive_more::From,
PartialEq,
Eq,
PartialOrd,
Ord,
serde::Deserialize,
derive_more::Constructor,
)]
pub(crate) struct NextcloudConfig {
pub(crate) hostname: NextcloudHostname,
pub(crate) username: NextcloudUsername,
pub(crate) password: NextcloudPassword,
pub(crate) board_id: NextcloudBoardId,
} }
#[derive( #[derive(
@ -22,7 +44,8 @@ pub struct TrelloConfig {
serde::Deserialize, serde::Deserialize,
)] )]
pub struct AppConfig { pub struct AppConfig {
pub trello: TrelloConfig, pub(crate) trello: TrelloConfig,
pub(crate) nextcloud: NextcloudConfig,
} }
impl AppConfig { impl AppConfig {
pub fn load(ctx: &Ctx) -> Result<Self> { pub fn load(ctx: &Ctx) -> Result<Self> {

View file

@ -2,12 +2,14 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::Parser; use clap::Parser;
pub use config::AppConfig;
use kxio::{fs::FileSystem, net::Net}; use kxio::{fs::FileSystem, net::Net};
// mod api_result; mod api_result;
mod config; mod config;
mod init; mod init;
mod macros; mod macros;
pub mod nextcloud;
mod template; mod template;
// mod trello; // mod trello;

22
src/nextcloud/board.rs Normal file
View file

@ -0,0 +1,22 @@
//
use kxio::net::Net;
use crate::{p, AppConfig, Ctx};
use crate::{p, FullCtx};
use super::DeckClient;
pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> {
let dc = DeckClient::new(&ctx.cfg.nextcloud, ctx.net);
let apiresult = dc.get_boards().await;
if dump {
p!("{}", apiresult.text);
} else {
let mut boards = apiresult.result?;
boards.sort_by_key(|stack| stack.title.clone());
boards
.iter()
.for_each(|stack| p!("{}:{}", stack.id, stack.title));
}
Ok(())
}

113
src/nextcloud/mod.rs Normal file
View file

@ -0,0 +1,113 @@
//
use kxio::net::Net;
use crate::api_result::APIResult;
use crate::{config::NextcloudConfig, f};
use model::{Board, Card, NextcloudBoardId, Stack};
pub mod model;
#[cfg(test)]
mod tests;
pub(crate) struct DeckClient {
net: Net,
hostname: String,
username: String,
password: String,
}
impl DeckClient {
pub fn new(cfg: &NextcloudConfig, net: Net) -> Self {
Self {
net,
hostname: cfg.hostname.to_string(),
username: cfg.username.to_string(),
password: cfg.password.to_string(),
}
}
fn url(&self, path: impl Into<String>) -> String {
f!(
"https://{}/index.php/apps/deck/api/v1.0/{}",
self.hostname,
path.into()
)
}
pub async fn get_boards(&self) -> APIResult<Vec<Board>> {
APIResult::new(
self.net
.get(self.url("boards"))
.basic_auth(&self.username, Some(&self.password))
.header("accept", "application/json")
.send()
.await,
)
.await
}
pub async fn create_board(&self, title: &str, color: &str) -> APIResult<Board> {
APIResult::new(
self.net
.post(self.url("boards"))
.basic_auth(&self.username, Some(&self.password))
.header("accept", "application/json")
.body(
serde_json::json!({
"title": title,
"color": color
})
.to_string(),
)
.send()
.await,
)
.await
}
pub async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult<Vec<Stack>> {
APIResult::new(
self.net
.get(self.url(f!("boards/{board_id}/stacks")))
.basic_auth(&self.username, Some(&self.password))
.header("accept", "application/json")
.send()
.await,
)
.await
}
pub async fn create_card(
&self,
board_id: i64,
stack_id: i64,
title: &str,
description: Option<&str>,
) -> APIResult<Card> {
let url = format!(
"https://{}/index.php/apps/deck/api/v1.0/boards/{}/stacks/{}/cards",
self.hostname, board_id, stack_id
);
let mut json = serde_json::json!({
"title": title,
});
if let Some(desc) = description {
json["description"] = serde_json::Value::String(desc.to_string());
}
APIResult::new(
self.net
.post(&url)
.basic_auth(&self.username, Some(&self.password))
.header("accept", "application/json")
.body(json.to_string())
.send()
.await,
)
.await
}
}

168
src/nextcloud/model.rs Normal file
View file

@ -0,0 +1,168 @@
use derive_more::derive::Display;
//
use serde::{Deserialize, Serialize};
use crate::newtype;
newtype!(
NextcloudHostname,
String,
Display,
PartialOrd,
Ord,
"Hostname of the Nextcloud server"
);
newtype!(
NextcloudUsername,
String,
Display,
PartialOrd,
Ord,
"Username to authenticate as"
);
newtype!(
NextcloudPassword,
String,
PartialOrd,
Ord,
"Password to authenticate with"
);
newtype!(
NextcloudBoardId,
i64,
Copy,
Display,
PartialOrd,
Ord,
"ID of a Nextcloud Board"
);
newtype!(
NextcloudStackId,
i64,
Copy,
Display,
PartialOrd,
Ord,
"ID of a Nextcloud Stack"
);
newtype!(
NextcloudCardId,
i64,
Copy,
Display,
PartialOrd,
Ord,
"ID of a Nextcloud Card"
);
newtype!(
NextcloudLabelId,
i64,
Copy,
Display,
PartialOrd,
Ord,
"ID of a Nextcloud Label"
);
newtype!(
NextcloudOrder,
i64,
Copy,
Display,
PartialOrd,
Ord,
"Relative position of the item amongst its peers"
);
newtype!(
NextcloudETag,
String,
PartialOrd,
Ord,
"ETag for a resource"
);
newtype!(
NextcloudBoardTitle,
String,
PartialOrd,
Ord,
"Title of the Board"
);
newtype!(
NextcloudBoardOwner,
String,
PartialOrd,
Ord,
"Owner of the Board"
);
newtype!(
NextcloudBoardColour,
String,
PartialOrd,
Ord,
"Colour of the Board"
);
newtype!(
NextcloudStackTitle,
String,
PartialOrd,
Ord,
"Title of the Stack"
);
newtype!(
NextcloudCardTitle,
String,
PartialOrd,
Ord,
"Title of the Card"
);
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Board {
pub id: NextcloudBoardId,
pub title: NextcloudBoardTitle,
pub owner: NextcloudBoardOwner,
pub color: NextcloudBoardColour,
pub archived: bool,
pub labels: Vec<Label>,
pub acl: Vec<Acl>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Stack {
pub id: NextcloudStackId,
pub title: NextcloudStackTitle,
pub order: NextcloudOrder,
#[serde(rename = "boardId")]
pub board_id: NextcloudBoardId,
#[serde(rename = "ETag")]
pub etag: NextcloudETag,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Card {
pub id: NextcloudCardId,
pub title: NextcloudCardTitle,
pub description: Option<String>,
#[serde(rename = "stackId")]
pub stack_id: NextcloudStackId,
pub order: NextcloudOrder,
pub archived: bool,
pub due_date: Option<String>,
pub labels: Vec<NextcloudLabelId>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Label {
pub id: NextcloudLabelId,
pub title: String,
pub color: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Acl {
pub participant: String,
pub permission_edit: bool,
pub permission_share: bool,
pub permission_manage: bool,
}

19
src/nextcloud/stack.rs Normal file
View file

@ -0,0 +1,19 @@
//
use crate::{p, FullCtx};
use super::DeckClient;
pub async fn list(ctx: FullCtx, dump: bool) -> color_eyre::Result<()> {
let dc = DeckClient::new(&ctx.cfg.nextcloud, ctx.net);
let apiresult = dc.get_stacks(ctx.cfg.nextcloud.board_id()).await;
if dump {
p!("{}", apiresult.text);
} else {
let mut stacks = apiresult.result?;
stacks.sort_by_key(|stack| stack.order);
stacks
.iter()
.for_each(|stack| p!("{}:{}", stack.id, stack.title));
}
Ok(())
}

213
src/nextcloud/tests.rs Normal file
View file

@ -0,0 +1,213 @@
//
use crate::{
config::NextcloudConfig,
nextcloud::{
model::{NextcloudBoardId, NextcloudHostname, NextcloudPassword, NextcloudUsername},
DeckClient,
},
};
mod config {
use super::*;
#[test]
fn config_hostname_returns_hostname() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig::new(hostname.clone(), username, password, board_id);
//when
//then
assert_eq!(cfg.hostname, hostname);
}
#[test]
fn config_username_returns_username() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig::new(hostname, username.clone(), password, board_id);
//when
//then
assert_eq!(cfg.username, username);
}
#[test]
fn config_password_returns_password() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig::new(hostname, username, password.clone(), board_id);
//when
//then
assert_eq!(cfg.password, password);
}
#[test]
fn config_board_id_returns_board_id() {
//given
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
let cfg = NextcloudConfig::new(hostname, username, password, board_id);
//when
//then
assert_eq!(cfg.board_id, board_id);
}
}
mod client {
use kxio::net::StatusCode;
use serde_json::json;
use crate::{
nextcloud::model::{
Acl, Board, Label, NextcloudBoardColour, NextcloudBoardOwner, NextcloudBoardTitle,
NextcloudETag, NextcloudLabelId, NextcloudOrder, NextcloudStackId, NextcloudStackTitle,
Stack,
},
s,
};
use super::*;
#[tokio::test]
async fn get_boards() {
//given
let mock_net = kxio::net::mock();
mock_net
.on()
.get("https://host-name/index.php/apps/deck/api/v1.0/boards")
.basic_auth("username", Some("password"))
.respond(StatusCode::OK)
.body(
json!([{
"id":2,
"title":"board-title",
"owner":"owner",
"color":"red",
"archived":false,
"labels":[
{
"id":2,
"title":"label-title",
"color":"blue"
}
],
"acl":[
{
"participant":"participant",
"permission_edit":true,
"permission_share":false,
"permission_manage":true
}
]
}])
.to_string(),
)
.expect("mock request");
let cfg = given::a_nextcloud_config();
let deck_client = DeckClient::new(&cfg, mock_net.into());
//when
let result = deck_client.get_boards().await.result.expect("get boards");
assert_eq!(
result,
vec![Board {
id: NextcloudBoardId::new(2),
title: NextcloudBoardTitle::new("board-title"),
owner: NextcloudBoardOwner::new("owner"),
color: NextcloudBoardColour::new("red"),
archived: false,
labels: vec![Label {
id: NextcloudLabelId::new(2),
title: s!("label-title"),
color: s!("blue")
}],
acl: vec![Acl {
participant: s!("participant"),
permission_edit: true,
permission_share: false,
permission_manage: true
}]
}]
);
}
#[tokio::test]
async fn get_stacks() {
//given
let mock_net = kxio::net::mock();
mock_net
.on()
.get("https://host-name/index.php/apps/deck/api/v1.0/boards/2/stacks")
.basic_auth("username", Some("password"))
.respond(StatusCode::OK)
.body(include_str!("../tests/responses/nextcloud-stack-list.json"))
.expect("mock request");
let cfg = given::a_nextcloud_config();
let deck_client = DeckClient::new(&cfg, mock_net.into());
//when
let result = deck_client
.get_stacks(cfg.board_id)
.await
.result
.expect("get stacks");
assert_eq!(
result,
vec![
Stack {
id: NextcloudStackId::new(3),
title: NextcloudStackTitle::new("Done"),
order: NextcloudOrder::new(2),
board_id: NextcloudBoardId::new(1),
etag: NextcloudETag::new("97592874d17017ef4f620c9c2a490086")
},
Stack {
id: NextcloudStackId::new(2),
title: NextcloudStackTitle::new("Doing"),
order: NextcloudOrder::new(1),
board_id: NextcloudBoardId::new(1),
etag: NextcloudETag::new("3da05f904903c88450b79e4f8f6e2160")
},
Stack {
id: NextcloudStackId::new(1),
title: NextcloudStackTitle::new("To do"),
order: NextcloudOrder::new(0),
board_id: NextcloudBoardId::new(1),
etag: NextcloudETag::new("b567d287210fa4d9b108ac68d5b087c1")
}
]
);
}
}
mod given {
use super::*;
pub fn a_nextcloud_config() -> NextcloudConfig {
let hostname = NextcloudHostname::new("host-name");
let username = NextcloudUsername::new("username");
let password = NextcloudPassword::new("password");
let board_id = NextcloudBoardId::new(2);
NextcloudConfig::new(hostname, username, password, board_id)
}
}

View file

@ -9,7 +9,7 @@ use crate::{config::AppConfig, f, NAME};
mod config { mod config {
use super::*; use super::*;
use crate::config::TrelloConfig; use crate::config::{NextcloudConfig, TrelloConfig};
use crate::s; use crate::s;
#[test] #[test]
@ -24,6 +24,12 @@ mod config {
"api_key = \"trello-api-key\"", "api_key = \"trello-api-key\"",
"api_secret = \"trello-api-secret\"", "api_secret = \"trello-api-secret\"",
"board_name = \"trello-board-name\"", "board_name = \"trello-board-name\"",
"",
"[nextcloud]",
"hostname = \"nextcloud-hostname\"",
"username = \"nextcloud-username\"",
"password = \"nextcloud-password\"",
"board_id = 22",
] ]
.join("\n"), .join("\n"),
) )
@ -41,6 +47,12 @@ mod config {
api_key: s!("trello-api-key"), api_key: s!("trello-api-key"),
api_secret: s!("trello-api-secret"), api_secret: s!("trello-api-secret"),
board_name: s!("trello-board-name"), board_name: s!("trello-board-name"),
},
nextcloud: NextcloudConfig {
hostname: s!("nextcloud-hostname").into(),
username: s!("nextcloud-username").into(),
password: s!("nextcloud-password").into(),
board_id: 22.into()
} }
} }
); );

View file

@ -0,0 +1,86 @@
[
{
"id": 1,
"title": "Personal Board",
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"color": "0087C5",
"archived": false,
"labels": [],
"acl": [],
"permissions": {
"PERMISSION_READ": true,
"PERMISSION_EDIT": true,
"PERMISSION_MANAGE": true,
"PERMISSION_SHARE": true
},
"users": [],
"shared": 0,
"stacks": [],
"activeSessions": [],
"deletedAt": 0,
"lastModified": 1733337423,
"settings": [],
"ETag": "b567d287210fa4d9b108ac68d5b087c1"
},
{
"id": 4,
"title": "4 Published: Cossmass Infinities",
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"color": "ff0000",
"archived": true,
"labels": [],
"acl": [],
"permissions": {
"PERMISSION_READ": true,
"PERMISSION_EDIT": true,
"PERMISSION_MANAGE": true,
"PERMISSION_SHARE": true
},
"users": [],
"shared": 0,
"stacks": [],
"activeSessions": [],
"deletedAt": 0,
"lastModified": 1699798570,
"settings": [],
"ETag": "5e0fe035f3b95672da3cba633086be37"
},
{
"id": 5,
"title": "Fulfilment: Cossmass Infinities",
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"color": "ff0000",
"archived": true,
"labels": [],
"acl": [],
"permissions": {
"PERMISSION_READ": true,
"PERMISSION_EDIT": true,
"PERMISSION_MANAGE": true,
"PERMISSION_SHARE": true
},
"users": [],
"shared": 0,
"stacks": [],
"activeSessions": [],
"deletedAt": 0,
"lastModified": 1699798567,
"settings": [],
"ETag": "90e2f9d53c5f6ec83088425d4486e54d"
}
]

View file

@ -0,0 +1,216 @@
[
{
"id": 3,
"title": "Done",
"boardId": 1,
"deletedAt": 0,
"lastModified": 1733337412,
"cards": [
{
"id": 322,
"title": "Lunch: Soup & Toast",
"description": "",
"stackId": 3,
"type": "plain",
"lastModified": 1733337412,
"lastEditor": null,
"createdAt": 1733043472,
"labels": [],
"assignedUsers": [
{
"id": 25,
"participant": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"cardId": 322,
"type": 0
}
],
"attachments": null,
"attachmentCount": 0,
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"order": 0,
"archived": false,
"done": null,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "97592874d17017ef4f620c9c2a490086",
"overdue": 0
}
],
"order": 2,
"ETag": "97592874d17017ef4f620c9c2a490086"
},
{
"id": 2,
"title": "Doing",
"boardId": 1,
"deletedAt": 0,
"lastModified": 1733337420,
"cards": [
{
"id": 319,
"title": "That",
"description": "",
"stackId": 2,
"type": "plain",
"lastModified": 1733335979,
"lastEditor": null,
"createdAt": 1732610551,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": 1,
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"order": 0,
"archived": false,
"done": null,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "79aeb703494e67736cc66b35053d258d",
"overdue": 0
},
{
"id": 323,
"title": "Second lunch: Poached Egg & Toasted Muffin",
"description": "",
"stackId": 2,
"type": "plain",
"lastModified": 1733337420,
"lastEditor": null,
"createdAt": 1733043481,
"labels": [],
"assignedUsers": [
{
"id": 26,
"participant": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"cardId": 323,
"type": 0
}
],
"attachments": null,
"attachmentCount": 0,
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"order": 1,
"archived": false,
"done": null,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "3da05f904903c88450b79e4f8f6e2160",
"overdue": 0
}
],
"order": 1,
"ETag": "3da05f904903c88450b79e4f8f6e2160"
},
{
"id": 1,
"title": "To do",
"boardId": 1,
"deletedAt": 0,
"lastModified": 1733337423,
"cards": [
{
"id": 318,
"title": "This",
"description": "",
"stackId": 1,
"type": "plain",
"lastModified": 1733049748,
"lastEditor": null,
"createdAt": 1732610548,
"labels": [],
"assignedUsers": [],
"attachments": null,
"attachmentCount": 1,
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"order": 0,
"archived": false,
"done": null,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "e5007451d88799e3e3d3581cbcb30210",
"overdue": 0
},
{
"id": 321,
"title": "Breakfast: Cereal",
"description": "",
"stackId": 1,
"type": "plain",
"lastModified": 1733337423,
"lastEditor": null,
"createdAt": 1733043461,
"labels": [],
"assignedUsers": [
{
"id": 24,
"participant": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"cardId": 321,
"type": 0
}
],
"attachments": null,
"attachmentCount": 0,
"owner": {
"primaryKey": "pcampbell",
"uid": "pcampbell",
"displayname": "Paul Campbell",
"type": 0
},
"order": 1,
"archived": false,
"done": null,
"duedate": null,
"deletedAt": 0,
"commentsUnread": 0,
"commentsCount": 0,
"ETag": "b567d287210fa4d9b108ac68d5b087c1",
"overdue": 0
}
],
"order": 0,
"ETag": "b567d287210fa4d9b108ac68d5b087c1"
}
]