@@ -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
195206async 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 ! ( "\n Dry 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 ! ( "\n Total: {} 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\n K2DEF\n N3GHI" ;
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\n K2DEF met at field day\n N3GHI" ;
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\n W1ABC\n # Another comment\n K2DEF" ;
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\n W1ABC\n // Another comment\n K2DEF" ;
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\n W1ABC" ;
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 \t K2DEF\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 \n W1ABC friend\n \n // Another comment\n K2DEF\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\n W1ABC" ;
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