gitlobster 1.3.0

A tool for cloning all available repositories in a GitLab instance
// Copied from https://gitlab.kitware.com/utils/rust-gitlab/-/blob/master/src/types.rs

#![allow(dead_code)]

use std::fmt::{self, Display, Formatter};
use std::str::FromStr;

use chrono::{DateTime, NaiveDate, Utc};
use serde::de::{DeserializeOwned, Error};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{self, Value};

use crate::gitlab::macros::impl_id;

impl_id!(UserId, "Type-safe user ID.");

/// The states a user account can be in.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum UserState {
    /// The user is active and may perform actions.
    #[serde(rename = "active")]
    Active,
    /// Blocked from logging in.
    #[serde(rename = "blocked")]
    Blocked,
    /// Blocked from logging in via LDAP.
    #[serde(rename = "ldap_blocked")]
    LdapBlocked,
    /// The user is deactivated until the next login.
    #[serde(rename = "deactivated")]
    Deactivated,
}

/// Basic user information.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserBasic {
    /// The username.
    pub username: String,
    /// The display name.
    pub name: String,
    /// The user's ID.
    pub id: UserId,
    /// The state of the user account.
    pub state: UserState,
    /// The URL of the user's avatar.
    pub avatar_url: Option<String>,
    /// The URL of the user's profile page.
    pub web_url: String,
}

/// A unifying trait for all user types.
///
/// This is used to allow (direct) user queries to return the right information because
/// administrator users receive additional information for all user queries versus
/// non-administrator users.
pub trait UserResult: DeserializeOwned {}

impl<T: DeserializeOwned + Into<UserBasic>> UserResult for T {}

/// More detailed information only accessible to administrators.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User {
    /// The username.
    pub username: String,
    /// The display name.
    pub name: String,
    /// The user's ID.
    pub id: UserId,
    /// The state of the user account.
    pub state: UserState,
    /// The URL of the user's avatar.
    pub avatar_url: Option<String>,
    /// The URL of the user's profile page.
    pub web_url: String,
    /// When the account was created.
    pub created_at: Option<DateTime<Utc>>,
    /// Whether the user is an administrator or not.
    ///
    /// Only available when talking to GitLab as an admin.
    pub is_admin: Option<bool>,
    /// The highest access level available to the user.
    ///
    /// Only available when talking to GitLab as an admin.
    pub highest_role: Option<AccessLevel>,
    /// Self-described biography of the user.
    pub bio: Option<String>,
    /// Whether the account has a private profile.
    pub private_profile: Option<bool>,
    /// Geographic location of the user.
    pub location: Option<String>,
    /// User public email address, if any.
    pub public_email: Option<String>,

    /// Skype contact information.
    pub skype: String,
    /// LinkedIn contact information.
    pub linkedin: String,
    /// Twitter contact information.
    pub twitter: String,
    /// Custom URL for the user's website.
    pub website_url: String,
    /// Organization the user belongs to.
    pub organization: Option<String>,
}

impl From<User> for UserBasic {
    fn from(user: User) -> Self {
        UserBasic {
            username: user.username,
            name: user.name,
            id: user.id,
            state: user.state,
            avatar_url: user.avatar_url,
            web_url: user.web_url,
        }
    }
}

/// External authentication tokens.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Identity {
    /// The provider of the token.
    pub provider: String,
    /// The UID for the provider.
    pub extern_uid: String,
}

impl_id!(ThemeId, "Type-safe theme ID.");

impl_id!(ColorSchemeId, "Type-safe color scheme ID.");

/// Full user structure information.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserPublic {
    /// The username.
    pub username: String,
    /// The display name.
    pub name: String,
    /// The user's ID.
    pub id: UserId,
    /// The state of the user account.
    pub state: UserState,
    /// The URL of the user's avatar.
    pub avatar_url: Option<String>,
    /// The URL of the user's profile page.
    pub web_url: String,
    /// When the account was created.
    pub created_at: Option<DateTime<Utc>>,
    /// Whether the user is an administrator or not.
    ///
    /// Only available when talking to GitLab as an admin.
    pub is_admin: Option<bool>,
    /// The highest access level available to the user.
    ///
    /// Only available when talking to GitLab as an admin.
    pub highest_role: Option<AccessLevel>,
    /// Self-described biography of the user.
    pub bio: Option<String>,
    /// Whether the account has a private profile.
    pub private_profile: Option<bool>,
    /// Geographic location of the user.
    pub location: Option<String>,
    /// User public email address, if any.
    pub public_email: Option<String>,

    /// Skype contact information.
    pub skype: String,
    /// LinkedIn contact information.
    pub linkedin: String,
    /// Twitter contact information.
    pub twitter: String,
    /// Custom URL for the user's website.
    pub website_url: String,
    /// Organization the user belongs to.
    pub organization: Option<String>,

    /// When the user last logged in.
    pub last_sign_in_at: Option<DateTime<Utc>>,
    /// When the user last made an action.
    pub last_activity_on: Option<NaiveDate>,
    /// When the user's account was confirmed.
    pub confirmed_at: Option<DateTime<Utc>>,
    /// The primary email address for the user.
    pub email: String,

    /// The theme used by the user, if configured.
    pub theme_id: Option<ThemeId>,
    /// The color scheme used by the user.
    pub color_scheme_id: ColorSchemeId,
    /// The number of projects the user may create.
    pub projects_limit: u64,
    /// When the user's current session started.
    pub current_sign_in_at: Option<DateTime<Utc>>,

    /// List of identities associated with the user.
    pub identities: Vec<Identity>,

    /// Whether the user can create groups.
    pub can_create_group: bool,
    /// Whether the user can create a new project.
    pub can_create_project: bool,
    /// Whether the user has two-factor authentication enabled.
    pub two_factor_enabled: bool,
    /// Whether the account is externally controlled.
    pub external: bool,
}

impl From<UserPublic> for UserBasic {
    fn from(user: UserPublic) -> Self {
        UserBasic {
            username: user.username,
            name: user.name,
            id: user.id,
            state: user.state,
            avatar_url: user.avatar_url,
            web_url: user.web_url,
        }
    }
}

impl From<UserPublic> for User {
    fn from(user: UserPublic) -> Self {
        User {
            username: user.username,
            name: user.name,
            id: user.id,
            state: user.state,
            avatar_url: user.avatar_url,
            web_url: user.web_url,
            created_at: user.created_at,
            is_admin: user.is_admin,
            highest_role: user.highest_role,
            bio: user.bio,
            private_profile: user.private_profile,
            location: user.location,
            public_email: user.public_email,
            skype: user.skype,
            linkedin: user.linkedin,
            twitter: user.twitter,
            website_url: user.website_url,
            organization: user.organization,
        }
    }
}

impl_id!(EmailId, "Type-safe email ID.");

/// Email address.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Email {
    /// ID of the email.
    pub id: EmailId,
    /// The email address.
    pub email: String,
}

impl_id!(HookId, "Type-safe hook ID.");

/// A web hook to notify of events.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Hook {
    /// The ID of the hook.
    pub id: HookId,
    /// The URL to contact.
    pub url: String,
    /// When the hook was created.
    pub created_at: DateTime<Utc>,
    /// Whether the hook is contacted for push events.
    pub push_events: bool,
    /// Filter branches for which the hook is contacted for push events.
    pub push_events_branch_filter: Option<String>,
    /// Whether the hook is contacted for tag push events.
    pub tag_push_events: bool,
    /// Whether the hook is contacted for issue events.
    pub issues_events: bool,
    /// Whether the hook is contacted for confidential issue events.
    pub confidential_issues_events: Option<bool>,
    /// Whether the hook is contacted for merge request events.
    pub merge_requests_events: bool,
    /// Whether the hook is contacted for note events.
    pub note_events: bool,
    /// Whether the hook is contacted for confidential note events.
    pub confidential_note_events: Option<bool>,
    /// Whether the hook is contacted for repository update events.
    pub repository_update_events: bool,
    /// Whether the hook is contacted for job events.
    pub job_events: bool,
    /// Whether the hook is contacted for pipeline events.
    pub pipeline_events: bool,
    /// Whether the hook is contacted for wiki page events.
    pub wiki_page_events: bool,
    /// Whether the communication with the hook is verified using TLS certificates.
    pub enable_ssl_verification: bool,
}

/// A web hook to notify of project events.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectHook {
    /// The ID of the hook.
    pub id: HookId,
    /// The URL to contact.
    pub url: String,
    /// When the hook was created.
    pub created_at: DateTime<Utc>,
    /// The project associated with the hook.
    pub project_id: ProjectId,
    /// Whether the hook is contacted for push events.
    pub push_events: bool,
    /// Filter branches for which the hook is contacted for push events.
    pub push_events_branch_filter: Option<String>,
    /// Whether the hook is contacted for tag push events.
    pub tag_push_events: bool,
    /// Whether the hook is contacted for issue events.
    pub issues_events: bool,
    /// Whether the hook is contacted for confidential issue events.
    pub confidential_issues_events: Option<bool>,
    /// Whether the hook is contacted for merge request events.
    pub merge_requests_events: bool,
    /// Whether the hook is contacted for note events.
    pub note_events: bool,
    /// Whether the hook is contacted for confidential note events.
    pub confidential_note_events: Option<bool>,
    /// Whether the hook is contacted for repository update events.
    pub repository_update_events: bool,
    /// Whether the communication with the hook is verified using TLS certificates.
    pub enable_ssl_verification: bool,
    /// Whether the hook is contacted for job events.
    pub job_events: bool,
    /// Whether the hook is contacted for pipeline events.
    pub pipeline_events: bool,
    /// Whether the hook is contacted for wiki page events.
    pub wiki_page_events: bool,
    /// Whether the hook is contacted for deployment events.
    pub deployment_events: bool,
    /// Whether the hook is contacted for releases events.
    pub releases_events: bool,
    /// Secret token to validate received payloads
    pub token: Option<String>,
}

impl From<ProjectHook> for Hook {
    fn from(hook: ProjectHook) -> Self {
        Hook {
            id: hook.id,
            url: hook.url,
            created_at: hook.created_at,
            push_events: hook.push_events,
            tag_push_events: hook.tag_push_events,
            enable_ssl_verification: hook.enable_ssl_verification,
            push_events_branch_filter: hook.push_events_branch_filter,
            issues_events: hook.issues_events,
            confidential_issues_events: hook.confidential_issues_events,
            merge_requests_events: hook.merge_requests_events,
            note_events: hook.note_events,
            confidential_note_events: hook.confidential_note_events,
            job_events: hook.job_events,
            pipeline_events: hook.pipeline_events,
            wiki_page_events: hook.wiki_page_events,
            repository_update_events: hook.repository_update_events,
        }
    }
}

