Skip to main content

linear_api/
ids.rs

1//! Typed ID newtypes for every Linear entity this crate touches, plus
2//! [`IssueRef`] for endpoints that accept a UUID *or* a human identifier.
3
4macro_rules! linear_id {
5    ($name:ident, $doc:literal) => {
6        #[doc = $doc]
7        #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8        pub struct $name(String);
9
10        impl $name {
11            #[doc = concat!("Creates a new `", stringify!($name), "` from a string-like value.")]
12            pub fn new(id: impl Into<String>) -> Self {
13                Self(id.into())
14            }
15
16            /// Returns the underlying ID as a string slice.
17            pub fn as_str(&self) -> &str {
18                &self.0
19            }
20
21            /// Consumes the ID, returning the underlying `String`.
22            pub fn into_inner(self) -> String {
23                self.0
24            }
25        }
26
27        impl std::fmt::Display for $name {
28            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29                f.write_str(&self.0)
30            }
31        }
32
33        impl From<String> for $name {
34            fn from(id: String) -> Self {
35                Self(id)
36            }
37        }
38
39        impl From<&str> for $name {
40            fn from(id: &str) -> Self {
41                Self(id.to_owned())
42            }
43        }
44
45        impl AsRef<str> for $name {
46            fn as_ref(&self) -> &str {
47                &self.0
48            }
49        }
50    };
51}
52
53linear_id!(IssueId, "UUID of a Linear issue.");
54linear_id!(TeamId, "UUID of a Linear team.");
55linear_id!(ProjectId, "UUID of a Linear project.");
56linear_id!(UserId, "UUID of a Linear user.");
57linear_id!(LabelId, "UUID of a Linear issue label.");
58linear_id!(WorkflowStateId, "UUID of a Linear workflow state.");
59linear_id!(ProjectMilestoneId, "UUID of a Linear project milestone.");
60linear_id!(CommentId, "UUID of a Linear comment.");
61linear_id!(IssueRelationId, "UUID of a Linear issue relation.");
62linear_id!(CycleId, "UUID of a Linear cycle.");
63linear_id!(OrganizationId, "UUID of a Linear organization (workspace).");
64linear_id!(ProjectStatusId, "UUID of a Linear project status.");
65linear_id!(TemplateId, "UUID of a Linear template.");
66
67/// Reference to an issue where the API accepts a UUID **or** a human
68/// identifier such as `"ENG-123"`: issue getters, `issueUpdate`'s `id`, and
69/// `issueRelationCreate` inputs.
70///
71/// There is deliberately no `From<&str>`: construct human identifiers
72/// explicitly with [`IssueRef::identifier`] so UUID-vs-`"ENG-123"` intent
73/// stays visible at call sites.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum IssueRef {
76    /// A UUID issue ID.
77    Id(IssueId),
78    /// A human identifier like `"ENG-123"`.
79    Identifier(String),
80}
81
82impl IssueRef {
83    /// Creates a reference from a human identifier such as `"ENG-123"`.
84    pub fn identifier(identifier: impl Into<String>) -> Self {
85        Self::Identifier(identifier.into())
86    }
87
88    /// The string that gets sent to the API.
89    pub fn api_string(&self) -> &str {
90        match self {
91            Self::Id(id) => id.as_str(),
92            Self::Identifier(identifier) => identifier,
93        }
94    }
95}
96
97impl std::fmt::Display for IssueRef {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.write_str(self.api_string())
100    }
101}
102
103impl From<IssueId> for IssueRef {
104    fn from(id: IssueId) -> Self {
105        Self::Id(id)
106    }
107}
108
109impl From<&IssueId> for IssueRef {
110    fn from(id: &IssueId) -> Self {
111        Self::Id(id.clone())
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn id_roundtrips_through_serde_as_a_plain_string() {
121        let id = IssueId::new("abc-123");
122        let json = serde_json::to_string(&id).unwrap();
123        assert_eq!(json, "\"abc-123\"");
124        let back: IssueId = serde_json::from_str(&json).unwrap();
125        assert_eq!(back, id);
126    }
127
128    #[test]
129    fn issue_ref_api_string() {
130        let by_id = IssueRef::from(IssueId::new("uuid-1"));
131        assert_eq!(by_id.api_string(), "uuid-1");
132        let by_identifier = IssueRef::identifier("ENG-123");
133        assert_eq!(by_identifier.api_string(), "ENG-123");
134        assert_eq!(by_identifier.to_string(), "ENG-123");
135    }
136}