Skip to content

Commit 227851f

Browse files
committed
Make import-polo-notes take a URL
1 parent b02aa19 commit 227851f

3 files changed

Lines changed: 150 additions & 30 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hamalert-cli"
3-
version = "0.1.0"
3+
version = "0.2.2"
44
edition = "2024"
55

66
[dependencies]

src/main.rs

Lines changed: 148 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ enum Commands {
4040
},
4141
/// Add triggers for all callsigns in a Ham2K PoLo callsign notes file
4242
ImportPoloNotes {
43-
/// Path to the Ham2K PoLo callsign notes file
43+
/// URL to the Ham2K PoLo callsign notes file
4444
#[arg(long)]
45-
file: PathBuf,
45+
url: String,
4646

4747
#[arg(long)]
4848
comment: String,
@@ -52,6 +52,10 @@ enum Commands {
5252

5353
#[arg(long, value_enum)]
5454
mode: Option<Mode>,
55+
56+
/// Show what would be added without actually adding triggers
57+
#[arg(long)]
58+
dry_run: bool,
5559
},
5660
}
5761

@@ -160,19 +164,11 @@ async fn login(client: &Client, username: &str, password: &str) -> Result<(), Bo
160164
Ok(())
161165
}
162166

163-
/// Parse a Ham2K PoLo callsign notes file and extract callsigns.
167+
/// Parse Ham2K PoLo callsign notes content and extract callsigns.
164168
/// Each line's first word is treated as a callsign.
165169
/// Empty lines and comment lines (starting with # or //) are skipped.
166-
fn parse_polo_notes(path: &PathBuf) -> Result<Vec<String>, Box<dyn Error>> {
167-
let content = fs::read_to_string(path).map_err(|e| {
168-
format!(
169-
"Failed to read PoLo notes file at {}: {}",
170-
path.display(),
171-
e
172-
)
173-
})?;
174-
175-
let callsigns: Vec<String> = content
170+
fn parse_polo_notes_content(content: &str) -> Vec<String> {
171+
content
176172
.lines()
177173
.filter_map(|line| {
178174
let trimmed = line.trim();
@@ -187,9 +183,24 @@ fn parse_polo_notes(path: &PathBuf) -> Result<Vec<String>, Box<dyn Error>> {
187183
// Extract the first word (callsign)
188184
trimmed.split_whitespace().next().map(|s| s.to_string())
189185
})
190-
.collect();
186+
.collect()
187+
}
191188

192-
Ok(callsigns)
189+
/// Fetch and parse Ham2K PoLo callsign notes from a URL.
190+
async fn fetch_polo_notes(client: &Client, url: &str) -> Result<Vec<String>, Box<dyn Error>> {
191+
let response = client.get(url).send().await?;
192+
193+
if !response.status().is_success() {
194+
return Err(format!(
195+
"Failed to fetch PoLo notes from {}: {}",
196+
url,
197+
response.status()
198+
)
199+
.into());
200+
}
201+
202+
let content = response.text().await?;
203+
Ok(parse_polo_notes_content(&content))
193204
}
194205

195206
async fn add_trigger(
@@ -265,37 +276,146 @@ async fn main() -> Result<(), Box<dyn Error>> {
265276
}
266277
}
267278
Commands::ImportPoloNotes {
268-
file,
279+
url,
269280
comment,
270281
actions,
271282
mode,
283+
dry_run,
272284
} => {
273-
let callsigns = parse_polo_notes(&file)?;
285+
let callsigns = fetch_polo_notes(&client, &url).await?;
274286

275287
if callsigns.is_empty() {
276-
println!("No callsigns found in {}", file.display());
288+
println!("No callsigns found at {}", url);
277289
return Ok(());
278290
}
279291

280-
println!("Found {} callsigns in {}", callsigns.len(), file.display());
292+
println!("Found {} callsigns at {}", callsigns.len(), url);
281293

282294
let action_strings: Vec<String> =
283295
actions.iter().map(|a| a.as_str().to_string()).collect();
284296

285297
let mode_string = mode.as_ref().map(|m| m.as_str().to_string());
286298

287-
for cs in callsigns {
288-
add_trigger(
289-
&client,
290-
&cs,
291-
&comment,
292-
action_strings.clone(),
293-
mode_string.clone(),
294-
)
295-
.await?;
299+
if dry_run {
300+
println!("\nDry run - would add triggers for:");
301+
for cs in &callsigns {
302+
println!(
303+
" {} (comment: {:?}, actions: {:?}, mode: {:?})",
304+
cs, comment, action_strings, mode_string
305+
);
306+
}
307+
println!("\nTotal: {} triggers", callsigns.len());
308+
} else {
309+
for cs in callsigns {
310+
add_trigger(
311+
&client,
312+
&cs,
313+
&comment,
314+
action_strings.clone(),
315+
mode_string.clone(),
316+
)
317+
.await?;
318+
}
296319
}
297320
}
298321
}
299322

300323
Ok(())
301324
}
325+
326+
#[cfg(test)]
327+
mod tests {
328+
use super::*;
329+
330+
#[test]
331+
fn test_parse_polo_notes_simple_callsigns() {
332+
let content = "W1ABC\nK2DEF\nN3GHI";
333+
let result = parse_polo_notes_content(content);
334+
assert_eq!(result, vec!["W1ABC", "K2DEF", "N3GHI"]);
335+
}
336+
337+
#[test]
338+
fn test_parse_polo_notes_callsigns_with_notes() {
339+
let content = "W1ABC friend from club\nK2DEF met at field day\nN3GHI";
340+
let result = parse_polo_notes_content(content);
341+
assert_eq!(result, vec!["W1ABC", "K2DEF", "N3GHI"]);
342+
}
343+
344+
#[test]
345+
fn test_parse_polo_notes_empty_content() {
346+
let content = "";
347+
let result = parse_polo_notes_content(content);
348+
assert!(result.is_empty());
349+
}
350+
351+
#[test]
352+
fn test_parse_polo_notes_only_empty_lines() {
353+
let content = "\n\n\n";
354+
let result = parse_polo_notes_content(content);
355+
assert!(result.is_empty());
356+
}
357+
358+
#[test]
359+
fn test_parse_polo_notes_hash_comments() {
360+
let content = "# This is a comment\nW1ABC\n# Another comment\nK2DEF";
361+
let result = parse_polo_notes_content(content);
362+
assert_eq!(result, vec!["W1ABC", "K2DEF"]);
363+
}
364+
365+
#[test]
366+
fn test_parse_polo_notes_slash_comments() {
367+
let content = "// This is a comment\nW1ABC\n// Another comment\nK2DEF";
368+
let result = parse_polo_notes_content(content);
369+
assert_eq!(result, vec!["W1ABC", "K2DEF"]);
370+
}
371+
372+
#[test]
373+
fn test_parse_polo_notes_mixed_comments() {
374+
let content = "# Hash comment\n// Slash comment\nW1ABC";
375+
let result = parse_polo_notes_content(content);
376+
assert_eq!(result, vec!["W1ABC"]);
377+
}
378+
379+
#[test]
380+
fn test_parse_polo_notes_whitespace_handling() {
381+
let content = " W1ABC \n\tK2DEF\t\n N3GHI notes here";
382+
let result = parse_polo_notes_content(content);
383+
assert_eq!(result, vec!["W1ABC", "K2DEF", "N3GHI"]);
384+
}
385+
386+
#[test]
387+
fn test_parse_polo_notes_mixed_content() {
388+
let content = "# Header comment\n\nW1ABC friend\n\n// Another comment\nK2DEF\n\n";
389+
let result = parse_polo_notes_content(content);
390+
assert_eq!(result, vec!["W1ABC", "K2DEF"]);
391+
}
392+
393+
#[test]
394+
fn test_parse_polo_notes_only_comments() {
395+
let content = "# Comment 1\n// Comment 2\n# Comment 3";
396+
let result = parse_polo_notes_content(content);
397+
assert!(result.is_empty());
398+
}
399+
400+
#[test]
401+
fn test_parse_polo_notes_indented_comments() {
402+
let content = " # Indented hash comment\n // Indented slash comment\nW1ABC";
403+
let result = parse_polo_notes_content(content);
404+
assert_eq!(result, vec!["W1ABC"]);
405+
}
406+
407+
#[test]
408+
fn test_parse_polo_notes_single_callsign() {
409+
let content = "W1ABC";
410+
let result = parse_polo_notes_content(content);
411+
assert_eq!(result, vec!["W1ABC"]);
412+
}
413+
414+
#[test]
415+
fn test_parse_polo_notes_callsign_with_hash_in_note() {
416+
// A hash in the middle of a note (not at start) should not be treated as comment
417+
let content = "W1ABC note with #hashtag";
418+
let result = parse_polo_notes_content(content);
419+
assert_eq!(result, vec!["W1ABC"]);
420+
}
421+
}

0 commit comments

Comments
 (0)