Files
signed/crates/signed_core/src/status.rs
T

214 lines
6.5 KiB
Rust

use nostr::prelude::*;
/// Status of a root patch, pull request or issue, kinds `1630..=1633`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RepoStatus {
Open,
Applied,
Closed,
Draft,
}
impl RepoStatus {
pub fn from_kind(kind: Kind) -> Option<Self> {
match kind {
Kind::GitStatusOpen => Some(Self::Open),
Kind::GitStatusApplied => Some(Self::Applied),
Kind::GitStatusClosed => Some(Self::Closed),
Kind::GitStatusDraft => Some(Self::Draft),
_ => None,
}
}
pub fn kind(self) -> Kind {
match self {
Self::Open => Kind::GitStatusOpen,
Self::Applied => Kind::GitStatusApplied,
Self::Closed => Kind::GitStatusClosed,
Self::Draft => Kind::GitStatusDraft,
}
}
}
/// NIP-10 and NIP-34 use the lowercase `e` tag.
///
/// NIP-22 comments, kind `1111`, use the uppercase `E` tag for the thread root.
pub fn references_root(event: &Event, root: &EventId) -> bool {
let root = root.to_hex();
event
.tags
.iter()
.any(|tag| matches!(tag.kind(), "e" | "E") && tag.content() == Some(root.as_str()))
}
/// Resolve the status of a root event per NIP-34.
///
/// Defaults to [`RepoStatus::Open`].
pub fn resolve_status<'a, I>(
status_events: I,
root_author: &PublicKey,
maintainers: &[PublicKey],
) -> RepoStatus
where
I: IntoIterator<Item = &'a Event>,
{
status_events
.into_iter()
.filter(|e| RepoStatus::from_kind(e.kind).is_some())
.filter(|e| &e.pubkey == root_author || maintainers.contains(&e.pubkey))
.max_by_key(|e| e.created_at)
.and_then(|e| RepoStatus::from_kind(e.kind))
.unwrap_or(RepoStatus::Open)
}
#[cfg(test)]
mod tests {
use super::*;
const ROOT_ID_HEX: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const OTHER_ID_HEX: &str = "2222222222222222222222222222222222222222222222222222222222222222";
fn keys_from_hex(hex: &str) -> Keys {
Keys::new(SecretKey::from_hex(hex).expect("valid secret key"))
}
fn root_event_id() -> EventId {
EventId::from_hex(ROOT_ID_HEX).expect("valid event id")
}
/// Build a signed status event with a controlled `created_at`.
fn status_event(author: &Keys, kind: Kind, root: EventId, created_at: u64) -> Event {
EventBuilder::new(kind, "")
.tags([Tag::event(root)])
.custom_created_at(Timestamp::from(created_at))
.finalize(author)
.expect("signed event")
}
#[test]
fn references_root_matches_e_tag() {
let root = root_event_id();
let event = EventBuilder::new(Kind::GitStatusOpen, "")
.tags([Tag::event(root)])
.finalize(&keys_from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
))
.expect("signed event");
assert!(references_root(&event, &root));
assert!(!references_root(
&event,
&EventId::from_hex(OTHER_ID_HEX).expect("valid id")
));
}
#[test]
fn references_root_matches_uppercase_e_tag() {
let root = root_event_id();
let event = EventBuilder::new(Kind::Comment, "")
.tags([Tag::parse(["E", ROOT_ID_HEX]).expect("valid E tag")])
.finalize(&keys_from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
))
.expect("signed event");
assert!(references_root(&event, &root));
assert!(!references_root(
&event,
&EventId::from_hex(OTHER_ID_HEX).expect("valid id")
));
}
#[test]
fn references_root_false_without_e_tags() {
let event = EventBuilder::new(Kind::GitStatusOpen, "")
.finalize(&keys_from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
))
.expect("signed event");
assert!(!references_root(&event, &root_event_id()));
}
#[test]
fn defaults_to_open_without_status_events() {
let owner =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
let maintainer =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000002");
let statuses: Vec<Event> = Vec::new();
assert_eq!(
resolve_status(
statuses.iter(),
&owner.public_key(),
&[maintainer.public_key()]
),
RepoStatus::Open
);
}
#[test]
fn latest_status_wins() {
let owner =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
let maintainer =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000002");
let root = root_event_id();
let statuses = [
status_event(&maintainer, Kind::GitStatusClosed, root, 100),
status_event(&owner, Kind::GitStatusOpen, root, 200),
];
assert_eq!(
resolve_status(
statuses.iter(),
&owner.public_key(),
&[maintainer.public_key()]
),
RepoStatus::Open
);
}
#[test]
fn ignores_statuses_from_others() {
let owner =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
let maintainer =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000002");
let stranger =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000003");
let root = root_event_id();
let statuses = [
status_event(&stranger, Kind::GitStatusClosed, root, 300),
status_event(&maintainer, Kind::GitStatusDraft, root, 100),
];
assert_eq!(
resolve_status(
statuses.iter(),
&owner.public_key(),
&[maintainer.public_key()]
),
RepoStatus::Draft
);
}
#[test]
fn ignores_non_status_kinds() {
let owner =
keys_from_hex("0000000000000000000000000000000000000000000000000000000000000001");
let root = root_event_id();
let statuses = [status_event(&owner, Kind::GitIssue, root, 100)];
assert_eq!(
resolve_status(statuses.iter(), &owner.public_key(), &[]),
RepoStatus::Open
);
}
}