/// A web hook to notify of events in groups.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GroupHook {
    /// The ID of the hook.
    pub id: HookId,
    /// The URL to contact.
    pub url: String,
    /// When the hook was created.
    pub created_at: DateTime<Utc>,
    /// The project associated with the hook.
    pub project_id: ProjectId,
    /// Whether the hook is contacted for push events.
    pub push_events: bool,
    /// Filter branches for which the hook is contacted for push events.
    pub push_events_branch_filter: Option<String>,
    /// Whether the hook is contacted for tag push events.
    pub tag_push_events: bool,
    /// Whether the hook is contacted for issue events.
    pub issues_events: bool,
    /// Whether the hook is contacted for confidential issue events.
    pub confidential_issues_events: Option<bool>,
    /// Whether the hook is contacted for merge request events.
    pub merge_requests_events: bool,
    /// Whether the hook is contacted for note events.
    pub note_events: bool,
    /// Whether the hook is contacted for confidential note events.
    pub confidential_note_events: Option<bool>,
    /// Whether the hook is contacted for repository update events.
    pub repository_update_events: bool,
    /// Whether the communication with the hook is verified using TLS certificates.
    pub enable_ssl_verification: bool,
    /// Whether the hook is contacted for job events.
    pub job_events: bool,
    /// Whether the hook is contacted for pipeline events.
    pub pipeline_events: bool,
    /// Whether the hook is contacted for wiki page events.
    pub wiki_page_events: bool,
    /// Whether the hook is contacted for deployment events.
    pub deployment_events: bool,
    /// Whether the hook is contacted for releases events.
    pub releases_events: bool,
    /// Whether the hook is contacted for subgroup events.
    pub subgroup_events: bool,
    /// Secret token to validate received payloads
    pub token: Option<String>,
}

impl From<GroupHook> for Hook {
    fn from(hook: GroupHook) -> Self {
        Hook {
            id: hook.id,
            url: hook.url,
            created_at: hook.created_at,
            push_events: hook.push_events,
            push_events_branch_filter: hook.push_events_branch_filter,
            tag_push_events: hook.tag_push_events,
            issues_events: hook.issues_events,
            confidential_issues_events: hook.confidential_issues_events,
            merge_requests_events: hook.merge_requests_events,
            note_events: hook.note_events,
            confidential_note_events: hook.confidential_note_events,
            repository_update_events: hook.repository_update_events,
            job_events: hook.job_events,
            pipeline_events: hook.pipeline_events,
            wiki_page_events: hook.wiki_page_events,
            enable_ssl_verification: hook.enable_ssl_verification,
        }
    }
}

/// Reponse of a project variable
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectVariable {
    /// The key
    pub key: String,
    // The variable type
    pub variable_type: String,
    /// The value
    pub value: String,
    /// Flag if its protected
    pub protected: bool,
    /// Flag if its masked
    pub masked: bool,
    /// Environment scope
    pub environment_scope: String,
}

/// Reponse of a project variable
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProtectedTagAccessLevel {
    /// The access level id
    pub access_level: u64,
    // The access level
    pub access_level_description: String,
}

/// Reponse of a project variable
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProtectedTag {
    /// The name or wildcard
    pub name: String,
    // The access level
    pub create_access_levels: Vec<ProtectedTagAccessLevel>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ReleaseTag {
    // The release tag name
    pub tag_name: String,
    // The release tag description
    pub description: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Tag {
    // Commit message
    pub commit: RepoCommit,
    // Release tag
    pub release: Option<ReleaseTag>,
    // Tag name
    pub name: String,
    // Target sha
    pub target: String,
    pub message: Option<String>,
    // Is tag protected
    pub protected: bool,
}

impl_id!(ProjectId, "Type-safe project ID.");

/// Basic project information.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BasicProjectDetails {
    /// The ID of the project.
    pub id: ProjectId,
    /// The display name of the project.
    pub name: String,
    /// The display name of the project with the namespace.
    pub name_with_namespace: String,
    /// The path to the project's repository.
    pub path: String,
    /// The path to the project's repository with its namespace.
    pub path_with_namespace: String,
    /// The URL to the main page of the repository.
    pub http_url_to_repo: String,
    /// The URL to the main page of the repository.
    pub web_url: String,
}

/// Visibility levels of projects.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum VisibilityLevel {
    /// The project is visible to anonymous users.
    #[serde(rename = "public")]
    Public,
    /// The project is visible to logged in users.
    #[serde(rename = "internal")]
    Internal,
    /// The project is visible only to users with explicit access.
    #[serde(rename = "private")]
    Private,
}

/// Visibility levels for project features.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FeatureVisibilityLevel {
    /// Feature is disabled.
    #[serde(rename = "disabled")]
    Disabled,
    /// Feature is enabled and accessible privately.
    #[serde(rename = "private")]
    Private,
    /// Feature is enabled and accessible with project-wide visibility level.
    #[serde(rename = "enabled")]
    Enabled,
    /// Feature is enabled and accessible publicly.
    #[serde(rename = "public")]
    Public,
}

// TODO: enum for NotificationLevel

/// Structure for a group a project has been shared with.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SharedGroup {
    /// The ID of the group.
    pub group_id: GroupId,
    /// The name of the group.
    pub group_name: String,
    /// The access level of the group.
    pub group_access_level: u64,
}

/// Access information to a project.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
// Called `MemberAccess` in entities.rb, but it is just a base class for `ProjectAccess` and
// `GroupAccess`. Combine them here.
pub struct MemberAccess {
    /// The access level of the membership (see `VisibilityLevel`).
    pub access_level: u64,
    /// The notification level of the current user.
    pub notification_level: Option<u64>,
}

/// Permissions granted to the current user to a project.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct Permissions {
    /// The access granted by the project to the current user.
    pub project_access: Option<MemberAccess>,
    /// The access granted by the group to the current user.
    pub group_access: Option<MemberAccess>,
}

/// The avatar of a project's namespace.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectNamespaceAvatar {
    /// The URL of the namespace avatar.
    pub url: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct ProjectLinks {
    #[serde(rename = "self")]
    /// API URL of project itself.
    self_: String,
    /// API URL of project issues, if enabled.
    issues: Option<String>,
    /// API URL of project merge requests, if enabled.
    merge_requests: Option<String>,
    /// API URL of project repository branches.
    repo_branches: String,
    /// API URL of project labels.
    labels: String,
    /// API URL of project events.
    events: String,
    /// API URL of project members.
    members: String,
}

/// Project information.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Project {
    /// The ID of the project.
    pub id: ProjectId,
    /// The description of the project.
    pub description: Option<String>,
    /// The default branch for the project.
    pub default_branch: Option<String>,
    /// A list of tags for the project.
    pub tag_list: Vec<String>,
    /// Whether the project is archived or not.
    pub archived: bool,
    /// Whether the project has an empty repository or not.
    pub empty_repo: bool,
    /// Whether the project is public, internal, or private.
    pub visibility: VisibilityLevel,
    /// The URL to clone the repository over SSH.
    pub ssh_url_to_repo: String,
    /// The URL to clone the repository over HTTPS.
    pub http_url_to_repo: String,
    /// The URL for the project's homepage.
    pub web_url: String,
    /// The URL for the project's readme.
    pub readme_url: Option<String>,
    /// The owner of the project (`None` for a group-owned project).
    pub owner: Option<UserBasic>,
    /// The display name of the project.
    pub name: String,
    /// The display name of the project with the namespace.
    pub name_with_namespace: String,
    /// The path to the project's repository.
    pub path: String,
    /// The path to the project's repository with its namespace.
    pub path_with_namespace: String,
    /// Whether the continuous integration container registry is enabled.
    ///
    /// This is supposed to be just `bool`, but projects created before the registry was
    /// supported appear to return `null`.
    pub container_registry_enabled: Option<bool>,
    /// When the repository was created.
    pub created_at: DateTime<Utc>,
    /// When the last activity on the project occurred.
    pub last_activity_at: DateTime<Utc>,
    /// Whether continuous integration shared runners are enabled.
    pub shared_runners_enabled: bool,
    /// Whether LFS object storage is enabled.
    pub lfs_enabled: bool,
    /// The user who created the repository.
    pub creator_id: UserId,
    /// The namespace the project lives in.
    pub namespace: Namespace,
    /// If the project is a fork, details about it.
    pub forked_from_project: Option<BasicProjectDetails>,
    /// The URL to the project avatar.
    pub avatar_url: Option<String>,
    /// The path to CI config file.
    pub ci_config_path: Option<String>,
    /// The default Git strategy for CI jobs of the project.
    pub build_git_strategy: Option<String>,
    /// The default number of revisions to fetch in CI jobs.
    pub ci_default_git_depth: Option<u64>,
    /// Description of error if project failed to import.
    pub import_error: Option<String>,
    /// The number of stars for the project.
    pub star_count: u64,
    /// The number of forks.
    pub forks_count: u64,
    /// The number of open issues (if issues are enabled).
    pub open_issues_count: Option<u64>,
    /// The continuous integration runner token (if enabled).
    pub runners_token: Option<String>,
    /// Whether jobs are publicly visible.
    pub public_jobs: bool,
    /// Groups the project is shared with.
    pub shared_with_groups: Vec<SharedGroup>,
    /// Whether the project only enables the merge button if all pipelines are passing.
    pub only_allow_merge_if_pipeline_succeeds: Option<bool>,
    /// Whether the project only enables the merge button if all discussions are resolved.
    pub only_allow_merge_if_all_discussions_are_resolved: Option<bool>,
    /// Whether enable 'Delete source branch' option by default for all new merge requests.
    pub remove_source_branch_after_merge: Option<bool>,
    /// Whether to show the link to create/view merge request when pusing from command line.
    pub printing_merge_request_link_enabled: Option<bool>,
    /// Whether access to the project may be requested.
    pub request_access_enabled: bool,
    /// Whether to automatically resolve merge request diff discussions when they become outdated,
    /// if configured.
    pub resolve_outdated_diff_discussions: Option<bool>,

    /// Whether jobs are enabled or not.
    pub jobs_enabled: bool,
    /// Whether issues are enabled or not.
    pub issues_enabled: bool,
    /// Whether merge requests are enabled or not.
    pub merge_requests_enabled: bool,
    /// Whether snippets are enabled or not.
    pub snippets_enabled: bool,
    /// Whether the project wiki is enabled or not.
    pub wiki_enabled: bool,

    /// Visibility of builds.
    pub builds_access_level: FeatureVisibilityLevel,
    /// Visibility of issues.
    pub issues_access_level: FeatureVisibilityLevel,
    /// Visibility of merge requests.
    pub merge_requests_access_level: FeatureVisibilityLevel,
    /// Visibility of repository.
    pub repository_access_level: FeatureVisibilityLevel,
    /// Visibility of snippets.
    pub snippets_access_level: FeatureVisibilityLevel,
    /// Visibility of wiki.
    pub wiki_access_level: FeatureVisibilityLevel,

    /// The merge method used when merging merge request.
    pub merge_method: Option<String>,
    /// Statistics about the project.
    pub statistics: Option<ProjectStatistics>,

    /// If this is present, it is `ProjectWithAccess`, but since it is so similar, just have it be
    /// optional here.
    pub permissions: Option<Permissions>,

    /// Links to related API URLs provided by GitLab in response to
    /// direct project lookup.  We do not expose this because our
    /// clients do not need them.
    _links: Option<ProjectLinks>,
}

