///|
pub fn analyze_namespace(raw : String, repository : String) -> NamespaceInfo {
let name = trim_ascii(raw)
let parts = namespace_split_char(name, 47)
let owner = if parts.length() == 2 { parts[0] } else { "" }
let package_name = if parts.length() == 2 { parts[1] } else { "" }
let valid = parts.length() == 2 &&
namespace_valid_segment(owner) &&
namespace_valid_segment(package_name)
let repo_parts = github_owner_repo(repository)
let repo_owner = if repo_parts.length() == 2 { repo_parts[0] } else { "" }
let repo_name = if repo_parts.length() == 2 { repo_parts[1] } else { "" }
let owner_matches = repo_owner.length() > 0 &&
ascii_lower(owner) == ascii_lower(repo_owner)
let package_matches = repo_name.length() > 0 &&
ascii_lower(package_name) == ascii_lower(repo_name)
namespace_info(
name, owner, package_name, valid, owner_matches, package_matches,
)
}
///|
pub fn NamespaceInfo::canonical_name(self : NamespaceInfo) -> String {
if self.valid {
self.owner + "/" + self.package_name
} else {
self.raw
}
}
///|
pub fn NamespaceInfo::mooncakes_url(self : NamespaceInfo) -> String {
if self.valid {
"https://mooncakes.io/docs/" + self.owner + "/" + self.package_name
} else {
"https://mooncakes.io/"
}
}
///|
pub fn NamespaceInfo::github_url(self : NamespaceInfo) -> String {
if self.valid {
"https://github.com/" + self.owner + "/" + self.package_name
} else {
""
}
}
///|
pub fn NamespaceInfo::repository_git_url(self : NamespaceInfo) -> String {
if self.valid {
self.github_url() + ".git"
} else {
""
}
}
///|
pub fn NamespaceInfo::is_publishable(self : NamespaceInfo) -> Bool {
self.valid && self.owner_matches_github && self.package_matches_repo
}
///|
pub fn NamespaceInfo::risk_level(self : NamespaceInfo) -> String {
if !self.valid {
"blocking"
} else if !self.owner_matches_github {
"review"
} else if !self.package_matches_repo {
"notice"
} else {
"ok"
}
}
///|
pub fn NamespaceInfo::summary(self : NamespaceInfo) -> String {
"namespace=\{self.canonical_name()}, valid=\{bool_text(self.valid)}, owner-match=\{bool_text(self.owner_matches_github)}, repo-match=\{bool_text(self.package_matches_repo)}"
}
///|
pub fn NamespaceInfo::recommendation(self : NamespaceInfo) -> String {
if !self.valid {
"Use the Mooncakes namespace format owner/package in moon.mod."
} else if !self.owner_matches_github {
"Confirm the Mooncakes owner is controlled by the same GitHub account or organization."
} else if !self.package_matches_repo {
"Package and repository names differ; document the reason or align them before release."
} else {
"Namespace and GitHub repository are aligned."
}
}
///|
pub fn NamespaceInfo::to_markdown(self : NamespaceInfo) -> String {
let mut out = "## Namespace Analysis\n\n"
out = out + "- Raw name: `" + self.raw + "`\n"
out = out + "- Owner: `" + self.owner + "`\n"
out = out + "- Package: `" + self.package_name + "`\n"
out = out + "- Valid: " + bool_text(self.valid) + "\n"
out = out +
"- Owner matches GitHub: " +
bool_text(self.owner_matches_github) +
"\n"
out = out +
"- Repository name matches package: " +
bool_text(self.package_matches_repo) +
"\n"
out = out + "- Mooncakes URL: " + self.mooncakes_url() + "\n"
out = out + "- Risk: " + self.risk_level() + "\n"
out = out + "- Recommendation: " + self.recommendation() + "\n"
out
}
///|
pub fn github_owner_repo(repository : String) -> Array[String] {
let value = namespace_clean_repository(repository)
if starts_with(value, "https://github.com/") {
return namespace_owner_repo_tail(slice(value, 19, value.length()))
}
if starts_with(value, "http://github.com/") {
return namespace_owner_repo_tail(slice(value, 18, value.length()))
}
if starts_with(value, "git@github.com:") {
return namespace_owner_repo_tail(slice(value, 15, value.length()))
}
[]
}
///|
pub fn namespace_alignment_table(items : Array[NamespaceInfo]) -> String {
let mut out = "| Package | Valid | Owner match | Repo match | Risk |\n| --- | --- | --- | --- | --- |\n"
let mut index = 0
while index < items.length() {
let item = items[index]
out = out + "| `" + item.canonical_name() + "`"
out = out + " | " + bool_text(item.valid)
out = out + " | " + bool_text(item.owner_matches_github)
out = out + " | " + bool_text(item.package_matches_repo)
out = out + " | " + item.risk_level() + " |\n"
index += 1
}
out
}
///|
pub fn namespace_from_manifest(manifest : ManifestInfo) -> NamespaceInfo {
let package_name = match manifest.get("name") {
None => ""
Some(value) => value
}
let repository = match manifest.get("repository") {
None => ""
Some(value) => value
}
analyze_namespace(package_name, repository)
}
///|
pub fn mooncakes_package_page(package_name : String) -> String {
let info = analyze_namespace(package_name, "")
info.mooncakes_url()
}
///|
pub fn package_name_from_repository(repository : String) -> String {
let parts = github_owner_repo(repository)
if parts.length() == 2 {
parts[0] + "/" + parts[1]
} else {
""
}
}
///|
pub fn suggested_package_name(owner : String, repo : String) -> String {
namespace_slug(owner) + "/" + namespace_slug(repo)
}
///|
pub fn namespace_has_placeholder(name : String) -> Bool {
has_placeholder(name) ||
contains_word(name, "owner") ||
contains_word(name, "username") ||
contains_word(name, "example")
}
///|
pub fn namespace_acceptance_issue(info : NamespaceInfo) -> String {
if namespace_has_placeholder(info.raw) {
"package namespace still looks like a placeholder"
} else if !info.valid {
"package namespace is malformed"
} else if !info.owner_matches_github {
"Mooncakes owner differs from GitHub owner"
} else if !info.package_matches_repo {
"Mooncakes package name differs from repository name"
} else {
""
}
}
///|
fn namespace_clean_repository(repository : String) -> String {
let trimmed = trim_ascii(repository)
if ends_with(trimmed, ".git") {
slice(trimmed, 0, trimmed.length() - 4)
} else if ends_with(trimmed, "/") {
slice(trimmed, 0, trimmed.length() - 1)
} else {
trimmed
}
}
///|
fn namespace_owner_repo_tail(tail : String) -> Array[String] {
let parts = namespace_split_char(tail, 47)
if parts.length() >= 2 {
[parts[0], namespace_strip_git_suffix(parts[1])]
} else {
[]
}
}
///|
fn namespace_strip_git_suffix(value : String) -> String {
if ends_with(value, ".git") {
slice(value, 0, value.length() - 4)
} else {
value
}
}
///|
fn namespace_split_char(value : String, code : Int) -> Array[String] {
let parts : Array[String] = []
let mut start = 0
let mut index = 0
while index < value.length() {
if code_at(value, index) == code {
parts.push(slice(value, start, index))
start = index + 1
}
index += 1
}
parts.push(slice(value, start, value.length()))
parts
}
///|
fn namespace_valid_segment(value : String) -> Bool {
if value.length() == 0 {
return false
}
if code_at(value, 0) == 45 || code_at(value, value.length() - 1) == 45 {
return false
}
let mut index = 0
while index < value.length() {
let code = code_at(value, index)
if !is_ident_char(code) {
return false
}
index += 1
}
true
}
///|
fn namespace_slug(value : String) -> String {
let lower = ascii_lower(trim_ascii(value))
let mut out = ""
let mut previous_dash = false
let mut index = 0
while index < lower.length() {
let code = code_at(lower, index)
if is_ident_char(code) && code != 95 {
out = out + slice(lower, index, index + 1)
previous_dash = false
} else if !previous_dash && out.length() > 0 {
out = out + "-"
previous_dash = true
}
index += 1
}
if ends_with(out, "-") {
slice(out, 0, out.length() - 1)
} else {
out
}
}