use crate::canonical_hash::sha256_json_prefixed;
use crate::error::{Result, ShoreError};
use crate::model::{ReviewTargetRef, TargetRef, id_prefix};
pub(crate) fn review_subject_id(target: &ReviewTargetRef) -> Result<String> {
subject_id(&TargetRef::Review(target.clone()))?.ok_or_else(|| {
ShoreError::Message("a review subject must have an opaque subject id".to_owned())
})
}
pub(crate) fn subject_id(subject: &TargetRef) -> Result<Option<String>> {
let mut material = match subject {
TargetRef::Journal => return Ok(None),
TargetRef::Review(review) => serde_json::to_value(review)?,
TargetRef::Task(task) => serde_json::to_value(task)?,
};
if let Some(object) = material.as_object_mut() {
object.remove("kind");
}
let digest = sha256_json_prefixed(&material)?;
Ok(Some(format!("{}:{digest}", id_prefix::SUBJECT)))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::model::{
CheckpointId, ObservationId, ReviewTargetRef, RevisionId, Side, TaskTargetRef, WorkObjectId,
};
fn revision(id: &str) -> RevisionId {
RevisionId::new(id)
}
#[test]
fn subject_id_absent_for_journal_carrier() {
assert_eq!(subject_id(&TargetRef::Journal).unwrap(), None);
}
#[test]
fn subject_id_is_a_prefixed_content_id() {
let subject = TargetRef::Review(ReviewTargetRef::Revision {
revision_id: revision("rev:sha256:abc"),
});
let id = subject_id(&subject).unwrap().unwrap();
assert!(id.starts_with("subject:sha256:"), "got {id}");
}
#[test]
fn subject_id_excludes_the_renamable_kind_tag() {
let subject = TargetRef::Review(ReviewTargetRef::File {
revision_id: revision("rev:sha256:abc"),
file_path: "src/lib.rs".to_owned(),
});
let expected_material = json!({
"revisionId": "rev:sha256:abc",
"filePath": "src/lib.rs",
});
let expected = format!(
"{}:{}",
id_prefix::SUBJECT,
sha256_json_prefixed(&expected_material).unwrap()
);
assert_eq!(subject_id(&subject).unwrap().unwrap(), expected);
}
#[test]
fn subject_id_changes_with_revision_id() {
let subject_x = TargetRef::Review(ReviewTargetRef::Revision {
revision_id: revision("rev:sha256:x"),
});
let subject_y = TargetRef::Review(ReviewTargetRef::Revision {
revision_id: revision("rev:sha256:y"),
});
assert_ne!(
subject_id(&subject_x).unwrap(),
subject_id(&subject_y).unwrap()
);
}
#[test]
fn subject_id_distinguishes_review_sub_anchors() {
let file = TargetRef::Review(ReviewTargetRef::File {
revision_id: revision("rev:sha256:abc"),
file_path: "src/lib.rs".to_owned(),
});
let range = TargetRef::Review(ReviewTargetRef::Range {
revision_id: revision("rev:sha256:abc"),
file_path: "src/lib.rs".to_owned(),
side: Side::New,
start_line: 1,
end_line: 4,
});
assert_ne!(subject_id(&file).unwrap(), subject_id(&range).unwrap());
}
#[test]
fn review_and_task_subjects_do_not_collide() {
let review = TargetRef::Review(ReviewTargetRef::Observation {
revision_id: revision("rev:sha256:abc"),
observation_id: ObservationId::new("obs:sha256:abc"),
});
let task = TargetRef::Task(TaskTargetRef::TaskAttempt {
task_attempt_id: WorkObjectId::new("task-attempt:sha256:abc"),
});
assert_ne!(subject_id(&review).unwrap(), subject_id(&task).unwrap());
}
#[test]
fn subject_id_distinguishes_task_attempts() {
let attempt_a = TargetRef::Task(TaskTargetRef::TaskAttempt {
task_attempt_id: WorkObjectId::new("task-attempt:sha256:a"),
});
let attempt_b = TargetRef::Task(TaskTargetRef::TaskAttempt {
task_attempt_id: WorkObjectId::new("task-attempt:sha256:b"),
});
assert_ne!(
subject_id(&attempt_a).unwrap(),
subject_id(&attempt_b).unwrap()
);
}
#[test]
fn task_checkpoint_distinguishes_from_bare_attempt() {
let bare = TargetRef::Task(TaskTargetRef::TaskAttempt {
task_attempt_id: WorkObjectId::new("task-attempt:sha256:abc"),
});
let checkpoint = TargetRef::Task(TaskTargetRef::Checkpoint {
checkpoint_id: CheckpointId::new("checkpoint:sha256:c"),
});
assert_ne!(subject_id(&bare).unwrap(), subject_id(&checkpoint).unwrap());
}
}