#[cfg(test)]
impl Project {
    pub fn has_links(&self) -> bool {
        self._links.is_some()
    }
}

/// Statistics about a project.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct ProjectStatistics {
    /// The number of commits in the repository.
    pub commit_count: u64,
    /// The size, in bytes, of the total storage required for the project.
    pub storage_size: u64,
    /// The size, in bytes, of the repository.
    pub repository_size: u64,
    /// The size, in bytes, of uploaded LFS files.
    pub lfs_objects_size: u64,
    /// The size, in bytes, of uploaded job artifacts.
    pub job_artifacts_size: u64,
}

/// Access levels for groups and projects.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AccessLevel {
    /// Anonymous access.
    Anonymous,
    /// Guest access (can see the project).
    Guest,
    /// Reporter access (can open issues).
    Reporter,
    /// Developer access (can push branches, handle issues and merge requests).
    Developer,
    /// Maintainer access (can push to protected branches).
    Maintainer,
    /// Owner access (full rights).
    Owner,
    /// Admin access (full rights).
    Admin,
}

impl From<AccessLevel> for u64 {
    fn from(access: AccessLevel) -> Self {
        match access {
            AccessLevel::Anonymous => 0,
            AccessLevel::Guest => 10,
            AccessLevel::Reporter => 20,
            AccessLevel::Developer => 30,
            AccessLevel::Maintainer => 40,
            AccessLevel::Owner => 50,
            AccessLevel::Admin => 60,
        }
    }
}

impl From<u64> for AccessLevel {
    fn from(access: u64) -> Self {
        if access >= 60 {
            AccessLevel::Admin
        } else if access >= 50 {
            AccessLevel::Owner
        } else if access >= 40 {
            AccessLevel::Maintainer
        } else if access >= 30 {
            AccessLevel::Developer
        } else if access >= 20 {
            AccessLevel::Reporter
        } else if access >= 10 {
            AccessLevel::Guest
        } else {
            AccessLevel::Anonymous
        }
    }
}

impl AccessLevel {
    pub fn as_str(&self) -> &str {
        match self {
            AccessLevel::Admin => "admin",
            AccessLevel::Owner => "owner",
            AccessLevel::Developer => "developer",
            AccessLevel::Anonymous => "anonymous",
            AccessLevel::Guest => "guest",
            AccessLevel::Maintainer => "maintainer",
            AccessLevel::Reporter => "reporter",
        }
    }
}

impl Display for AccessLevel {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}", Into::<u64>::into(*self))
    }
}

impl Serialize for AccessLevel {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        u64::from(*self).serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for AccessLevel {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        Ok(<u64 as Deserialize>::deserialize(deserializer)?.into())
    }
}

/// A member with extra permissions on a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Member {
    /// The username.
    pub username: String,
    /// The display name.
    pub name: String,
    /// The user's ID.
    pub id: UserId,
    /// The state of the user account.
    pub state: UserState,
    /// The URL of the user's avatar.
    pub avatar_url: Option<String>,
    /// The URL of the user's profile page.
    pub web_url: String,
    /// The access level of the user.
    pub access_level: u64,
    /// When the membership expires.
    pub expires_at: Option<NaiveDate>,
}

impl From<Member> for UserBasic {
    fn from(member: Member) -> Self {
        UserBasic {
            username: member.username,
            name: member.name,
            id: member.id,
            state: member.state,
            avatar_url: member.avatar_url,
            web_url: member.web_url,
        }
    }
}

/// A member with extra permissions on a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AccessRequester {
    /// The username.
    pub username: String,
    /// The display name.
    pub name: String,
    /// The user's ID.
    pub id: UserId,
    /// The state of the user account.
    pub state: UserState,
    /// The URL of the user's avatar.
    pub avatar_url: Option<String>,
    /// The URL of the user's profile page.
    pub web_url: String,
    /// When the membership request was created.
    pub requested_at: DateTime<Utc>,
}

impl From<AccessRequester> for UserBasic {
    fn from(member: AccessRequester) -> Self {
        UserBasic {
            username: member.username,
            name: member.name,
            id: member.id,
            state: member.state,
            avatar_url: member.avatar_url,
            web_url: member.web_url,
        }
    }
}

impl_id!(GroupId, "Type-safe group ID.");

/// Group information.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Group {
    /// The ID of the group.
    pub id: GroupId,
    /// The name of the group.
    pub name: String,
    /// The path to the group.
    pub path: String,
    /// The description of the group.
    pub description: Option<String>,
    /// Whether the project is public, internal, or private.
    pub visibility: VisibilityLevel,
    /// Whether LFS is enabled for the group.
    pub lfs_enabled: bool,
    /// The URL to the group avatar.
    pub avatar_url: Option<String>,
    /// The URL to the group's profile page.
    pub web_url: String,
    /// Whether membership requests are allowed for the group.
    pub request_access_enabled: bool,
    pub full_name: String,
    pub full_path: String,
    pub parent_id: Option<GroupId>,
    /// Statistics about the group.
    pub statistics: Option<GroupStatistics>,
}

/// Statistics about a group.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct GroupStatistics {
    /// The size, in bytes, of the total storage required for the group.
    pub storage_size: u64,
    /// The size, in bytes, of all repositories in the group.
    pub repository_size: u64,
    /// The size, in bytes, of uploaded LFS files in the group.
    pub lfs_objects_size: u64,
    /// The size, in bytes, of uploaded job artifacts in the group.
    pub job_artifacts_size: u64,
}

/// Group information with a project listing.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GroupDetail {
    /// The ID of the group.
    pub id: GroupId,
    /// The name of the group.
    pub name: String,
    /// The path to the group.
    pub path: String,
    /// The description of the group.
    pub description: Option<String>,
    /// Whether the project is public, internal, or private.
    pub visibility: VisibilityLevel,
    /// Whether LFS is enabled for the group.
    pub lfs_enabled: bool,
    /// The URL to the group avatar.
    pub avatar_url: Option<String>,
    /// The URL to the group's profile page.
    pub web_url: String,
    /// The projects in a group.
    pub projects: Vec<Project>,
    /// Projects the group shares with other groups or users.
    pub shared_projects: Vec<Project>,
    /// Whether membership requests are allowed for the group.
    pub request_access_enabled: bool,
    pub full_name: String,
    pub full_path: String,
    pub parent_id: Option<GroupId>,
    /// Statistics about the group.
    pub statistics: Option<GroupStatistics>,
}

impl From<GroupDetail> for Group {
    fn from(detail: GroupDetail) -> Self {
        Group {
            id: detail.id,
            name: detail.name,
            path: detail.path,
            description: detail.description,
            visibility: detail.visibility,
            lfs_enabled: detail.lfs_enabled,
            avatar_url: detail.avatar_url,
            web_url: detail.web_url,
            request_access_enabled: detail.request_access_enabled,
            full_name: detail.full_name,
            full_path: detail.full_path,
            parent_id: detail.parent_id,
            statistics: detail.statistics,
        }
    }
}

/// A branch on a repository.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RepoBranch {
    /// The name of the branch.
    pub name: String,
    /// The commit of the branch.
    pub commit: Option<RepoCommit>,
    /// Whether the branch is merged into the main branch or not.
    pub merged: Option<bool>,
    /// Whether the branch is protected or not.
    pub protected: Option<bool>,
    /// Whether the developers can push directly to the branch or not.
    pub developers_can_push: Option<bool>,
    /// Whether the developers can merge into the branch or not.
    pub developers_can_merge: Option<bool>,
    /// Whether the current user can push to the branch.
    pub can_push: Option<bool>,
    /// Whether the branch is the repository default branch.
    pub default: Option<bool>,
}

#[allow(clippy::upper_case_acronyms)]
#[derive(Deserialize, Debug, Clone)]
pub struct PRBAccessLevel {
    pub access_level: u64,
    pub access_level_description: String,
}

/// A protected branch on a repository
#[derive(Deserialize, Debug, Clone)]
pub struct ProtectedRepoBranch {
    pub name: String,
    pub push_access_levels: Vec<PRBAccessLevel>,
    pub merge_access_levels: Vec<PRBAccessLevel>,
    pub code_owner_approval_required: Option<bool>,
}

/// The ID of a git object.
#[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)]
pub struct ObjectId(String);

impl ObjectId {
    /// Construct a new `ObjectId`
    pub fn new<O: ToString>(oid: O) -> Self {
        ObjectId(oid.to_string())
    }

    /// The value of the id.
    pub fn value(&self) -> &String {
        &self.0
    }
}

/// The kinds of objects Gitlab can return.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectType {
    /// A `tree` object.
    #[serde(rename = "tree")]
    Tree,
    /// A `blob` object.
    #[serde(rename = "blob")]
    Blob,
}

/// An object inside of a repository.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RepoTreeObject {
    /// The ID of the object.
    pub id: ObjectId,
    /// The name of the object.
    pub name: String,
    #[serde(rename = "type")]
    /// The type of the object.
    pub type_: ObjectType,
    /// The path to the object inside of the repository.
    pub path: String,
    /// The mode of the object.
    pub mode: String,
}

