linear-api 0.1.0

Unofficial async Rust client for the Linear GraphQL API (API-key auth)
Documentation
//! Typed ID newtypes for every Linear entity this crate touches, plus
//! [`IssueRef`] for endpoints that accept a UUID *or* a human identifier.

macro_rules! linear_id {
    ($name:ident, $doc:literal) => {
        #[doc = $doc]
        #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
        pub struct $name(String);

        impl $name {
            #[doc = concat!("Creates a new `", stringify!($name), "` from a string-like value.")]
            pub fn new(id: impl Into<String>) -> Self {
                Self(id.into())
            }

            /// Returns the underlying ID as a string slice.
            pub fn as_str(&self) -> &str {
                &self.0
            }

            /// Consumes the ID, returning the underlying `String`.
            pub fn into_inner(self) -> String {
                self.0
            }
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.write_str(&self.0)
            }
        }

        impl From<String> for $name {
            fn from(id: String) -> Self {
                Self(id)
            }
        }

        impl From<&str> for $name {
            fn from(id: &str) -> Self {
                Self(id.to_owned())
            }
        }

        impl AsRef<str> for $name {
            fn as_ref(&self) -> &str {
                &self.0
            }
        }
    };
}

linear_id!(IssueId, "UUID of a Linear issue.");
linear_id!(TeamId, "UUID of a Linear team.");
linear_id!(ProjectId, "UUID of a Linear project.");
linear_id!(UserId, "UUID of a Linear user.");
linear_id!(LabelId, "UUID of a Linear issue label.");
linear_id!(WorkflowStateId, "UUID of a Linear workflow state.");
linear_id!(ProjectMilestoneId, "UUID of a Linear project milestone.");
linear_id!(CommentId, "UUID of a Linear comment.");
linear_id!(IssueRelationId, "UUID of a Linear issue relation.");
linear_id!(CycleId, "UUID of a Linear cycle.");
linear_id!(OrganizationId, "UUID of a Linear organization (workspace).");
linear_id!(ProjectStatusId, "UUID of a Linear project status.");
linear_id!(TemplateId, "UUID of a Linear template.");

/// Reference to an issue where the API accepts a UUID **or** a human
/// identifier such as `"ENG-123"`: issue getters, `issueUpdate`'s `id`, and
/// `issueRelationCreate` inputs.
///
/// There is deliberately no `From<&str>`: construct human identifiers
/// explicitly with [`IssueRef::identifier`] so UUID-vs-`"ENG-123"` intent
/// stays visible at call sites.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IssueRef {
    /// A UUID issue ID.
    Id(IssueId),
    /// A human identifier like `"ENG-123"`.
    Identifier(String),
}

impl IssueRef {
    /// Creates a reference from a human identifier such as `"ENG-123"`.
    pub fn identifier(identifier: impl Into<String>) -> Self {
        Self::Identifier(identifier.into())
    }

    /// The string that gets sent to the API.
    pub fn api_string(&self) -> &str {
        match self {
            Self::Id(id) => id.as_str(),
            Self::Identifier(identifier) => identifier,
        }
    }
}

impl std::fmt::Display for IssueRef {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.api_string())
    }
}

impl From<IssueId> for IssueRef {
    fn from(id: IssueId) -> Self {
        Self::Id(id)
    }
}

impl From<&IssueId> for IssueRef {
    fn from(id: &IssueId) -> Self {
        Self::Id(id.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn id_roundtrips_through_serde_as_a_plain_string() {
        let id = IssueId::new("abc-123");
        let json = serde_json::to_string(&id).unwrap();
        assert_eq!(json, "\"abc-123\"");
        let back: IssueId = serde_json::from_str(&json).unwrap();
        assert_eq!(back, id);
    }

    #[test]
    fn issue_ref_api_string() {
        let by_id = IssueRef::from(IssueId::new("uuid-1"));
        assert_eq!(by_id.api_string(), "uuid-1");
        let by_identifier = IssueRef::identifier("ENG-123");
        assert_eq!(by_identifier.api_string(), "ENG-123");
        assert_eq!(by_identifier.to_string(), "ENG-123");
    }
}