git-ents.gitmain
⌘K
foforge
entity.rs93 lines · 3.1 KB · rusthistorycomment on this file
1//! The Issue entity: title, body, state, assignees, and labels.
2//!
3//! Spec coverage: `model.issue`.
4
5use ents_attrs as ents;
6use facet::Facet;
7
8use ents_model::MemberId;
9
10/// One issue, living at its own `refs/meta/issues/<id>` ref
11/// (`namespace::issue_ref`, `meta-ref.granularity`).
12///
13/// `model.issue` requires `state` to accept custom values and `assignees`
14/// to accept more than one member — "multiple assignees and custom states
15/// are schema, not platform features" — so `state` is a plain `String`
16/// rather than a fixed enum (contrast [`ents_model::Status`], which *is* a
17/// fixed taxonomy because `model.result-taxonomy` says so explicitly).
18/// Extending this struct with further fields later is a storage migration
19/// (`model.extensibility`, `meta-ref.migration`), not a platform request.
20///
21/// # Examples
22///
23/// ```
24/// use ents_forge::Issue;
25/// use ents_model::MemberId;
26///
27/// let issue = Issue {
28/// title: "gate rejects a valid signature".to_owned(),
29/// body: "steps to reproduce...".to_owned(),
30/// state: "triaged".to_owned(),
31/// assignees: vec![MemberId::new("jdc")],
32/// labels: vec!["bug".to_owned(), "gate".to_owned()],
33/// };
34/// let (id, store) = facet_git_tree::serialize(&issue).expect("serialize");
35/// let back: Issue = facet_git_tree::deserialize(&id, &store).expect("deserialize");
36/// assert_eq!(back, issue);
37/// ```
38// @relation(model.issue, meta-ref.typed-tree, model.extensibility, scope=file)
39#[derive(Debug, Clone, PartialEq, Eq, Facet)]
40pub struct Issue {
41 /// The issue's title.
42 #[facet(ents::col)]
43 pub title: String,
44 /// The issue's body.
45 #[facet(ents::body)]
46 pub body: String,
47 /// The issue's current state. Not a fixed enum: custom states are
48 /// schema, not a platform feature (`model.issue`).
49 #[facet(ents::head)]
50 pub state: String,
51 /// The members assigned to the issue. More than one is ordinary.
52 #[facet(ents::skip_empty)]
53 pub assignees: Vec<MemberId>,
54 /// Free-form labels.
55 #[facet(ents::skip_empty)]
56 pub labels: Vec<String>,
57}
58
59#[cfg(test)]
60mod tests {
61 #![allow(clippy::expect_used, reason = "unit test")]
62
63 use facet_git_tree::{deserialize, serialize};
64 use rstest::rstest;
65
66 use super::*;
67
68 #[rstest]
69 #[case::default_state_no_assignees("open", vec![], vec![])]
70 #[case::custom_state_one_assignee("triaged", vec![MemberId::new("jdc")], vec!["bug".to_owned()])]
71 #[case::custom_state_many_assignees(
72 "needs-review",
73 vec![MemberId::new("jdc"), MemberId::new("ci-worker")],
74 vec!["bug".to_owned(), "gate".to_owned()]
75 )]
76 // @relation(model.issue, meta-ref.typed-tree, scope=function, role=Verifies)
77 fn issue_round_trips_with_custom_state_and_any_assignee_count(
78 #[case] state: &str,
79 #[case] assignees: Vec<MemberId>,
80 #[case] labels: Vec<String>,
81 ) {
82 let issue = Issue {
83 title: "title".to_owned(),
84 body: "body".to_owned(),
85 state: state.to_owned(),
86 assignees,
87 labels,
88 };
89 let (id, store) = serialize(&issue).expect("serialize");
90 let back: Issue = deserialize(&id, &store).expect("deserialize");
91 assert_eq!(back, issue);
92 }
93}