/// A commit in a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RepoCommit {
    /// The ID of the commit.
    pub id: ObjectId,
    /// The short ID of the commit.
    pub short_id: ObjectId,
    /// The summary of the commit.
    pub title: String,
    /// The commit ID of the parents of the commit.
    /// Can be null on projects/:id/repository/branches call
    pub parent_ids: Option<Vec<ObjectId>>,
    /// The commit author's name.
    pub author_name: String,
    /// The commit author's email address.
    pub author_email: String,
    /// The commit's authorship date.
    pub authored_date: DateTime<Utc>,
    /// The committer's name.
    pub committer_name: String,
    /// The committer's email address.
    pub committer_email: String,
    /// The commit's commit date.
    pub committed_date: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
    /// The full commit message.
    pub message: String,
}

/// A commit in a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MergeRequestCommit {
    /// The ID of the commit.
    pub id: ObjectId,
    /// The short ID of the commit.
    pub short_id: ObjectId,
    /// The summary of the commit.
    pub title: String,
    /// The commit author's name.
    pub author_name: String,
    /// The commit author's email address.
    pub author_email: String,
    pub created_at: DateTime<Utc>,
    /// The full commit message.
    pub message: String,
}

/// Stats about a commit.
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct RepoCommitStats {
    /// The number of lines added by the commit.
    pub additions: u64,
    /// The number of lines deleted by the commit.
    pub deletions: u64,
    /// The number of lines changed by the commit.
    pub total: u64,
}

/// A commit in a project with statistics.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RepoCommitDetail {
    /// The ID of the commit.
    pub id: ObjectId,
    /// The short ID of the commit.
    pub short_id: ObjectId,
    /// The summary of the commit.
    pub title: String,
    /// The commit ID of the parents of the commit.
    pub parent_ids: Vec<ObjectId>,
    /// The commit author's name.
    pub author_name: String,
    /// The commit author's email address.
    pub author_email: String,
    /// The commit's authorship date.
    pub authored_date: DateTime<Utc>,
    /// The committer's name.
    pub committer_name: String,
    /// The committer's email address.
    pub committer_email: String,
    /// The commit's commit date.
    pub committed_date: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
    /// The full commit message.
    pub message: String,
    /// Statistics about the commit.
    pub stats: Option<RepoCommitStats>,
    /// The last pipeline for this commit, if any.
    pub last_pipeline: Option<PipelineBasic>,
    /// The project associated with the commit.
    pub project_id: ProjectId,
    // XXX: Investigate what this is.
    /// This looks to be CI related; ignoring without better docs.
    status: Value,
}

impl_id!(SnippetId, "Type-safe snippet ID.");

/// A project-specific snippet.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectSnippet {
    /// The ID of the snippet.
    pub id: SnippetId,
    /// The title of the snippet.
    pub title: String,
    /// The name of the snippet.
    pub file_name: String,
    /// The author of the snippet.
    pub author: UserBasic,
    /// When the snippet was last updated.
    pub updated_at: DateTime<Utc>,
    /// When the snippet was created.
    pub created_at: DateTime<Utc>,
    /// When the snippet was created.
    pub expires_at: Option<DateTime<Utc>>,
    /// The URL of the snippet.
    pub web_url: String,
}

// This is just used as a common "base class" in Ruby.
//#[derive(Serialize, Deserialize, Debug, Clone)]
//pub struct ProjectEntity {
//    pub id: ProjectEntityId,
//    pub iid: ProjectEntityInternalId,
//    pub project_id: ProjectId,
//    pub title: String,
//    pub description: String,
//    pub state: ProjectEntityState,
//    pub created_at: DateTime<Utc>,
//    pub updated_at: DateTime<Utc>,
//}

/// A diff within a repository.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RepoDiff {
    /// The path on the old side of the diff.
    pub old_path: String,
    /// The path on the new side of the diff.
    pub new_path: String,
    /// The mode on the old side of the diff.
    pub a_mode: String,
    /// The mode on the new side of the diff.
    pub b_mode: String,
    pub diff: String,
    /// Whether the diff indicates the addition of a file.
    pub new_file: bool,
    /// Whether the diff indicates the rename of a file.
    pub renamed_file: bool,
    /// Whether the diff indicates the deletion of a file.
    pub deleted_file: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct DiffRefs {
    /// SHA referencing base commit in the source branch
    pub base_sha: Option<ObjectId>,
    /// SHA referencing head commit in the source branch
    pub head_sha: Option<ObjectId>,
    /// SHA referencing commit in target branch
    pub start_sha: Option<ObjectId>,
}

impl_id!(MilestoneId, "Type-safe milestone ID.");

impl_id!(
    MilestoneInternalId,
    "Type-safe milestone internal ID (internal to a project).",
);

/// The states a milestone may be in.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum MilestoneState {
    /// The milestone is active.
    #[serde(rename = "active")]
    Active,
    /// The milestone has been closed.
    #[serde(rename = "closed")]
    Closed,
}

/// A milestone in a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Milestone {
    /// The ID of the milestone.
    pub id: MilestoneId,
    /// The user-visible ID of the milestone.
    pub iid: MilestoneInternalId,
    /// The ID of the project if this is a project milestone.
    pub project_id: Option<ProjectId>,
    /// The ID of the group if this is a group milestone.
    pub group_id: Option<GroupId>,
    /// The title of the milestone.
    pub title: String,
    /// The description of the milestone.
    pub description: Option<String>,
    /// The state of the milestone.
    pub state: MilestoneState,
    /// When the milestone was created.
    pub created_at: DateTime<Utc>,
    /// When the milestone was last updated.
    pub updated_at: DateTime<Utc>,
    /// When the milestone is due.
    pub due_date: Option<NaiveDate>,
    /// When the milestone was started.
    pub start_date: Option<NaiveDate>,
}

impl Milestone {
    /// Create a new blank milestone: it needs at least the ProjectId and title
    /// ProjectId and title are mandatory for new milestone API of Gitlab
    pub fn new_for_project(project_id: ProjectId, title: String) -> Milestone {
        Milestone {
            id: MilestoneId::new(0),
            iid: MilestoneInternalId::new(0),
            project_id: Some(project_id),
            group_id: None,
            title,
            description: None,
            state: MilestoneState::Active,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            due_date: None,
            start_date: None,
        }
    }
    /// Create a new blank group milestone: it needs at least the GroupId and title
    /// GroupId and title are mandatory for new milestone API of Gitlab
    pub fn new_for_group(group_id: GroupId, title: String) -> Milestone {
        Milestone {
            id: MilestoneId::new(0),
            iid: MilestoneInternalId::new(0),
            project_id: None,
            group_id: Some(group_id),
            title,
            description: None,
            state: MilestoneState::Active,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            due_date: None,
            start_date: None,
        }
    }
    /// Complements the milestone with optional paramater: description
    pub fn with_description(mut self, description: String) -> Milestone {
        self.description = Some(description);
        self
    }
    /// Complements the milestone with optional parameter: due_date
    pub fn with_due_date(mut self, due_date: NaiveDate) -> Milestone {
        self.due_date = Some(due_date);
        self
    }
    /// Complements the milestone with optional parameter: start_date
    pub fn with_start_date(mut self, start_date: NaiveDate) -> Milestone {
        self.start_date = Some(start_date);
        self
    }
}

impl_id!(LabelId, "Type-safe label ID.");

/// Type-safe label color.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct LabelColor(String);

impl LabelColor {
    /// Creates a LabelColor from RGB values
    pub fn from_rgb(r: u8, g: u8, b: u8) -> LabelColor {
        LabelColor(format!("#{:02X}{:02X}{:02X}", r, g, b))
    }

    /// Get the value from a LabelColor
    pub fn value(self) -> String {
        self.0
    }
}

impl FromStr for LabelColor {
    type Err = ();

    /// Creates a LabelColor from standard HTML values
    fn from_str(stdcolor: &str) -> Result<Self, Self::Err> {
        let hex = match stdcolor {
            "white" => "FFFFFF",
            "silver" => "C0C0C0",
            "gray" => "808080",
            "black" => "000000",
            "red" => "FF0000",
            "maroon" => "800000",
            "yellow" => "FFFF00",
            "olive" => "808000",
            "lime" => "00FF00",
            "green" => "008000",
            "aqua" => "00FFFF",
            "teal" => "008080",
            "blue" => "0000FF",
            "navy" => "000080",
            "fuchsia" => "FF00FF",
            "purple" => "800080",
            _ => "808080",
        };

        Ok(LabelColor(format!("#{}", hex)))
    }
}

/// An label on a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Label {
    /// The Id of the label.
    pub id: LabelId,
    /// The name of the label.
    pub name: String,
    /// The color of the label.
    pub color: LabelColor,
    /// The description of the label.
    pub description: Option<String>,
    /// The number of opened issues associated with the label.
    pub open_issues_count: Option<u64>,
    /// the number of closed issues associated with the label.
    pub closed_issues_count: Option<u64>,
    /// The number of open merge request associated with the label.
    pub open_merge_requests_count: Option<u64>,
    /// Whether or not the account connecting has subscribed to the label.
    pub subscribed: bool,
    /// The priority of the label.
    pub priority: Option<u64>,
}

impl Label {
    /// Create a new Label: it needs at least a name and a color.
    /// ProjectId is mandatory for Gitlab API
    pub fn new(name: String, color: LabelColor) -> Label {
        Label {
            id: LabelId::new(0),
            name,
            color,
            description: None,
            open_issues_count: None,
            closed_issues_count: None,
            open_merge_requests_count: None,
            subscribed: false,
            priority: None,
        }
    }
    /// Complements the label with optional parameter: description
    pub fn with_description(mut self, description: String) -> Label {
        self.description = Some(description);
        self
    }

    /// Complements the label with optional parameter: priority
    pub fn with_priority(mut self, priority: u64) -> Label {
        self.priority = Some(priority);
        self
    }
}

impl_id!(IssueId, "Type-safe issue ID.");

impl_id!(
    IssueInternalId,
    "Type-safe issue internal ID (internal to a project).",
);

/// The states an issue may be in.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueState {
    /// The issue is open.
    #[serde(rename = "opened")]
    Opened,
    /// The issue has been closed.
    #[serde(rename = "closed")]
    Closed,
    /// The issue has been opened after being closed.
    #[serde(rename = "reopened")]
    Reopened,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct IssueLinks {
    #[serde(rename = "self")]
    /// API URL of issue itself.
    self_: String,
    /// API URL of issue notes.
    notes: String,
    /// API URL of issue award emoji.
    award_emoji: String,
    /// API URL of issue project.
    project: String,
}

