Skip to main content

ephemeris_forge_github/api/model/
team.rs

1use ephemeris_forge::Observed;
2use facet::Facet;
3
4use crate::Permission;
5
6/// A team and the permission it has.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct Team {
9    /// The team's slug, which identifies it in the API and in URLs.
10    pub slug: String,
11    /// The team's permission.
12    pub permission: Observed<Permission>,
13    /// Whether the grant comes from the organization or the enterprise rather
14    /// than a direct repository assignment.
15    pub inherited: bool,
16}
17
18/// A team the authenticated user belongs to.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct Membership {
21    /// The login of the organization that owns the team.
22    pub organization: String,
23    /// The team's slug.
24    pub team: String,
25}
26
27/// Where a team's access to the repository comes from.
28#[derive(Clone, Copy, Debug, Eq, Facet, PartialEq)]
29#[repr(u8)]
30pub(crate) enum AccessSource {
31    #[facet(rename = "direct")]
32    Direct,
33    #[facet(rename = "organization")]
34    Organization,
35    #[facet(rename = "enterprise")]
36    Enterprise,
37}
38
39/// The permission booleans GitHub reports on teams and collaborators.
40#[derive(Default, Facet)]
41#[expect(
42    clippy::struct_excessive_bools,
43    reason = "this mirrors the five independent flags GitHub reports"
44)]
45pub(crate) struct PermissionFlags {
46    #[facet(default)]
47    pull: bool,
48    #[facet(default)]
49    triage: bool,
50    #[facet(default)]
51    push: bool,
52    #[facet(default)]
53    maintain: bool,
54    #[facet(default)]
55    admin: bool,
56}
57
58/// The wire shape of GitHub's team listing endpoints.
59#[derive(Facet)]
60pub(crate) struct TeamResponse {
61    pub(crate) slug: String,
62    #[facet(default)]
63    pub(crate) permission: String,
64    #[facet(default)]
65    pub(crate) permissions: Option<PermissionFlags>,
66    #[facet(default)]
67    pub(crate) access_source: Option<AccessSource>,
68}
69
70/// The organization in GitHub's membership listing.
71#[derive(Facet)]
72pub(crate) struct MembershipOrganization {
73    login: String,
74}
75
76/// The wire shape of GitHub's listing of the user's teams.
77#[derive(Facet)]
78pub(crate) struct MembershipResponse {
79    slug: String,
80    organization: MembershipOrganization,
81}
82
83/// The wire shape of the authenticated user.
84#[derive(Facet)]
85pub(crate) struct CurrentUserResponse {
86    pub(crate) login: String,
87}
88
89impl From<MembershipResponse> for Membership {
90    fn from(value: MembershipResponse) -> Self {
91        Self {
92            organization: value.organization.login,
93            team: value.slug,
94        }
95    }
96}
97
98impl PermissionFlags {
99    /// The highest permission whose flag is set.
100    pub(crate) const fn highest(&self) -> Option<Permission> {
101        if self.admin {
102            Some(Permission::Admin)
103        } else if self.maintain {
104            Some(Permission::Maintain)
105        } else if self.push {
106            Some(Permission::Push)
107        } else if self.triage {
108            Some(Permission::Triage)
109        } else if self.pull {
110            Some(Permission::Pull)
111        } else {
112            None
113        }
114    }
115}
116
117impl From<TeamResponse> for Team {
118    fn from(value: TeamResponse) -> Self {
119        // A team without `access_source` is treated as a direct grant, so a
120        // nested team is never inherited because of its parent alone.
121        let inherited = matches!(
122            value.access_source,
123            Some(AccessSource::Organization | AccessSource::Enterprise)
124        );
125        Self {
126            permission: Permission::observe(
127                &value.permission,
128                value
129                    .permissions
130                    .as_ref()
131                    .and_then(PermissionFlags::highest),
132            ),
133            slug: value.slug,
134            inherited,
135        }
136    }
137}