|
7 | 7 | //! - `notes set --name <s> --title T [--summary S] [--tag t]... --content -` |
8 | 8 | //! Idempotent upsert. Read-before-write preserves `published_at` and carries |
9 | 9 | //! forward the existing title when `--title` is omitted on an update. |
10 | | -//! - `notes get (--naddr <n> | --name <slug> [--author <ref>]) [--content-only]` |
| 10 | +//! - `notes get (--naddr <n> | --name <slug> [--author <ref>] [--latest]) [--content-only]` |
11 | 11 | //! `--naddr` / coordinate form is exact. `--name` does a cross-author `#d` |
12 | | -//! query; >1 hit prints candidates and exits 1. |
| 12 | +//! query; >1 hit prints candidates and exits 1, unless `--latest` picks the |
| 13 | +//! most recently updated one. `--latest` conflicts with `--author`/`--naddr`. |
13 | 14 | //! - `notes ls [--author <ref>] [--tag t] [--limit N]` — own notes by default. |
14 | 15 | //! - `notes rm --name <s>` — NIP-09 deletion (kind:5) targeting the addressable |
15 | 16 | //! coordinate via an `a` tag only (no `e` tag — see [`build_rm_event`]). |
@@ -619,11 +620,40 @@ pub async fn cmd_set( |
619 | 620 | Ok(()) |
620 | 621 | } |
621 | 622 |
|
| 623 | +/// Validate the flag combination for `notes get`. Pure (booleans only) so the |
| 624 | +/// full matrix is unit-testable without a relay. `--naddr` and `--name` are |
| 625 | +/// exclusive-or; `--author` and `--latest` only refine `--name`, and they |
| 626 | +/// disambiguate the same multi-author case in opposite ways, so they conflict. |
| 627 | +fn validate_get_args(naddr: bool, name: bool, author: bool, latest: bool) -> Result<(), CliError> { |
| 628 | + if naddr == name { |
| 629 | + return Err(CliError::Usage( |
| 630 | + "exactly one of --naddr or --name is required".into(), |
| 631 | + )); |
| 632 | + } |
| 633 | + if naddr && author { |
| 634 | + return Err(CliError::Usage( |
| 635 | + "--author only applies with --name; --naddr already identifies the author".into(), |
| 636 | + )); |
| 637 | + } |
| 638 | + if naddr && latest { |
| 639 | + return Err(CliError::Usage( |
| 640 | + "--latest only applies with --name; --naddr already identifies one note".into(), |
| 641 | + )); |
| 642 | + } |
| 643 | + if author && latest { |
| 644 | + return Err(CliError::Usage( |
| 645 | + "--latest and --author are mutually exclusive".into(), |
| 646 | + )); |
| 647 | + } |
| 648 | + Ok(()) |
| 649 | +} |
| 650 | + |
622 | 651 | pub async fn cmd_get( |
623 | 652 | client: &SproutClient, |
624 | 653 | naddr: Option<&str>, |
625 | 654 | name: Option<&str>, |
626 | 655 | author: Option<&str>, |
| 656 | + latest: bool, |
627 | 657 | content_only: bool, |
628 | 658 | ) -> Result<(), CliError> { |
629 | 659 | let snapshot = if let Some(raw) = naddr { |
@@ -653,10 +683,14 @@ pub async fn cmd_get( |
653 | 683 | 1 => snapshots.remove(0), |
654 | 684 | _ => { |
655 | 685 | sort_snapshots_newest_first(&mut snapshots); |
656 | | - return Err(CliError::Usage(format!( |
657 | | - "note name {slug:?} is ambiguous; pass --author <pubkey>\n{}", |
658 | | - format_note_candidates(&snapshots) |
659 | | - ))); |
| 686 | + if latest { |
| 687 | + snapshots.remove(0) |
| 688 | + } else { |
| 689 | + return Err(CliError::Usage(format!( |
| 690 | + "note name {slug:?} is ambiguous; pass --author <pubkey> or --latest\n{}", |
| 691 | + format_note_candidates(&snapshots) |
| 692 | + ))); |
| 693 | + } |
660 | 694 | } |
661 | 695 | } |
662 | 696 | } |
@@ -799,24 +833,16 @@ pub async fn dispatch(cmd: crate::NotesCmd, client: &SproutClient) -> Result<(), |
799 | 833 | naddr, |
800 | 834 | name, |
801 | 835 | author, |
| 836 | + latest, |
802 | 837 | content_only, |
803 | 838 | } => { |
804 | | - if naddr.is_some() == name.is_some() { |
805 | | - return Err(CliError::Usage( |
806 | | - "exactly one of --naddr or --name is required".into(), |
807 | | - )); |
808 | | - } |
809 | | - if naddr.is_some() && author.is_some() { |
810 | | - return Err(CliError::Usage( |
811 | | - "--author only applies with --name; --naddr already identifies the author" |
812 | | - .into(), |
813 | | - )); |
814 | | - } |
| 839 | + validate_get_args(naddr.is_some(), name.is_some(), author.is_some(), latest)?; |
815 | 840 | cmd_get( |
816 | 841 | client, |
817 | 842 | naddr.as_deref(), |
818 | 843 | name.as_deref(), |
819 | 844 | author.as_deref(), |
| 845 | + latest, |
820 | 846 | content_only, |
821 | 847 | ) |
822 | 848 | .await |
@@ -1300,4 +1326,44 @@ mod tests { |
1300 | 1326 | let event = build_and_sign(Some(&prior), "x", None, None, None, "body", 2_000).unwrap(); |
1301 | 1327 | assert_eq!(tag_value(&event, "published_at"), Some("2000")); |
1302 | 1328 | } |
| 1329 | + |
| 1330 | + // -- validate_get_args -- |
| 1331 | + |
| 1332 | + #[test] |
| 1333 | + fn validate_get_args_accepts_minimal_forms() { |
| 1334 | + // (naddr, name, author, latest) |
| 1335 | + assert!(validate_get_args(true, false, false, false).is_ok()); // --naddr |
| 1336 | + assert!(validate_get_args(false, true, false, false).is_ok()); // --name |
| 1337 | + assert!(validate_get_args(false, true, true, false).is_ok()); // --name --author |
| 1338 | + assert!(validate_get_args(false, true, false, true).is_ok()); // --name --latest |
| 1339 | + } |
| 1340 | + |
| 1341 | + #[test] |
| 1342 | + fn validate_get_args_requires_exactly_one_selector() { |
| 1343 | + let neither = validate_get_args(false, false, false, false); |
| 1344 | + let both = validate_get_args(true, true, false, false); |
| 1345 | + for err in [neither, both] { |
| 1346 | + assert!(matches!(err, Err(CliError::Usage(m)) if m.contains("exactly one"))); |
| 1347 | + } |
| 1348 | + } |
| 1349 | + |
| 1350 | + #[test] |
| 1351 | + fn validate_get_args_rejects_naddr_with_refiners() { |
| 1352 | + assert!(matches!( |
| 1353 | + validate_get_args(true, false, true, false), |
| 1354 | + Err(CliError::Usage(m)) if m.contains("--author only applies with --name") |
| 1355 | + )); |
| 1356 | + assert!(matches!( |
| 1357 | + validate_get_args(true, false, false, true), |
| 1358 | + Err(CliError::Usage(m)) if m.contains("--latest only applies with --name") |
| 1359 | + )); |
| 1360 | + } |
| 1361 | + |
| 1362 | + #[test] |
| 1363 | + fn validate_get_args_rejects_author_and_latest_together() { |
| 1364 | + assert!(matches!( |
| 1365 | + validate_get_args(false, true, true, true), |
| 1366 | + Err(CliError::Usage(m)) if m.contains("mutually exclusive") |
| 1367 | + )); |
| 1368 | + } |
1303 | 1369 | } |
0 commit comments