/// An issue on a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Issue {
    /// The ID of the issue.
    pub id: IssueId,
    /// The user-visible ID of the issue.
    pub iid: IssueInternalId,
    /// The ID of the project.
    pub project_id: ProjectId,
    /// The title of the issue.
    pub title: String,
    /// The description of the issue.
    pub description: Option<String>,
    /// The state of the issue.
    pub state: IssueState,
    /// When the issue was created.
    pub created_at: DateTime<Utc>,
    /// When the issue was last updated.
    pub updated_at: DateTime<Utc>,
    /// When the issue was closed, if closed.
    pub closed_at: Option<DateTime<Utc>>,
    /// The user that closed the issue.
    pub closed_by: Option<UserBasic>,
    /// The labels attached to the issue.
    pub labels: Vec<String>,
    /// The milestone of the issue.
    pub milestone: Option<Milestone>,
    /// The author of the issue.
    pub author: UserBasic,
    /// The assignee of the issue.
    pub assignee: Option<UserBasic>,
    /// The assignees of the issue.
    pub assignees: Option<Vec<UserBasic>>,
    /// Whether the current user is subscribed or not.
    /// GitLab does not include this in responses with lists of issues but
    /// does on an individual issue.
    pub subscribed: Option<bool>,
    /// Time estimates.
    pub time_stats: IssuableTimeStats,
    /// The number of comments on the issue.
    pub user_notes_count: u64,
    /// The number of merge requests referencing the issue.
    pub merge_requests_count: u64,
    /// The number of upvotes for the issue.
    pub upvotes: u64,
    /// The number of downvotes against the issue.
    pub downvotes: u64,
    /// When the issue is due.
    pub due_date: Option<NaiveDate>,
    /// Whether the issue is has a non-empty task list.
    /// GitLab does not include this in issue references.
    pub has_tasks: Option<bool>,
    /// Whether the issue is confidential or not.
    pub confidential: bool,
    /// Whether the discussion has been locked.
    pub discussion_locked: Option<bool>,
    /// The URL of the issue.
    pub web_url: String,

    /// Links to related API URLs provided by GitLab in response to
    /// direct issue lookup.  We do not expose this because our
    /// clients do not need them.
    _links: Option<IssueLinks>,
}

impl Issue {
    /// Creates a new blank issue: it needs at least the ProjectId, title and author
    /// ProjectId and author are mandatory in the Issue struct itself
    /// title is mandatory for the new issue API of Gitlab
    pub fn new(project_id: ProjectId, title: String, author: UserBasic) -> Issue {
        // initialize with default parameters
        Issue {
            id: IssueId::new(0),
            iid: IssueInternalId::new(0),
            project_id,
            title,
            description: None,
            state: IssueState::Opened,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            closed_at: None,
            closed_by: None,
            labels: Vec::new(),
            milestone: None,
            author,
            assignee: None,
            assignees: None,
            subscribed: None,
            time_stats: IssuableTimeStats {
                time_estimate: 0,
                total_time_spent: 0,
                human_time_estimate: None,
                human_total_time_spent: None,
            },
            user_notes_count: 0,
            merge_requests_count: 0,
            upvotes: 0,
            downvotes: 0,
            due_date: None,
            has_tasks: None,
            confidential: false,
            discussion_locked: None,
            web_url: "".into(),
            _links: None,
        }
    }
    /// Complements the issue with optional parameter: iid
    pub fn with_iid(mut self, iid: IssueInternalId) -> Issue {
        self.iid = iid;
        self
    }
    /// Complements the issue with optional parameter: description
    pub fn with_description(mut self, description: String) -> Issue {
        self.description = Some(description);
        self
    }
    /// Complements the issue with optional parameter: confidential
    pub fn with_confidential(mut self, confidential: bool) -> Issue {
        self.confidential = confidential;
        self
    }
    /// Complements the issue with optional parameter: assignees
    pub fn with_assignees(mut self, assignees: Vec<UserBasic>) -> Issue {
        self.assignees = Some(assignees);
        self
    }
    /// Complements the issue with optional parameter: milestone
    pub fn with_milestone(mut self, milestone: Milestone) -> Issue {
        self.milestone = Some(milestone);
        self
    }
    /// Complements the issue with optional parameter: labels
    pub fn with_labels(mut self, labels: Vec<String>) -> Issue {
        self.labels = labels;
        self
    }
    /// Complements the issue with optional parameter: created_at
    pub fn with_created_at(mut self, created_at: DateTime<Utc>) -> Issue {
        self.created_at = created_at;
        self
    }
    /// Complements the issue with optional parameter: due_date
    pub fn with_due_date(mut self, due_date: NaiveDate) -> Issue {
        self.due_date = Some(due_date);
        self
    }
    pub fn has_links(&self) -> bool {
        self._links.is_some()
    }
}

/// A time estimate on an issue or merge request.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct IssuableTimeStats {
    /// The time estimate, in seconds.
    pub time_estimate: u64,
    /// The total time spent, in seconds.
    pub total_time_spent: u64,
    /// The time estimate, as a human-readable string.
    pub human_time_estimate: Option<String>,
    /// The total time spent, as a human-readable string.
    pub human_total_time_spent: Option<String>,
}

/// Type-safe external issue ID.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExternalIssueId(u64);

/// An external issue reference.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ExternalIssue {
    /// The ID of the issue.
    pub id: ExternalIssueId,
    /// The title of the issue.
    pub title: String,
}

/// A reference to an issue.
#[derive(Debug, Clone)]
pub enum IssueReference {
    /// A reference to an issue on the same Gitlab host.
    Internal(Box<Issue>),
    /// An external issue reference.
    External(ExternalIssue),
}

impl Serialize for IssueReference {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match *self {
            IssueReference::Internal(ref issue) => issue.serialize(serializer),
            IssueReference::External(ref issue) => issue.serialize(serializer),
        }
    }
}

impl<'de> Deserialize<'de> for IssueReference {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let val = <Value as Deserialize>::deserialize(deserializer)?;

        serde_json::from_value::<Issue>(val.clone())
            .map(|issue| IssueReference::Internal(Box::new(issue)))
            .or_else(|_| serde_json::from_value::<ExternalIssue>(val).map(IssueReference::External))
            .map_err(|err| D::Error::custom(format!("invalid issue reference: {:?}", err)))
    }
}

impl_id!(MergeRequestId, "Type-safe merge request ID.");

impl_id!(
    MergeRequestInternalId,
    "Type-safe merge request internal ID (internal to a project).",
);

/// The status of the possible merge for a merge request.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeStatus {
    /// The merge request has just been created.
    #[serde(rename = "preparing")]
    Preparing,
    /// The merge request has not been checked yet.
    #[serde(rename = "unchecked")]
    Unchecked,
    /// The merge request is currently being checked.
    #[serde(rename = "checking")]
    Checking,
    /// The merge request may be merged.
    #[serde(rename = "can_be_merged")]
    CanBeMerged,
    /// The merge request may not be merged yet.
    #[serde(rename = "cannot_be_merged")]
    CannotBeMerged,
    /// The merge request has not been checked but previously could not be merged.
    #[serde(rename = "cannot_be_merged_recheck")]
    CannotBeMergedRecheck,
    /// The merge request could not be merged previously, but is being rechecked.
    #[serde(rename = "cannot_be_merged_rechecking")]
    CannotBeMergedRechecking,
}

/// The states a merge request may be in.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeRequestState {
    /// The merge request is open.
    #[serde(rename = "opened")]
    Opened,
    /// The merge request has been closed before merging.
    #[serde(rename = "closed")]
    Closed,
    /// The merge request has been opened after closing.
    #[serde(rename = "reopened")]
    Reopened,
    /// The merge request has been merged.
    #[serde(rename = "merged")]
    Merged,
    /// The merge request is locked from further discussion or updates.
    #[serde(rename = "locked")]
    Locked,
}

/// Information about current user's access to the merge request.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MergeRequestUser {
    /// Whether the current user can merge the MR.
    pub can_merge: bool,
}

/// A merge request.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MergeRequestBasic {
    /// The ID of the merge request.
    pub id: MergeRequestId,
    /// The user-visible ID of the merge request.
    pub iid: MergeRequestInternalId,
    /// The ID of the project.
    pub project_id: ProjectId,
    /// The title of the merge request.
    pub title: String,
    /// The description of the merge request.
    pub description: Option<String>,
    /// The state of the merge request.
    pub state: MergeRequestState,
    /// When the merge request was created.
    pub created_at: DateTime<Utc>,
    /// When the merge request was last updated.
    pub updated_at: DateTime<Utc>,
    /// The URL of the merge request.
    pub web_url: String,
}

