git-gemini-forge 0.6.2

A simple Gemini server that serves a read-only view of public repositories from a Git forge.
use super::converters::{GitLabContents, GitLabProjectItem, GitLabProjectItemFile};
use super::get_repo::ProjectEndpoint;
use super::GitLabApi;
use crate::handlers::templates::PercentEncoded;
use crate::network::error;
use crate::network::net::{ApiEndpoint, Net};
use crate::network::ForgeApi;
use url::Url;

/// See https://docs.gitlab.com/ee/api/repository_files/
struct FileEndpoint<'a> {
	api: &'a GitLabApi,
	base_url: Url,
	project_id: &'a PercentEncoded,
	branch_name: &'a PercentEncoded,
	path: &'a PercentEncoded,
}

impl ApiEndpoint<GitLabApi, GitLabProjectItemFile> for FileEndpoint<'_> {
	fn api(&self) -> &GitLabApi {
		self.api
	}

	fn url(&self) -> Url {
		let mut url = self
			.base_url
			.join(&format!(
				"projects/{}/repository/files/{}",
				self.project_id.uri_component(),
				self.path.uri_component()
			))
			.expect("Valid URL path");
		url.set_query(Some(&format!("ref={}", self.branch_name.uri_component())));
		return url;
	}
}

/// See https://docs.gitlab.com/ee/api/repositories/#list-repository-tree
struct TreePathEndpoint<'a> {
	api: &'a GitLabApi,
	base_url: Url,
	project_id: &'a PercentEncoded,
	branch_name: &'a PercentEncoded,
	path: &'a PercentEncoded,
}

impl ApiEndpoint<GitLabApi, Vec<GitLabProjectItem>> for TreePathEndpoint<'_> {
	fn api(&self) -> &GitLabApi {
		self.api
	}

	fn url(&self) -> Url {
		let mut url = self
			.base_url
			.join(&format!(
				"projects/{}/repository/tree",
				self.project_id.uri_component()
			))
			.expect("Valid URL path");
		url.set_query(Some(&format!(
			"ref={}&path={}",
			self.branch_name.uri_component(),
			self.path.uri_component()
		)));
		url
	}
}

/// Retrieves the file or list of files at the given path.
pub fn get_contents(
	api: &GitLabApi,
	net: &Net,
	project_id: &PercentEncoded,
	branch_name: &PercentEncoded,
	path: &PercentEncoded,
) -> Result<GitLabContents, error::Error> {
	let base_url = api.base_url().clone();
	let file = FileEndpoint {
		api,
		base_url: base_url.clone(),
		project_id,
		branch_name,
		path,
	};
	let tree = TreePathEndpoint {
		api,
		base_url: base_url.clone(),
		project_id,
		branch_name,
		path,
	};
	let repo = ProjectEndpoint {
		api,
		base_url,
		project_id,
	};

	// Get the repo's HTML URL and construct the file or directory's URL from that
	let html_url = {
		let project = net.call(&repo)?;
		project.web_url
	};

	// Try the path as a file first
	// TODO: What if this is huge?
	match net.call(&file) {
		Ok(file) => Ok(GitLabContents::Single(file.with_html_url(html_url))),
		Err(error::Error::ResourceNotFound) => {
			// Failing that, try as a tree
			// TODO: Paginate this (see tree api doc)
			let mut items = net.call(&tree)?;
			items.sort();
			Ok(GitLabContents::Multiple(
				items
					.iter()
					.cloned()
					.map(|i| i.with_html_url(html_url.clone()))
					.collect(),
			))
		}
		Err(error) => Err(error),
	}
}