use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use schemars::JsonSchema;
use url::Url;
pub type Token = String;
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub site: Site,
pub auth: HashMap<Token, Vec<Source>>,
#[serde(rename = "allowed-licenses")]
pub allowed_licenses: Vec<License>,
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
let data =
fs::read(path).with_context(|| format!("unable to read from `{}`", path.display()))?;
toml::from_str(
str::from_utf8(&data)
.with_context(|| format!("unable to parse `{}` as UTF-8", path.display()))?,
)
.context("unable to parse config")
}
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Site {
pub name: String,
pub description: String,
#[serde(skip)]
#[serde(default = "default_output_dir")]
pub output_dir: PathBuf,
#[serde(skip)]
#[serde(default = "default_static_dir")]
pub static_dir: PathBuf,
#[serde(skip)]
#[serde(default = "default_files_dir")]
pub files_dir: PathBuf,
}
impl Site {
pub fn full_static_dir(&self) -> PathBuf {
self.output_dir.join(self.static_dir.clone())
}
pub fn full_files_dir(&self) -> PathBuf {
self.output_dir.join(self.files_dir.clone())
}
}
fn default_output_dir() -> PathBuf {
PathBuf::from(".")
}
fn default_static_dir() -> PathBuf {
PathBuf::from("static")
}
fn default_files_dir() -> PathBuf {
PathBuf::from("files")
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Source {
pub provider: Provider,
pub owner: String,
pub repo: String,
#[serde(rename = "custom-display-url")]
pub custom_display_url: Option<Url>,
#[serde(rename = "custom-fetch-url")]
pub custom_fetch_url: Option<Url>,
#[serde(default)]
#[serde(rename = "hide-urls")]
pub hide_urls: bool,
}
impl Source {
pub fn display_url(&self) -> Url {
match self.provider {
Provider::Codeberg => {
let default_display_url = Url::parse("https://codeberg.org/").unwrap();
match &self.custom_display_url {
Some(url) => url,
None => &default_display_url,
}
.join(&format!("{owner}/", owner = self.owner))
.unwrap()
.join(&format!("{repo}/", repo = self.repo))
.unwrap()
}
Provider::Gitea => {
let default_display_url = Url::parse("https://gitea.com/").unwrap();
match &self.custom_display_url {
Some(url) => url,
None => &default_display_url,
}
.join(&format!("{owner}/", owner = self.owner))
.unwrap()
.join(&format!("{repo}/", repo = self.repo))
.unwrap()
}
Provider::File => self.custom_display_url.clone().unwrap(),
}
}
pub fn fetch_url(&self) -> Url {
match self.provider {
Provider::Codeberg => {
let default_fetch_url = Url::parse("https://codeberg.org/").unwrap();
match &self.custom_fetch_url {
Some(url) => url,
None => &default_fetch_url,
}
.join(&format!("{owner}/", owner = self.owner))
.unwrap()
.join(&format!("{repo}/", repo = self.repo))
.unwrap()
.join("releases/download/latest/")
.unwrap()
}
Provider::Gitea => {
let default_fetch_url = Url::parse("https://gitea.com/").unwrap();
match &self.custom_fetch_url {
Some(url) => url,
None => &default_fetch_url,
}
.join(&format!("{owner}/", owner = self.owner))
.unwrap()
.join(&format!("{repo}/", repo = self.repo))
.unwrap()
.join("releases/download/latest/")
.unwrap()
}
Provider::File => self
.custom_fetch_url
.clone()
.unwrap_or_else(|| self.display_url()),
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, JsonSchema)]
pub enum Provider {
Codeberg,
Gitea,
File,
}
impl std::fmt::Display for Provider {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Provider::Codeberg => "Codeberg",
Provider::Gitea => "Gitea",
Provider::File => "File",
}
)
}
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct License {
pub name: String,
pub url: Url,
}
#[cfg(test)]
pub mod test {
use parameterized::parameterized;
use super::*;
#[test]
fn test_load_minimal_config() {
let expected_config = Config {
site: Site {
name: "".to_string(),
description: "".to_string(),
static_dir: PathBuf::from("static"),
files_dir: PathBuf::from("files"),
output_dir: PathBuf::from("."),
},
auth: HashMap::new(),
allowed_licenses: vec![],
};
let file = assert_fs::NamedTempFile::new("config.toml").unwrap();
let _ = fs::write(
file.path(),
r#"
site.name = ""
site.description = ""
auth = {}
allowed-licenses = []
"#,
);
let actual_config = Config::load(file.path()).unwrap();
pretty_assertions::assert_eq!(expected_config, actual_config);
}
#[test]
fn test_load_example_config() {
let expected_config = example_config();
let file = assert_fs::NamedTempFile::new("config.toml").unwrap();
let _ = fs::write(file.path(), raw_example_config());
let actual_config = Config::load(file.path()).unwrap();
pretty_assertions::assert_eq!(expected_config, actual_config);
}
#[parameterized(custom_display_url = {
None,
Some(Url::parse("https://forgejo.guemax.de/").unwrap()),
}, expected_display_url = {
Url::parse("https://codeberg.org/guemax/kleine-kochbuecher-der-elektrotechnik/")
.unwrap(),
Url::parse("https://forgejo.guemax.de/guemax/kleine-kochbuecher-der-elektrotechnik/")
.unwrap()
})]
fn test_codeberg_display_url(custom_display_url: Option<Url>, expected_display_url: Url) {
let source = Source {
provider: Provider::Codeberg,
owner: "guemax".to_string(),
repo: "kleine-kochbuecher-der-elektrotechnik".to_string(),
custom_display_url: custom_display_url,
custom_fetch_url: None,
hide_urls: false,
};
let actual_display_url = source.display_url();
pretty_assertions::assert_eq!(expected_display_url, actual_display_url);
}
#[parameterized(custom_display_url = {
None,
Some(Url::parse("https://gitea.localhost/").unwrap()),
}, expected_display_url = {
Url::parse("https://gitea.com/alexander/TUM-Formelsammlungen/")
.unwrap(),
Url::parse("https://gitea.localhost/alexander/TUM-Formelsammlungen/")
.unwrap()
})]
fn test_gitea_display_url(custom_display_url: Option<Url>, expected_display_url: Url) {
let source = Source {
provider: Provider::Gitea,
owner: "alexander".to_string(),
repo: "TUM-Formelsammlungen".to_string(),
custom_display_url: custom_display_url,
custom_fetch_url: None,
hide_urls: false,
};
let actual_display_url = source.display_url();
pretty_assertions::assert_eq!(expected_display_url, actual_display_url);
}
#[test]
fn test_file_display_url() {
let custom_display_url = Some(
Url::parse(&format!(
"file://{}",
std::env::current_dir().unwrap().display()
))
.unwrap(),
);
let source = Source {
provider: Provider::File,
owner: "owner".to_string(),
repo: "repo".to_string(),
custom_display_url: custom_display_url.clone(),
custom_fetch_url: None,
hide_urls: false,
};
let actual_display_url = source.display_url();
let expected_display_url = custom_display_url.unwrap();
pretty_assertions::assert_eq!(expected_display_url, actual_display_url);
}
#[test]
#[should_panic]
fn test_file_display_url_fails_when_none() {
let source = Source {
provider: Provider::File,
owner: String::new(),
repo: String::new(),
custom_display_url: None,
custom_fetch_url: None,
hide_urls: false,
};
source.display_url();
}
#[parameterized(custom_fetch_url = {
None,
Some(Url::parse("https://forgejo.guemax.de/").unwrap()),
}, expected_fetch_url = {
Url::parse("https://codeberg.org/guemax/kleine-kochbuecher-der-elektrotechnik/releases/download/latest/")
.unwrap(),
Url::parse("https://forgejo.guemax.de/guemax/kleine-kochbuecher-der-elektrotechnik/releases/download/latest/")
.unwrap()
})]
fn test_codeberg_fetch_url(custom_fetch_url: Option<Url>, expected_fetch_url: Url) {
let source = Source {
provider: Provider::Codeberg,
owner: "guemax".to_string(),
repo: "kleine-kochbuecher-der-elektrotechnik".to_string(),
custom_display_url: None,
custom_fetch_url: custom_fetch_url,
hide_urls: false,
};
let actual_fetch_url = source.fetch_url();
pretty_assertions::assert_eq!(expected_fetch_url, actual_fetch_url);
}
#[parameterized(custom_fetch_url = {
None,
Some(Url::parse("https://gitea.localhost/").unwrap()),
}, expected_fetch_url = {
Url::parse("https://gitea.com/alexander/TUM-Formelsammlungen/releases/download/latest/")
.unwrap(),
Url::parse("https://gitea.localhost/alexander/TUM-Formelsammlungen/releases/download/latest/")
.unwrap()
})]
fn test_gitea_fetch_url(custom_fetch_url: Option<Url>, expected_fetch_url: Url) {
let source = Source {
provider: Provider::Gitea,
owner: "alexander".to_string(),
repo: "TUM-Formelsammlungen".to_string(),
custom_display_url: None,
custom_fetch_url: custom_fetch_url,
hide_urls: false,
};
let actual_fetch_url = source.fetch_url();
pretty_assertions::assert_eq!(expected_fetch_url, actual_fetch_url);
}
#[test]
fn test_file_fetch_url() {
let custom_fetch_url = Some(
Url::parse(&format!(
"file://{}",
std::env::current_dir().unwrap().display()
))
.unwrap(),
);
let source = Source {
provider: Provider::File,
owner: String::new(),
repo: String::new(),
custom_display_url: None,
custom_fetch_url: custom_fetch_url.clone(),
hide_urls: false,
};
let actual_fetch_url = source.fetch_url();
let expected_fetch_url = custom_fetch_url.unwrap();
pretty_assertions::assert_eq!(expected_fetch_url, actual_fetch_url);
}
#[test]
fn test_file_fetch_url_fallback_to_display_url() {
let custom_display_url = Some(
Url::parse(&format!(
"file://{}",
std::env::current_dir().unwrap().display()
))
.unwrap(),
);
let source = Source {
provider: Provider::File,
owner: String::new(),
repo: String::new(),
custom_display_url: custom_display_url.clone(),
custom_fetch_url: None,
hide_urls: false,
};
let actual_fetch_url = source.fetch_url();
let expected_fetch_url = custom_display_url.unwrap();
pretty_assertions::assert_eq!(expected_fetch_url, actual_fetch_url);
}
pub fn example_config() -> Config {
Config {
site: Site {
name: "Typst4EI".to_string(),
description: "Hello World!".to_string(),
static_dir: PathBuf::from("static"),
files_dir: PathBuf::from("files"),
output_dir: PathBuf::from("."),
},
auth: HashMap::from([
(
"acedcbf5977a64f6ca03deb8de08f388fadd5d87".to_string(),
vec![Source {
provider: Provider::Codeberg,
owner: "guemax".to_string(),
repo: "kleine-kochbuecher-der-elektrotechnik".to_string(),
custom_display_url: None,
custom_fetch_url: None,
hide_urls: false,
}],
),
(
"9cedcbf5977a64f6ca03deb8de08f388fadd5d87".to_string(),
vec![
Source {
provider: Provider::Gitea,
owner: "alexander".to_string(),
repo: "TUM-Formelsammlungen".to_string(),
custom_display_url: Some(
Url::parse("https://gitea.mintcalc.com/").unwrap(),
),
custom_fetch_url: Some(Url::parse("https://gitea.localhost/").unwrap()),
hide_urls: false,
},
Source {
provider: Provider::Gitea,
owner: "alexander".to_string(),
repo: "RAW-Formelsammlungen".to_string(),
custom_display_url: Some(
Url::parse("https://gitea.mintcalc.com/").unwrap(),
),
custom_fetch_url: Some(Url::parse("https://gitea.localhost/").unwrap()),
hide_urls: true,
},
],
),
]),
allowed_licenses: vec![
License {
name: "CC0".to_string(),
url: Url::parse("https://creativecommons.org/publicdomain/zero/1.0/").unwrap(),
},
License {
name: "CC BY-SA 4.0".to_string(),
url: Url::parse("https://creativecommons.org/licenses/by-sa/4.0/").unwrap(),
},
],
}
}
fn raw_example_config() -> String {
r#"
[site]
name = "Typst4EI"
description = "Hello World!"
[[auth.acedcbf5977a64f6ca03deb8de08f388fadd5d87]]
provider = "Codeberg"
owner = "guemax"
repo = "kleine-kochbuecher-der-elektrotechnik"
[[auth.9cedcbf5977a64f6ca03deb8de08f388fadd5d87]]
provider = "Gitea"
owner = "alexander"
repo = "TUM-Formelsammlungen"
custom-display-url = "https://gitea.mintcalc.com/"
custom-fetch-url = "https://gitea.localhost/"
[[auth.9cedcbf5977a64f6ca03deb8de08f388fadd5d87]]
provider = "Gitea"
owner = "alexander"
repo = "RAW-Formelsammlungen"
custom-display-url = "https://gitea.mintcalc.com/"
custom-fetch-url = "https://gitea.localhost/"
hide-urls = true
[[allowed-licenses]]
name = "CC0"
url = "https://creativecommons.org/publicdomain/zero/1.0/"
[[allowed-licenses]]
name = "CC BY-SA 4.0"
url = "https://creativecommons.org/licenses/by-sa/4.0/"
"#
.to_string()
}
pub fn example_licenses() -> (License, License) {
let cc0 = License {
name: "CC0".to_string(),
url: Url::parse("https://creativecommons.org/publicdomain/zero/1.0/").unwrap(),
};
let cc_by_sa_4_0 = License {
name: "CC BY-SA 4.0".to_string(),
url: Url::parse("https://creativecommons.org/licenses/by-sa/4.0/").unwrap(),
};
(cc0, cc_by_sa_4_0)
}
}