/// A merge request.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MergeRequest {
    /// The ID of the merge request.
    pub id: MergeRequestId,
    /// The user-visible ID of the merge request.
    pub iid: MergeRequestInternalId,
    /// The ID of the project.
    pub project_id: ProjectId,
    /// The title of the merge request.
    pub title: String,
    /// The description of the merge request.
    pub description: Option<String>,
    /// The state of the merge request.
    pub state: MergeRequestState,
    /// When the merge request was created.
    pub created_at: DateTime<Utc>,
    /// When the merge request was last updated.
    pub updated_at: DateTime<Utc>,
    /// When the merge request was merged.
    pub merged_at: Option<DateTime<Utc>>,
    /// When the merge request was closed.
    pub closed_at: Option<DateTime<Utc>>,
    /// The user that merged the merge request.
    pub merged_by: Option<UserBasic>,
    /// The user that closed the merge request.
    pub closed_by: Option<UserBasic>,
    /// The target branch of the merge request.
    pub target_branch: String,
    /// The source branch of the merge request.
    pub source_branch: String,
    /// The number of upvotes for the merge request.
    pub upvotes: u64,
    /// The number of downvotes against the merge request.
    pub downvotes: u64,
    /// The author of the merge request.
    pub author: UserBasic,
    /// The assignee of the merge request.
    pub assignee: Option<UserBasic>,
    /// The assignees of the merge request.
    pub assignees: Option<Vec<UserBasic>>,
    /// The reviewers of the merge request.
    pub reviewers: Option<Vec<UserBasic>>,
    /// The ID of the project hosting the source branch.
    pub source_project_id: Option<ProjectId>,
    /// The ID of the project hosting the target branch.
    pub target_project_id: ProjectId,
    /// The labels attached to the merge request.
    pub labels: Vec<String>,
    /// Whether the merge request is a work-in-progress or not.
    pub work_in_progress: bool,
    /// Whether the merge request allows a maintainer to collaborate.
    pub allow_collaboration: Option<bool>,
    /// Whether the merge request allows a maintainer to push (deprecated).
    pub allow_maintainer_to_push: Option<bool>,
    /// The milestone of the merge request.
    pub milestone: Option<Milestone>,
    /// Whether to squash commits on merge.
    pub squash: bool,
    /// Whether the merge request will be merged once all pipelines succeed or not.
    pub merge_when_pipeline_succeeds: bool,
    /// The status of the merge request.
    pub merge_status: MergeStatus,
    /// The object ID of the head of the source branch.
    ///
    /// This is `None` if the source branch has been deleted.
    pub sha: Option<ObjectId>,
    /// The commits used to construct the merge request diffs.
    pub diff_refs: Option<DiffRefs>,
    /// Description of error if MR failed to merge.
    pub merge_error: Option<String>,
    /// Whether a rebase is in progress.
    pub rebase_in_progress: Option<bool>,
    /// The object ID of the commit which merged the merge request.
    pub merge_commit_sha: Option<ObjectId>,
    /// The object ID of the merge request squash commit.
    pub squash_commit_sha: Option<ObjectId>,
    /// Whether the current user is subscribed or not.
    /// GitLab does not include this in responses with lists of merge requests but
    /// does on an individual merge request.
    pub subscribed: Option<bool>,
    /// Time estimates.
    pub time_stats: IssuableTimeStats,
    /// Whether or not all blocking discussions are resolved.
    pub blocking_discussions_resolved: bool,
    /// The number of paths changed by the merge request.
    ///
    /// This is an integer suffixed by `+` if there are more files changed than some threshold
    /// (probably determined by a timeout).
    pub changes_count: Option<String>,
    /// The number of comments on the merge request.
    pub user_notes_count: u64,
    /// Whether the discussion has been locked.
    pub discussion_locked: Option<bool>,
    /// Whether the merge request should be deleted or not (set by the merger).
    pub should_remove_source_branch: Option<bool>,
    /// Whether the merge request should be deleted or not (set by the author).
    pub force_remove_source_branch: Option<bool>,
    /// Whether the merge request currently has conflicts with the target branch.
    pub has_conflicts: bool,
    /// Information about current user's access to the merge request.
    pub user: Option<MergeRequestUser>,
    /// The URL of the merge request.
    pub web_url: String,
    /// Basic pipeline information for the MR.
    pub pipeline: Option<PipelineBasic>,
    /// Whether the user doesn't have any commits in the repo, denoting the first contribution.
    pub first_contribution: Option<bool>,
}

/// A merge request with changes.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MergeRequestChanges {
    /// The ID of the merge request.
    pub id: MergeRequestId,
    /// The user-visible ID of the merge request.
    pub iid: MergeRequestInternalId,
    /// The ID of the project.
    pub project_id: ProjectId,
    /// The title of the merge request.
    pub title: String,
    /// The description of the merge request.
    pub description: Option<String>,
    /// The state of the merge request.
    pub state: MergeRequestState,
    /// When the merge request was created.
    pub created_at: DateTime<Utc>,
    /// When the merge request was last updated.
    pub updated_at: DateTime<Utc>,
    /// When the merge request was merged.
    pub merged_at: Option<DateTime<Utc>>,
    /// When the merge request was closed.
    pub closed_at: Option<DateTime<Utc>>,
    /// The user that merged the merge request.
    pub merged_by: Option<UserBasic>,
    /// The user that closed the merge request.
    pub closed_by: Option<UserBasic>,
    /// The target branch of the merge request.
    pub target_branch: String,
    /// The source branch of the merge request.
    pub source_branch: String,
    /// The number of upvotes for the merge request.
    pub upvotes: u64,
    /// The number of downvotes against the merge request.
    pub downvotes: u64,
    /// The author of the merge request.
    pub author: UserBasic,
    /// The assignee of the merge request.
    pub assignee: Option<UserBasic>,
    /// The assignees of the merge request.
    pub assignees: Option<Vec<UserBasic>>,
    /// The reviewers of the merge request.
    pub reviewers: Option<Vec<UserBasic>>,
    /// The ID of the project hosting the source branch.
    pub source_project_id: Option<ProjectId>,
    /// The ID of the project hosting the target branch.
    pub target_project_id: ProjectId,
    /// The labels attached to the merge request.
    pub labels: Vec<String>,
    /// Whether the merge request is a work-in-progress or not.
    pub work_in_progress: bool,
    /// Whether the merge request allows a maintainer to collaborate.
    pub allow_collaboration: Option<bool>,
    /// Whether the merge request allows a maintainer to push (deprecated).
    pub allow_maintainer_to_push: Option<bool>,
    /// The milestone of the merge request.
    pub milestone: Option<Milestone>,
    /// Whether to squash commits on merge.
    pub squash: bool,
    /// Whether the merge request will be merged once all jobs succeed or not.
    pub merge_when_pipeline_succeeds: bool,
    /// The status of the merge request.
    pub merge_status: MergeStatus,
    /// The object ID of the head of the source branch.
    ///
    /// This is `None` if the source branch has been deleted.
    pub sha: Option<ObjectId>,
    /// The commits used to construct the merge request diffs.
    pub diff_refs: Option<DiffRefs>,
    /// Description of error if MR failed to merge.
    pub merge_error: Option<String>,
    /// Whether a rebase is in progress.
    pub rebase_in_progress: Option<bool>,
    /// The object ID of the commit which merged the merge request.
    pub merge_commit_sha: Option<ObjectId>,
    /// The object ID of the merge request squash commit.
    pub squash_commit_sha: Option<ObjectId>,
    /// GitLab does not include this in responses with lists of merge requests but
    /// does on an individual merge request.
    pub subscribed: Option<bool>,
    /// Time estimates.
    pub time_stats: IssuableTimeStats,
    /// Whether or not all blocking discussions are resolved.
    pub blocking_discussions_resolved: bool,
    /// The number of paths changed by the merge request.
    pub changes_count: Option<String>,
    /// The number of comments on the merge request.
    pub user_notes_count: u64,
    /// Whether the discussion has been locked.
    pub discussion_locked: Option<bool>,
    /// Whether the merge request should be deleted or not (set by the merger).
    pub should_remove_source_branch: Option<bool>,
    /// Whether the merge request should be deleted or not (set by the author).
    pub force_remove_source_branch: Option<bool>,
    /// Whether the merge request currently has conflicts with the target branch.
    pub has_conflicts: bool,
    /// Information about current user's access to the merge request.
    pub user: MergeRequestUser,
    /// The URL of the merge request.
    pub web_url: String,
    /// Basic pipeline information for the MR.
    pub pipeline: Option<PipelineBasic>,
    pub changes: Vec<RepoDiff>,
}

impl From<MergeRequestChanges> for MergeRequest {
    fn from(mr: MergeRequestChanges) -> Self {
        MergeRequest {
            id: mr.id,
            iid: mr.iid,
            project_id: mr.project_id,
            title: mr.title,
            description: mr.description,
            state: mr.state,
            created_at: mr.created_at,
            updated_at: mr.updated_at,
            merged_at: mr.merged_at,
            closed_at: mr.closed_at,
            merged_by: mr.merged_by,
            closed_by: mr.closed_by,
            target_branch: mr.target_branch,
            source_branch: mr.source_branch,
            upvotes: mr.upvotes,
            downvotes: mr.downvotes,
            author: mr.author,
            assignee: mr.assignee,
            assignees: mr.assignees,
            reviewers: mr.reviewers,
            source_project_id: mr.source_project_id,
            target_project_id: mr.target_project_id,
            labels: mr.labels,
            work_in_progress: mr.work_in_progress,
            allow_collaboration: mr.allow_collaboration,
            allow_maintainer_to_push: mr.allow_maintainer_to_push,
            milestone: mr.milestone,
            squash: mr.squash,
            merge_when_pipeline_succeeds: mr.merge_when_pipeline_succeeds,
            merge_status: mr.merge_status,
            sha: mr.sha,
            diff_refs: mr.diff_refs,
            merge_error: mr.merge_error,
            rebase_in_progress: mr.rebase_in_progress,
            merge_commit_sha: mr.merge_commit_sha,
            squash_commit_sha: mr.squash_commit_sha,
            subscribed: mr.subscribed,
            time_stats: mr.time_stats,
            blocking_discussions_resolved: mr.blocking_discussions_resolved,
            changes_count: mr.changes_count,
            user_notes_count: mr.user_notes_count,
            discussion_locked: mr.discussion_locked,
            should_remove_source_branch: mr.should_remove_source_branch,
            force_remove_source_branch: mr.force_remove_source_branch,
            has_conflicts: mr.has_conflicts,
            user: Some(mr.user),
            web_url: mr.web_url,
            pipeline: mr.pipeline,
            first_contribution: None,
        }
    }
}

impl_id!(MergeTrainId, "Type-safe merge train ID.");

/// The states a merge train may be in.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeTrainState {
    /// TODO: Figure out when this is used.
    #[serde(rename = "idle")]
    Idle,
    /// This merge train has been marked as stale.
    /// E.g. The MR is dropped from the merge train.
    #[serde(rename = "stale")]
    Stale,
    /// The merge train has just started.
    #[serde(rename = "fresh")]
    Fresh,
    /// The merge train finished and is in the process of merging.
    #[serde(rename = "merging")]
    Merging,
    /// The merge train succeded.
    #[serde(rename = "merged")]
    Merged,
}

/// A single MergeTrain entry
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MergeTrain {
    /// The ID of this merge train.
    pub id: MergeTrainId,
    /// The associated merge request.
    pub merge_request: MergeRequestBasic,
    /// The Pipeline for this merge.
    pub pipeline: PipelineBasic,
    /// The user that triggered this merge.
    pub user: UserBasic,
    /// When this entry has been added to the train.
    pub created_at: DateTime<Utc>,
    /// When this entry has been last updated.
    pub updated_at: DateTime<Utc>,
    /// The name of the target branch for this merge request.
    pub target_branch: String,
    /// The current status of this merge train.
    pub status: MergeTrainState,
    /// When this train has been merged.
    pub merged_at: Option<DateTime<Utc>>,
    pub duration: Option<u64>,
}

impl_id!(SshKeyId, "Type-safe SSH key ID.");

/// An uploaded SSH key.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SshKey {
    /// The ID of the SSH key.
    pub id: SshKeyId,
    /// The title of the key.
    pub title: String,
    /// The public half of the SSH key.
    pub key: String,
    /// When the key was created.
    pub created_at: DateTime<Utc>,
    /// Whether the key may push to repositories or not.
    pub can_push: bool,
}

