Skip to content

Commit a1f9a5e

Browse files
committed
feat: preview mutations with dry run flag
1 parent 15ef25a commit a1f9a5e

8 files changed

Lines changed: 809 additions & 28 deletions

File tree

lib/endpoints/raindrops.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::client::RaindropClient;
22
use crate::error::Error;
3-
use crate::http::Response;
3+
use crate::http::{Response, ResponseMetadata};
44
use crate::models::common::{
55
BoolResponse, CollectionScope, ItemResponse, ItemsResponse, ModifiedResponse,
66
};
@@ -34,6 +34,20 @@ impl RaindropsApi {
3434
})
3535
}
3636

37+
pub async fn get_many(&self, ids: &[i64]) -> Result<Response<Vec<Raindrop>>, Error> {
38+
let mut items = Vec::with_capacity(ids.len());
39+
40+
for id in ids {
41+
let response = self.get(RaindropId::new(*id)).await?;
42+
items.push(response.data);
43+
}
44+
45+
Ok(Response {
46+
data: items,
47+
meta: ResponseMetadata::default(),
48+
})
49+
}
50+
3751
pub async fn create(&self, payload: &CreateRaindrop) -> Result<Response<Raindrop>, Error> {
3852
let res = self
3953
.client

lib/models/raindrops.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,49 @@ impl Raindrop {
3030
.is_some_and(|value| value.id == i64::from(scope)),
3131
}
3232
}
33+
34+
pub fn change_lines(&self, payload: &UpdateRaindrop) -> Vec<String> {
35+
let existing_title = self.title.as_deref().unwrap_or("(untitled)");
36+
let mut changes = Vec::new();
37+
38+
if let Some(title) = payload.title.as_deref() {
39+
changes.push(format!("Title: {existing_title} -> {title}"));
40+
}
41+
42+
if let Some(excerpt) = payload.excerpt.as_deref() {
43+
let existing_excerpt = self.excerpt.as_deref().unwrap_or("(none)");
44+
45+
changes.push(format!("Excerpt: {existing_excerpt} -> {excerpt}"));
46+
}
47+
48+
if let Some(collection) = payload.collection.as_ref() {
49+
let from_collection = self
50+
.collection
51+
.as_ref()
52+
.map(|value| CollectionScope::from(value.id).to_string())
53+
.unwrap_or_else(|| "(none)".to_string());
54+
let to_collection = CollectionScope::from(collection.id);
55+
56+
changes.push(format!("Collection: {from_collection} -> {to_collection}"));
57+
}
58+
59+
if let Some(tags) = payload.tags.as_ref() {
60+
let existing_tags = if self.tags.is_empty() {
61+
"(none)".to_string()
62+
} else {
63+
self.tags.join(", ")
64+
};
65+
let updated_tags = if tags.is_empty() {
66+
"(none)".to_string()
67+
} else {
68+
tags.join(", ")
69+
};
70+
71+
changes.push(format!("Tags: {existing_tags} -> {updated_tags}"));
72+
}
73+
74+
changes
75+
}
3376
}
3477

3578
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]

src/app.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,32 @@ use crate::Command;
22
use crate::config::Config;
33
use puddle::{Error, RaindropClient};
44

5+
pub(crate) struct RunContext {
6+
pub(crate) is_dry_run: bool,
7+
}
8+
59
pub(crate) struct CliApp {
610
pub(crate) config: Config,
711
pub(crate) client: RaindropClient,
12+
pub(crate) context: RunContext,
813
}
914

