git-gemini-forge 0.6.2

A simple Gemini server that serves a read-only view of public repositories from a Git forge.
use crate::handlers::templates::PercentEncoded;
use crate::network::responses::{Contents, FileEncoding, Item, Repo, ServerVersion, User};
use bytesize::ByteSize;
use serde::Deserialize;
use std::cmp::Ordering;
use url::Url;

/// Constructs a GitLab Project ID from the given percent-encoded username and project pathname values.
pub fn gitlab_project_id(
	username: &PercentEncoded,
	project_pathname: &PercentEncoded,
) -> PercentEncoded {
	let combined = format!("{}/{}", username.display(), project_pathname.display());
	PercentEncoded::new(&combined)
}

#[derive(Deserialize, Debug)]
pub struct GitLabMetadata {
	pub version: String,
	pub revision: String,
}

impl From<GitLabMetadata> for ServerVersion {
	fn from(metadata: GitLabMetadata) -> Self {
		let version = &metadata.version;
		let revision = &metadata.revision;
		return Self {
			version: format!("{version} ({revision})"),
		};
	}
}

#[derive(Deserialize, Debug)]
pub struct GitLabProjectNamespace {
	pub path: String,
}

#[derive(Deserialize, Debug)]
pub struct GitLabProject {
	/// An empty project has no default branch. See https://stackoverflow.com/a/73479901
	pub default_branch: Option<String>,
	pub web_url: Url,
	pub name: String,
	pub description: String,
	pub namespace: GitLabProjectNamespace,
	pub updated_at: Option<String>,
	pub created_at: String,

	/// If the project is a fork of another, these are the details of the parent.
	pub forked_from_project: Option<Box<GitLabProject>>,
}

impl From<&GitLabProject> for Repo {
	fn from(project: &GitLabProject) -> Self {
		let owner = User {
			login: project.namespace.path.clone(),
		};
		let default_branch = project
			.default_branch
			.clone()
			.or(Some(String::from("main")))
			.expect("");
		let updated_at = project
			.updated_at
			.clone()
			.or(Some(project.created_at.clone()))
			.expect("");
		return Self {
			default_branch,
			html_url: project.web_url.clone(),
			name: project.name.clone(),
			description: project.description.clone(),
			website: String::new(), // GitLab doesn't have anything like this afaik
			owner,
			updated_at,
			parent: project
				.forked_from_project
				.as_ref()
				.map(|p| Box::new(p.as_ref().into())),
		};
	}
}

impl From<GitLabProject> for Repo {
	fn from(project: GitLabProject) -> Self {
		Self::from(&project)
	}
}

#[derive(Deserialize, Debug)]
pub struct GitLabUser {
	pub username: String,
}

impl From<&GitLabUser> for User {
	fn from(user: &GitLabUser) -> Self {
		let login = user.username.clone();
		return Self { login };
	}
}

#[derive(Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum GitLabProjectFileEncoding {
	Base64,
}

impl From<&GitLabProjectFileEncoding> for FileEncoding {
	fn from(value: &GitLabProjectFileEncoding) -> Self {
		match value {
			GitLabProjectFileEncoding::Base64 => Self::Base64,
		}
	}
}

#[derive(Deserialize, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum GitLabProjectItemType {
	Blob,
	Tree,
}

#[derive(Deserialize, PartialEq, Eq, Clone, Debug)]
pub struct GitLabProjectItem {
	#[serde(rename = "type")]
	pub kind: GitLabProjectItemType,
	#[serde(rename = "id")]
	pub _id: String,
	pub name: String,
	pub path: String,
}

impl Ord for GitLabProjectItem {
	fn cmp(&self, other: &Self) -> Ordering {
		// Sort (file; alphabetically) before (dir; alphabetically)
		let a = self;
		let b = other;
		match a.kind {
			GitLabProjectItemType::Blob => match b.kind {
				GitLabProjectItemType::Tree => Ordering::Greater,
				GitLabProjectItemType::Blob => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
			},
			GitLabProjectItemType::Tree => match b.kind {
				GitLabProjectItemType::Blob => Ordering::Less,
				GitLabProjectItemType::Tree => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
			},
		}
	}
}