/// An uploaded SSH key with its owner.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SshKeyWithUser {
    /// The ID of the SSH key.
    pub id: SshKeyId,
    /// The title of the key.
    pub title: String,
    /// The public half of the SSH key.
    pub key: String,
    /// When the key was created.
    pub created_at: DateTime<Utc>,
    /// The user associated with the SSH key.
    pub user: UserPublic,
}

/// The entities a note may be added to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoteType {
    /// A note on a commit.
    Commit,
    /// A note on an issue.
    Issue,
    /// A note on a merge request.
    MergeRequest,
    /// A note on a snippet.
    Snippet,
}

/// The various types a note can have
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscussionNoteType {
    /// A note in a discussion
    Note,
    /// A note in a standard discussion
    DiscussionNote,
    /// A note attached to a diff
    DiffNote,
}

/// The ID of an entity a note is attached to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NoteableId {
    /// The ID of the commit for a commit note.
    Commit(ObjectId),
    /// The ID of the issue for an issue note.
    Issue(IssueId),
    /// The ID of the merge request for a merge request note.
    MergeRequest(MergeRequestId),
    /// The ID of the snippet for a snippet note.
    Snippet(SnippetId),
}

/// The internal ID of an entity a note is attached to (internal to a project).
/// GitLab only has this for notes attached to issues and merge requests.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NoteableInternalId {
    /// The internal ID of the issue for an issue note.
    Issue(IssueInternalId),
    /// The internal ID of the merge request for a merge request note.
    MergeRequest(MergeRequestInternalId),
}

impl_id!(NoteId, "Type-safe note (comment) ID.");

/// A note can be attached to text or an image
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum NotePositionType {
    #[serde(rename = "text")]
    Text,
    #[serde(rename = "image")]
    Image,
}

/// When a note is against a diff, the position of the note
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NotePosition {
    /// Base commit in the source branch
    pub base_sha: ObjectId,
    /// SHA referencing the commit in the target branch
    pub start_sha: ObjectId,
    /// The HEAD of the merge request
    pub head_sha: ObjectId,
    /// Whether this note is against text or image
    /// FIXME: image not supported yet.
    pub position_type: NotePositionType,
    /// File path before change
    pub old_path: String,
    /// File path after change
    pub new_path: String,
    /// Line number before the change
    pub old_line: Option<u64>,
    /// Line number after the change
    pub new_line: Option<u64>,
}

/// A comment on an entity.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Note {
    /// The ID of the note.
    pub id: NoteId,
    /// The type of the note.
    #[serde(rename = "type")]
    pub note_type: Option<DiscussionNoteType>,
    /// The content of the note.
    pub body: String,
    /// The URL of an attachment to the note.
    pub attachment: Option<String>,
    /// The author of the note.
    pub author: UserBasic,
    /// When the note was created.
    pub created_at: DateTime<Utc>,
    /// When the note was last updated.
    pub updated_at: DateTime<Utc>,
    /// Whether the note can be resolved.
    pub resolvable: bool,
    /// Whether the note has been resolved.
    pub resolved: Option<bool>,
    /// The user that resolved the note.
    pub resolved_by: Option<UserBasic>,
    /// Whether the note was created by a user or in response to an external action.
    ///
    /// System notes include indications that the commit, issue, etc. was referenced elsewhere, a
    /// milestone, assignee, or label change, status chages, and so on.
    pub system: bool,
    // Keep as JSON because its type depends on what `noteable_type` is.
    noteable_id: Value,
    // Keep as JSON because its type depends on what `noteable_type` is.
    noteable_iid: Option<Value>,
    /// The type of entity the note is attached to.
    pub noteable_type: NoteType,
    /// If applicable, the diff data to which the note is attached
    pub position: Option<NotePosition>,
}

impl Note {
    /// The ID of the entity the note is attached to.
    pub fn noteable_id(&self) -> Option<NoteableId> {
        match self.noteable_type {
            NoteType::Commit => self
                .noteable_id
                .as_str()
                .map(|id| NoteableId::Commit(ObjectId::new(id))),
            NoteType::Issue => self
                .noteable_id
                .as_u64()
                .map(|id| NoteableId::Issue(IssueId::new(id))),
            NoteType::MergeRequest => self
                .noteable_id
                .as_u64()
                .map(|id| NoteableId::MergeRequest(MergeRequestId::new(id))),
            NoteType::Snippet => self
                .noteable_id
                .as_u64()
                .map(|id| NoteableId::Snippet(SnippetId::new(id))),
        }
    }

    /// The internal ID of the entity the note is attached to (internal to a project).
    /// This is available only for notes attached to issues and merge requests.
    pub fn noteable_iid(&self) -> Option<NoteableInternalId> {
        match self.noteable_type {
            NoteType::Commit => None,
            NoteType::Issue => self
                .noteable_iid
                .as_ref()
                .and_then(|value| value.as_u64())
                .map(|id| NoteableInternalId::Issue(IssueInternalId::new(id))),
            NoteType::MergeRequest => self
                .noteable_iid
                .as_ref()
                .and_then(|value| value.as_u64())
                .map(|id| NoteableInternalId::MergeRequest(MergeRequestInternalId::new(id))),
            NoteType::Snippet => None,
        }
    }
}

/// A threaded discussion
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Discussion {
    /// The discussion ID, a SHA hash
    pub id: ObjectId,
    /// True if the discussion only holds one note.
    pub individual_note: bool,
    /// The discussion notes
    pub notes: Vec<Note>,
}

impl_id!(AwardId, "Type-safe award ID.");

/// An ID of an entity which may receive an award.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AwardableId {
    /// The ID of an awarded issue.
    Issue(IssueId),
    /// The ID of an awarded merge request.
    MergeRequest(MergeRequestId),
    /// The ID of an awarded snippet.
    Snippet(SnippetId),
    /// The ID of an awarded note.
    Note(NoteId),
}

/// The entities which may be awarded.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum AwardableType {
    /// An award on an issue.
    Issue,
    /// An award on a merge request.
    MergeRequest,
    /// An award on a snippet.
    Snippet,
    /// An award on a note.
    Note,
}

/// An awarded emoji on an entity.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AwardEmoji {
    /// The ID of the award.
    pub id: AwardId,
    /// The name of the awarded emoji.
    pub name: String,
    /// The user which created the award.
    pub user: UserBasic,
    /// When the award was created.
    pub created_at: DateTime<Utc>,
    /// When the award was last updated.
    pub updated_at: DateTime<Utc>,
    awardable_id: u64,
    /// The type of entity that is awarded.
    pub awardable_type: AwardableType,
}

impl AwardEmoji {
    /// The ID of the entity the award is attached to.
    pub fn awardable_id(&self) -> AwardableId {
        match self.awardable_type {
            AwardableType::Issue => AwardableId::Issue(IssueId::new(self.awardable_id)),
            AwardableType::MergeRequest => {
                AwardableId::MergeRequest(MergeRequestId::new(self.awardable_id))
            }
            AwardableType::Snippet => AwardableId::Snippet(SnippetId::new(self.awardable_id)),
            AwardableType::Note => AwardableId::Note(NoteId::new(self.awardable_id)),
        }
    }
}

/// The type of line commented on.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineType {
    /// An added line was commented on.
    #[serde(rename = "new")]
    New,
    /// An deleted line was commented on.
    #[serde(rename = "old")]
    Old,
}

/// A note on a commit diff.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CommitNote {
    /// The content of the note.
    pub note: String,
    /// The path of the file commented on.
    pub path: Option<String>,
    /// The line of the file commented on.
    pub line: Option<u64>,
    /// The type of the line commented on.
    pub line_type: Option<LineType>,
    /// The author of the note.
    pub author: UserBasic,
    /// When the note was created.
    pub created_at: DateTime<Utc>,
}

impl_id!(CommitStatusId, "Type-safe commit status ID.");

/// States for commit statuses.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusState {
    /// The check was created.
    #[serde(rename = "created")]
    Created,
    /// The check is waiting for some other resource.
    #[serde(rename = "waiting_for_resource")]
    WaitingForResource,
    /// The check is currently being prepared.
    #[serde(rename = "preparing")]
    Preparing,
    /// The check is queued.
    #[serde(rename = "pending")]
    Pending,
    /// The check is currently running.
    #[serde(rename = "running")]
    Running,
    /// The check succeeded.
    #[serde(rename = "success")]
    Success,
    /// The check failed.
    #[serde(rename = "failed")]
    Failed,
    /// The check was canceled.
    #[serde(rename = "canceled")]
    Canceled,
    /// The check was skipped.
    #[serde(rename = "skipped")]
    Skipped,
    /// The check is waiting for manual action.
    #[serde(rename = "manual")]
    Manual,
    /// The check is scheduled to run at some point in time.
    #[serde(rename = "scheduled")]
    Scheduled,
}

/// A status of a commit.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CommitStatus {
    /// The ID of the commit status.
    pub id: CommitStatusId,
    /// The object ID of the commit this status is for.
    pub sha: ObjectId,
    #[serde(rename = "ref")]
    /// The name of the reference the status was created for.
    pub ref_: Option<String>,
    /// The state of the commit status.
    pub status: StatusState,
    /// The name of the commit status.
    pub name: String,
    /// The URL associated with the commit status.
    pub target_url: Option<String>,
    /// The description of the commit status.
    pub description: Option<String>,
    /// When the commit status was created.
    pub created_at: DateTime<Utc>,
    /// When the commit status started.
    pub started_at: Option<DateTime<Utc>>,
    /// When the commit status completed.
    pub finished_at: Option<DateTime<Utc>>,
    /// Whether the commit status is allowed to fail.
    pub allow_failure: bool,
    pub coverage: Option<f64>,
    /// The author of the commit status.
    pub author: UserBasic,
}

impl_id!(EnvironmentId, "Type-safe environment ID.");

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Environment {
    pub id: EnvironmentId,
    pub name: String,
    pub slug: String,
    pub external_url: Option<String>,
    pub state: Option<String>,
    pub last_deployment: Option<Deployment>,
}