1015
impl CliApp {
11-
pub(crate) fn new() -> Result<Self, Box<dyn std::error::Error>> {
16+
pub(crate) fn new(context: RunContext) -> Result<Self, Box<dyn std::error::Error>> {
1217
let config = Config::load()?;
1318
let client = RaindropClient::builder()
1419
.access_token(config.values().access_token.clone())
1520
.build()?;
1621

17-
Ok(Self { config, client })
22+
Ok(Self {
23+
config,
24+
client,
25+
context,
26+
})
27+
}
28+
29+
pub(crate) fn is_dry_run(&self) -> bool {
30+
self.context.is_dry_run
1831
}
1932

2033
pub(crate) async fn run_command(

src/collections.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use crate::app::CliApp;
22
use crate::common::{display_text, read_upload_file};
3+
use crate::previews::{
4+
CreateCollectionPreview, DeleteCollectionPreview, UpdateCollectionPreview,
5+
UploadCollectionCoverPreview,
6+
};
37
use clap::{Args, Subcommand};
48
use puddle::models::collections::{Collection, CreateCollection, UpdateCollection};
59
use puddle::models::common::CollectionScope;
@@ -99,6 +103,13 @@ impl CliApp {
99103
parent: args.parent,
100104
extra: HashMap::new(),
101105
};
106+
107+
if self.is_dry_run() {
108+
println!("{}", CreateCollectionPreview::new(&payload));
109+
110+
return Ok(());
111+
}
112+
102113
let response = self.client.collections().create(&payload).await?;
103114
print_collection_detail(&response.data);
104115

@@ -114,6 +125,23 @@ impl CliApp {
114125
parent: args.parent,
115126
extra: HashMap::new(),
116127
};
128+
129+
if self.is_dry_run() {
130+
let existing_collection = self
131+
.client
132+
.collections()
133+
.get(CollectionScope::from(args.id))
134+
.await?
135+
.data;
136+
137+
println!(
138+
"{}",
139+
UpdateCollectionPreview::new(&existing_collection, &payload)
140+
);
141+
142+
return Ok(());
143+
}
144+
117145
let response = self.client.collections().update(args.id, &payload).await?;
118146
print_collection_detail(&response.data);
119147

@@ -124,6 +152,19 @@ impl CliApp {
124152
&self,
125153
args: DeleteCollectionArgs,
126154
) -> Result<(), Box<dyn std::error::Error>> {
155+
if self.is_dry_run() {
156+
let existing_collection = self
157+
.client
158+
.collections()
159+
.get(CollectionScope::from(args.id))
160+
.await?
161+
.data;
162+
163+
println!("{}", DeleteCollectionPreview::new(&existing_collection));
164+
165+
return Ok(());
166+
}
167+
127168
let response = self.client.collections().delete(args.id).await?;
128169
println!("deleted: {}", response.data);
129170

@@ -134,6 +175,23 @@ impl CliApp {
134175
&self,
135176
args: UploadCollectionCoverArgs,
136177
) -> Result<(), Box<dyn std::error::Error>> {
178+
if self.is_dry_run() {
179+
let _ = read_upload_file(&args.path)?;
180+
let existing_collection = self
181+
.client
182+
.collections()
183+
.get(CollectionScope::from(args.id))
184+
.await?
185+
.data;
186+
187+
println!(
188+
"{}",
189+
UploadCollectionCoverPreview::new(&existing_collection, &args.path)
190+
);
191+
192+
return Ok(());
193+
}
194+
137195
let (bytes, mime, file_name) = read_upload_file(&args.path)?;
138196
let response = self
139197
.client
@@ -189,7 +247,6 @@ fn format_collection_summary(item: &Collection) -> String {
189247

190248
parts.join(" | ")
191249
}
192-
193250
#[cfg(test)]
194251
mod tests {
195252
use super::*;

src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ mod common;
44
mod config;
55
mod constants;
66
mod filters;
7+
mod previews;
78
mod raindrops;
89
mod tags;
910
mod user;
1011

11-
use crate::app::CliApp;
12+
use crate::app::{CliApp, RunContext};
1213
use crate::config::{ConfigValues, global_config_path};
1314
use crate::constants::{
1415
AUTHORIZE_URL_BASE, DEFAULT_OAUTH_DEBUG_REDIRECT_URI, RAINDROP_INTEGRATIONS_URI,
@@ -25,6 +26,8 @@ use url::form_urlencoded;
2526
#[derive(Debug, Parser)]
2627
#[command(name = "puddle", version, about = "")]
2728
struct Cli {
29+
#[arg(long = "dry-run", global = true)]
30+
is_dry_run: bool,
2831
#[command(subcommand)]
2932
command: Option<Command>,
3033
}
@@ -94,7 +97,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
9497
println!("{}", path.display());
9598
}
9699
command => {
97-
let mut app = CliApp::new()?;
100+
let context = RunContext {
101+
is_dry_run: cli.is_dry_run,
102+
};
103+
104+
let mut app = CliApp::new(context)?;
98105
app.run_command(command).await?;
99106
}
100107
}

0 commit comments

Comments
 (0)