impl PartialOrd for GitLabProjectItem {
	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
		Some(self.cmp(other))
	}
}

impl GitLabProjectItem {
	/// Consumes self and constructs a new item representation with an HTTP URL.
	pub fn with_html_url(self, url: Url) -> GitLabProjectItemWithUrl {
		GitLabProjectItemWithUrl {
			item: self,
			html_url: url,
		}
	}
}

pub struct GitLabProjectItemWithUrl {
	item: GitLabProjectItem,
	html_url: Url,
}

impl GitLabProjectItemWithUrl {
	pub fn kind(&self) -> &GitLabProjectItemType {
		&self.item.kind
	}

	pub fn name(&self) -> &String {
		&self.item.name
	}

	pub fn path(&self) -> &String {
		&self.item.path
	}
}

impl From<&GitLabProjectItemWithUrl> for Item {
	fn from(item: &GitLabProjectItemWithUrl) -> Self {
		match item.kind() {
			GitLabProjectItemType::Blob => Self::File {
				content: None,
				encoding: None,
				name: item.name().clone(),
				path: item.path().clone(),
				size: ByteSize(0),
				html_url: item.html_url.clone(),
			},
			GitLabProjectItemType::Tree => Self::Dir {
				name: item.name().clone(),
				path: item.path().clone(),
				size: ByteSize(0),
				html_url: item.html_url.clone(),
			},
		}
	}
}

#[derive(Deserialize, PartialEq, Eq, Debug)]
pub struct GitLabProjectItemFile {
	file_name: String,
	file_path: String,
	size: ByteSize,
	encoding: GitLabProjectFileEncoding,
	content: String,
}

impl Ord for GitLabProjectItemFile {
	fn cmp(&self, other: &Self) -> Ordering {
		let other_name = other.file_name.to_lowercase();
		self.file_name.to_lowercase().cmp(&other_name)
	}
}

impl PartialOrd for GitLabProjectItemFile {
	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
		Some(self.cmp(other))
	}
}

impl GitLabProjectItemFile {
	/// Consumes self and constructs a new file representation with an HTTP URL.
	pub fn with_html_url(self, url: Url) -> GitLabProjectItemFileWithUrl {
		GitLabProjectItemFileWithUrl {
			file: self,
			html_url: url,
		}
	}
}

pub struct GitLabProjectItemFileWithUrl {
	file: GitLabProjectItemFile,
	html_url: Url,
}

impl GitLabProjectItemFileWithUrl {
	pub fn content(&self) -> &String {
		&self.file.content
	}

	pub fn encoding(&self) -> &GitLabProjectFileEncoding {
		&self.file.encoding
	}

	pub fn file_name(&self) -> &String {
		&self.file.file_name
	}

	pub fn file_path(&self) -> &String {
		&self.file.file_path
	}

	pub fn size(&self) -> &ByteSize {
		&self.file.size
	}
}

/// Describes either an object or a list of that object.
pub enum GitLabContents {
	Single(GitLabProjectItemFileWithUrl),
	Multiple(Vec<GitLabProjectItemWithUrl>),
}

impl From<GitLabContents> for Contents<Item> {
	fn from(contents: GitLabContents) -> Self {
		match contents {
			GitLabContents::Single(file) => Self::Single(Item::File {
				content: Some(file.content().clone()),
				encoding: Some(file.encoding().into()),
				name: file.file_name().clone(),
				path: file.file_path().clone(),
				size: file.size().clone(),
				html_url: file.html_url,
			}),
			GitLabContents::Multiple(items) => {
				Self::Multiple(items.iter().map(Into::into).collect())
			}
		}
	}
}