impl_id!(DeploymentId, "Type-safe deployment ID.");

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Deployment {
    pub id: DeploymentId,
    pub iid: u64,
    pub r#ref: String,
    pub sha: String,
    pub created_at: String,
    pub status: Option<String>,
    pub user: UserBasic,
    pub deployable: Deployable,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Deployable {
    pub commit: Commit,
    pub status: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Commit {
    pub id: Option<String>,
    pub short_id: Option<String>,
    pub created_at: Option<String>,
    pub title: Option<String>,
}

/// The target of an event.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventTargetType {
    /// An event targeted a commit.
    #[serde(rename = "commit")]
    Commit,
    /// An event targeted an issue.
    #[serde(rename = "issue")]
    Issue,
    /// An event targeted a merge request.
    #[serde(rename = "merge_request")]
    MergeRequest,
    /// An event targeted a snippet.
    #[serde(rename = "snippet")]
    Snippet,
    /// An event targeted a project snippet.
    #[serde(rename = "project_snippet")]
    ProjectSnippet,
}

/// The ID of an event target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventTargetId {
    /// The object ID of a commit event target.
    Commit(ObjectId),
    /// The ID of an issue event target.
    Issue(IssueId),
    /// The ID of a merge request event target.
    MergeRequest(MergeRequestId),
    /// The ID of a snippet event target.
    Snippet(SnippetId),
}

/// An event on a project.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Event {
    /// The title of the event.
    pub title: Option<String>,
    /// The ID of the project.
    pub project_id: ProjectId,
    /// The action which triggered the event.
    // FIXME: This should be an enumeration.
    pub action_name: String,
    target_id: Value,
    /// The type of the event target.
    pub target_type: EventTargetType,
    /// The ID of the author of the event.
    pub author_id: UserId,
    pub data: Option<Value>,
    /// The title of the target.
    pub target_title: String,
    /// When the event was created.
    pub created_at: DateTime<Utc>,
    pub note: Option<Note>,
    /// The author of the event.
    pub author: Option<UserBasic>,
    /// The handle of the author.
    pub author_username: Option<String>,
}

impl Event {
    /// The ID of an event's target.
    pub fn target_id(&self) -> Option<EventTargetId> {
        match self.target_type {
            EventTargetType::Commit => self
                .target_id
                .as_str()
                .map(|id| EventTargetId::Commit(ObjectId(id.into()))),
            EventTargetType::Issue => self
                .target_id
                .as_u64()
                .map(|id| EventTargetId::Issue(IssueId(id))),
            EventTargetType::MergeRequest => self
                .target_id
                .as_u64()
                .map(|id| EventTargetId::MergeRequest(MergeRequestId(id))),
            EventTargetType::Snippet => self
                .target_id
                .as_u64()
                .map(|id| EventTargetId::Snippet(SnippetId(id))),
            EventTargetType::ProjectSnippet => self
                .target_id
                .as_u64()
                .map(|id| EventTargetId::Snippet(SnippetId(id))),
        }
    }
}

/// The kinds of namespaces supported by Gitlab.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum NamespaceKind {
    /// A user namespace.
    #[serde(rename = "user")]
    User,
    /// A group namespace.
    #[serde(rename = "group")]
    Group,
}

/// The ID of a namespace.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NamespaceId {
    /// A user namespace ID.
    User(UserId),
    /// A group namespace ID.
    Group(GroupId),
}

/// An entity which can own projects.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Namespace {
    id: u64,
    /// The URL of the namespace.
    pub path: String,
    /// The name of the namespace.
    pub name: String,
    /// The kind of the namespace.
    pub kind: NamespaceKind,
    pub full_path: String,
    /// Number of members in the namespace and its descendants.
    ///
    /// Only available when talking to GitLab as a user that can admin the namespace.
    pub members_count_with_descendants: Option<u64>,
    /// The URL of the user's avatar if namespace is a user.
    pub avatar_url: Option<String>,
    /// The URL to the namespace page (user or group).
    pub web_url: String,
}

impl Namespace {
    /// The ID of the namespace.
    pub fn id(&self) -> NamespaceId {
        match self.kind {
            NamespaceKind::User => NamespaceId::User(UserId(self.id)),
            NamespaceKind::Group => NamespaceId::Group(GroupId(self.id)),
        }
    }
}

impl_id!(RunnerId, "Type-safe runner ID.");

/// A Gitlab CI runner.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Runner {
    /// The ID of the runner.
    pub id: RunnerId,
    /// The description of the runner.
    pub description: Option<String>,
    /// Whether the runner is active or not.
    pub active: bool,
    /// Whether the runner is shared or not.
    pub is_shared: bool,
    /// The name of the runner.
    pub name: Option<String>,
}

/// An uploaded artifact from a job.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JobArtifactFile {
    /// The name of the artifact.
    pub filename: String,
    /// The size (in bytes) of the artifact.
    pub size: u64,
}

/// An uploaded artifact from a job.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JobArtifact {
    pub file_type: String,
    pub file_format: Option<String>,
    /// The name of the artifact.
    pub filename: String,
    /// The size (in bytes) of the artifact.
    pub size: u64,
}

impl_id!(JobId, "Type-safe job ID.");

/// Information about a job in Gitlab CI.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Job {
    /// The ID of the job.
    pub id: JobId,
    /// The status of the job.
    pub status: StatusState,
    pub stage: String,
    /// The name of the job.
    pub name: String,
    #[serde(rename = "ref")]
    /// The name of the reference that was tested.
    pub ref_: Option<String>,
    pub tag: bool,
    pub coverage: Option<f64>,
    /// When the job was created or marked as pending.
    pub created_at: DateTime<Utc>,
    /// When the job was started.
    pub started_at: Option<DateTime<Utc>>,
    /// When the job completed.
    pub finished_at: Option<DateTime<Utc>>,
    /// The user which ran the job.
    pub user: Option<User>,
    /// The artifact file uploaded from the job.
    pub artifacts_file: Option<JobArtifactFile>,
    /// The commit the job tested.
    pub commit: RepoCommit,
    /// The runner which ran the job.
    pub runner: Option<Runner>,
    /// The pipeline the job belongs to.
    pub pipeline: PipelineBasic,
    pub allow_failure: bool,
    pub duration: Option<f64>,
    pub artifacts: Vec<JobArtifact>,
    pub artifacts_expire_at: Option<DateTime<Utc>>,
    pub web_url: String,
}

impl_id!(PipelineId, "Type-safe pipeline ID.");

/// Information about a pipeline in Gitlab CI.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PipelineBasic {
    /// The ID of the pipeline.
    pub id: PipelineId,
    /// The ID of the project holding the pipeline.
    pub project_id: ProjectId,
    #[serde(rename = "ref")]
    /// The name of the reference that was tested.
    pub ref_: Option<String>,
    /// The object ID that was tested.
    pub sha: ObjectId,
    /// The status of the pipeline.
    pub status: StatusState,
    /// When the pipeline was created.
    pub created_at: Option<DateTime<Utc>>,
    /// When the pipeline was last updated.
    pub updated_at: Option<DateTime<Utc>>,
    /// The URL to the pipeline page.
    pub web_url: String,
}

/// More information about a pipeline in Gitlab CI.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Pipeline {
    /// The ID of the pipeline.
    pub id: PipelineId,
    /// The ID of the project holding the pipeline.
    pub project_id: ProjectId,
    /// The object ID that was tested.
    pub sha: ObjectId,
    #[serde(rename = "ref")]
    /// The name of the reference that was tested.
    pub ref_: Option<String>,
    /// The status of the pipeline.
    pub status: StatusState,
    /// The URL to the pipeline page.
    pub web_url: String,
    /// FIXME What are the semantics of this field?
    pub before_sha: Option<ObjectId>,
    /// Was this pipeline triggered by a tag.
    pub tag: bool,
    /// Error returned by the parser of `gitlab-ci.yml`, if any.
    pub yaml_errors: Option<String>,
    /// When the pipeline was created.
    pub created_at: Option<DateTime<Utc>>,
    /// When the pipeline was last updated.
    pub updated_at: Option<DateTime<Utc>>,
    /// When the pipeline began running.
    pub started_at: Option<DateTime<Utc>>,
    /// When the pipeline completed.
    pub finished_at: Option<DateTime<Utc>>,
    /// FIXME What are the semantics of this field?
    pub committed_at: Option<DateTime<Utc>>,
    /// Duration of pipeline in seconds.
    pub duration: Option<u64>,
    /// FIXME What are the semantics of this field?
    pub coverage: Option<String>,
    /// The user who triggered this pipeline.
    pub user: UserBasic,
    /// FIXME: What are the semantics of this field?
    /// See <https://gitlab.com/gitlab-org/gitlab-foss/blob/master/app/serializers/detailed_status_entity.rb>.
    pub detailed_status: Value,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PipelineVariableType {
    #[serde(rename = "env_var")]
    EnvVar,
    #[serde(rename = "file")]
    File,
}

impl Default for PipelineVariableType {
    fn default() -> Self {
        PipelineVariableType::EnvVar
    }
}

/// A pipeline variable.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PipelineVariable {
    /// Name of the variable.
    pub key: String,
    /// Value of the variable.
    pub value: String,

    /// Type of the variable (eg. `env_var`).
    #[serde(default)]
    pub variable_type: PipelineVariableType,
}

impl_id!(LabelEventId, "Type-safe label event ID.");

/// A resource label event
///
/// Note that resource events were added in Gitlab 11.2.  Any labels added or
/// removed before then will not be returned by the API.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResourceLabelEvent {
    /// The ID for the label event
    pub id: LabelEventId,
    pub user: UserBasic,
    pub created_at: DateTime<Utc>,
    /// The merge request id, or issue id (depending on the value of resource_type)
    resource_id: u64,
    /// Either "MergeRequest" or "Issue"
    resource_type: String,
    /// The label may be None if the label has been deleted.
    pub label: Option<EventLabel>,
    pub action: String,
}

impl ResourceLabelEvent {
    /// Returns the id of the merge request or issue that this event is from
    pub fn event_target(&self) -> Option<ResourceLabelEventTarget> {
        match self.resource_type.as_ref() {
            "MergeRequest" => Some(ResourceLabelEventTarget::MergeRequest(MergeRequestId::new(
                self.resource_id,
            ))),
            "Issue" => Some(ResourceLabelEventTarget::Issue(IssueId::new(
                self.resource_id,
            ))),
            _ => None,
        }
    }
}

/// The type of object that on which the resource label event was created
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResourceLabelEventTarget {
    /// The ID of an issue event target.
    Issue(IssueId),
    /// The ID of a merge request event target.
    MergeRequest(MergeRequestId),
}

/// An label on a project.
///
/// This is like [Label], except that it doesn't have all the same fields
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EventLabel {
    /// The Id of the label.
    pub id: LabelId,
    /// The name of the label.
    pub name: String,
    /// The color of the label.
    pub color: LabelColor,
    /// The description of the label.
    pub description: Option<String>,
}