use super::{ExternalObjectKind, ExternalProvider};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum NamespaceRule {
Required,
Forbidden,
Optional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum IdForm {
Number,
Key,
Raw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum OtherKinds {
Rejected,
Label(&'static str),
OwnSpace,
}
pub(super) struct KindRow {
pub(super) name: &'static str,
pub(super) synonyms: &'static [&'static str],
pub(super) space: &'static str,
}
pub(super) struct ProviderRules {
pub(super) family: fn() -> ExternalProvider,
pub(super) namespace: NamespaceRule,
pub(super) folds_namespace_case: bool,
pub(super) strips_git_suffix: bool,
pub(super) ids: IdForm,
pub(super) kinds: &'static [KindRow],
pub(super) other_kinds: OtherKinds,
pub(super) reviews: bool,
pub(super) www_host: Option<&'static str>,
}
const ISSUE: &str = "issue";
const PULL_REQUEST: &str = "pull_request";
const REVIEW_SYNONYMS: &[&str] = &["pull", "pr", "mergerequest", "mr"];
const fn row(
name: &'static str,
synonyms: &'static [&'static str],
space: &'static str,
) -> KindRow {
KindRow {
name,
synonyms,
space,
}
}
const GITHUB_KINDS: &[KindRow] = &[
row(ISSUE, &[], "issue"),
row(PULL_REQUEST, REVIEW_SYNONYMS, "issue"),
row("discussion", &[], "issue"),
];
const FORGEJO_KINDS: &[KindRow] = &[
row(ISSUE, &[], "issue"),
row(PULL_REQUEST, REVIEW_SYNONYMS, "issue"),
];
const GITLAB_KINDS: &[KindRow] = &[
row(ISSUE, &[], "issue"),
row("incident", &[], "issue"),
row("task", &[], "issue"),
row("test_case", &[], "issue"),
row("ticket", &[], "issue"),
row("objective", &[], "issue"),
row("key_result", &[], "issue"),
row("work_item", &[], "issue"),
row(PULL_REQUEST, REVIEW_SYNONYMS, "merge_request"),
row("epic", &[], "epic"),
];
const KEYED_KINDS: &[KindRow] = &[row(ISSUE, &[], "key")];
const OTHER_KINDS: &[KindRow] = &[
row(ISSUE, &[], ISSUE),
row(PULL_REQUEST, REVIEW_SYNONYMS, PULL_REQUEST),
];
const fn forge(
family: fn() -> ExternalProvider,
kinds: &'static [KindRow],
www_host: Option<&'static str>,
) -> ProviderRules {
ProviderRules {
family,
namespace: NamespaceRule::Required,
folds_namespace_case: true,
strips_git_suffix: true,
ids: IdForm::Number,
kinds,
other_kinds: OtherKinds::Rejected,
reviews: true,
www_host,
}
}
const GITHUB: ProviderRules = forge(
|| ExternalProvider::GitHub,
GITHUB_KINDS,
Some("github.com"),
);
const GITLAB: ProviderRules = forge(|| ExternalProvider::GitLab, GITLAB_KINDS, None);
const FORGEJO: ProviderRules = forge(|| ExternalProvider::Forgejo, FORGEJO_KINDS, None);
const JIRA: ProviderRules = ProviderRules {
family: || ExternalProvider::Jira,
namespace: NamespaceRule::Forbidden,
folds_namespace_case: false,
strips_git_suffix: false,
ids: IdForm::Key,
kinds: KEYED_KINDS,
other_kinds: OtherKinds::Label("key"),
reviews: false,
www_host: None,
};
const LINEAR: ProviderRules = ProviderRules {
family: || ExternalProvider::Linear,
namespace: NamespaceRule::Required,
folds_namespace_case: true,
..JIRA
};
const OTHER: ProviderRules = ProviderRules {
family: || ExternalProvider::Other(String::new()),
namespace: NamespaceRule::Optional,
folds_namespace_case: false,
strips_git_suffix: false,
ids: IdForm::Raw,
kinds: OTHER_KINDS,
other_kinds: OtherKinds::OwnSpace,
reviews: true,
www_host: None,
};
impl ExternalProvider {
pub(super) fn rules(&self) -> &'static ProviderRules {
match self {
Self::GitHub => &GITHUB,
Self::GitLab => &GITLAB,
Self::Forgejo | Self::Gitea => &FORGEJO,
Self::Jira => &JIRA,
Self::Linear => &LINEAR,
Self::Other(_) => &OTHER,
}
}
pub(super) fn family(&self) -> ExternalProvider {
match self {
Self::Other(_) => self.clone(),
known => (known.rules().family)(),
}
}
}
pub(super) fn fold_kind(name: &str) -> String {
let folded: String = name
.chars()
.flat_map(char::to_lowercase)
.filter(|c| c.is_alphanumeric())
.collect();
if let Some(stem) = folded.strip_suffix("ies").filter(|stem| !stem.is_empty()) {
format!("{stem}y")
} else if let Some(stem) = folded
.strip_suffix('s')
.filter(|stem| !stem.is_empty() && !stem.ends_with('s'))
{
stem.to_owned()
} else {
folded
}
}
pub(super) fn neutral_kind(name: &str) -> ExternalObjectKind {
OTHER.canonical_kind(&ExternalObjectKind::Other(name.to_owned()))
}
fn kind_name(kind: &ExternalObjectKind) -> &str {
match kind {
ExternalObjectKind::Issue => ISSUE,
ExternalObjectKind::PullRequest => PULL_REQUEST,
ExternalObjectKind::Other(name) => name,
}
}
pub(super) fn kind_named(name: &str) -> ExternalObjectKind {
match name {
ISSUE => ExternalObjectKind::Issue,
PULL_REQUEST => ExternalObjectKind::PullRequest,
other => ExternalObjectKind::Other(other.to_owned()),
}
}
impl ProviderRules {
fn row(&self, kind: &ExternalObjectKind) -> Option<&'static KindRow> {
let folded = fold_kind(kind_name(kind));
self.kinds
.iter()
.find(|row| fold_kind(row.name) == folded || row.synonyms.contains(&folded.as_str()))
}
pub(super) fn canonical_kind(&self, kind: &ExternalObjectKind) -> ExternalObjectKind {
match self.row(kind).or_else(|| OTHER.row(kind)) {
Some(row) => kind_named(row.name),
None => kind_named(&fold_kind(kind_name(kind))),
}
}
pub(super) fn space(&self, kind: &ExternalObjectKind) -> Option<String> {
if *kind == ExternalObjectKind::PullRequest && !self.reviews {
return None;
}
match (self.row(kind), self.other_kinds) {
(Some(row), _) => Some(row.space.to_owned()),
(None, OtherKinds::Label(space)) => Some(space.to_owned()),
(None, OtherKinds::OwnSpace) => Some(kind_name(kind).to_owned()),
(None, OtherKinds::Rejected) => None,
}
}
pub(super) fn accepted_kinds(&self) -> String {
let names: Vec<_> = self.kinds.iter().map(|row| row.name).collect();
match self.other_kinds {
OtherKinds::Rejected => names.join(", "),
_ => format!("{} or any other kind name", names.join(", ")),
}
}
}
#[cfg(test)]
mod tests;