use serde::Deserialize;
use smart_default::SmartDefault;
use std::borrow::Borrow;
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct FileSet {
from: Option<String>,
#[serde(default)]
to: Option<String>,
#[serde(default)]
pub(crate) filter: MightBeSingle<String>,
}
impl<'a> FileSet {
pub fn from(&'a self) -> Option<&'a str> {
self.from
.as_ref()
.map(|f| f.strip_prefix("./"))
.flatten()
.or_else(|| self.from.as_deref())
}
pub fn to(&'a self) -> Option<&'a str> {
self.to
.as_ref()
.map(|to| to.strip_prefix("./"))
.flatten()
.or_else(|| self.to.as_deref())
}
pub fn filters(&'a self) -> Vec<&'a str> {
self.filter.as_vec_str()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum CopyDef {
Simple(String),
Set(FileSet),
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct EBDirectories {
pub output: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProtocolAssociation {
pub name: Option<String>,
pub schemes: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileAssociation {
ext: MightBeSingle<String>,
pub mime_type: Option<String>,
}
impl FileAssociation {
pub fn exts(&self) -> Vec<&str> {
self.ext.as_vec_str()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, SmartDefault)]
#[serde(untagged)]
pub(crate) enum MightBeSingle<T> {
Multiple(Vec<T>),
One(T),
#[default]
None,
}
impl<T> MightBeSingle<T> {
fn is_empty(&self) -> bool {
match self {
MightBeSingle::None => true,
MightBeSingle::One(_) => false,
MightBeSingle::Multiple(multiple) => multiple.is_empty(),
}
}
fn or<'a>(&'a self, other: &'a MightBeSingle<T>) -> &'a MightBeSingle<T> {
if self.is_empty() {
other
} else {
self
}
}
fn as_vec<'a>(&'a self) -> Vec<&'a T> {
match self {
MightBeSingle::None => Vec::new(),
MightBeSingle::One(one) => vec![one],
MightBeSingle::Multiple(multiple) => multiple.iter().collect(),
}
}
}
impl<T> From<Vec<T>> for MightBeSingle<T> {
fn from(value: Vec<T>) -> Self {
Self::Multiple(value)
}
}
impl<'a, T: Borrow<str>> MightBeSingle<T> {
fn as_vec_str(&'a self) -> Vec<&'a str> {
match self {
MightBeSingle::None => Vec::new(),
MightBeSingle::One(one) => vec![one.borrow()],
MightBeSingle::Multiple(multiple) => multiple.iter().map(Borrow::borrow).collect(),
}
}
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CommonOverridableProperties {
pub description: Option<String>,
pub executable_name: Option<String>,
pub product_name: Option<String>,
pub desktop_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EBuilderBaseConfig {
#[serde(flatten)]
pub(crate) common: CommonOverridableProperties,
#[serde(default)]
files: MightBeSingle<CopyDef>,
#[serde(default)]
asar_unpack: MightBeSingle<String>,
#[serde(default)]
extra_files: MightBeSingle<CopyDef>,
#[serde(default)]
extra_resources: MightBeSingle<CopyDef>,
#[serde(default)]
directories: EBDirectories,
icon: Option<String>,
#[serde(default)]
protocols: MightBeSingle<ProtocolAssociation>,
#[serde(default)]
file_associations: MightBeSingle<FileAssociation>,
#[serde(default)]
category: MightBeSingle<String>,
desktop: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EBuilderConfig {
#[serde(flatten)]
pub(crate) base: EBuilderBaseConfig,
#[serde(default)]
linux: EBuilderBaseConfig,
#[serde(default)]
mac: EBuilderBaseConfig,
#[serde(default)]
win: EBuilderBaseConfig,
}
impl<'a> EBuilderConfig {
#[cfg(target_os = "linux")]
#[inline]
pub(crate) fn current_platform(&'a self) -> &'a EBuilderBaseConfig {
&self.linux
}
pub fn files(&'a self) -> Vec<&'a CopyDef> {
self.current_platform()
.files
.or(&self.base.files)
.as_vec()
}
pub fn asar_unpack(&'a self) -> Vec<&'a str> {
self.current_platform()
.asar_unpack
.or(&self.base.asar_unpack)
.as_vec_str()
}
pub fn extra_files(&'a self) -> Vec<&'a CopyDef> {
self.current_platform()
.extra_files
.or(&self.base.extra_files)
.as_vec()
}
pub fn extra_resources(&'a self) -> Vec<&'a CopyDef> {
self.current_platform()
.extra_resources
.or(&self.base.extra_resources)
.as_vec()
}
pub fn desktop_properties(&'a self) -> Option<Vec<(String, String)>> {
self.current_platform()
.desktop
.as_ref()
.or_else(|| self.base.desktop.as_ref())
.map(|m| m.clone().into_iter().collect())
}
pub fn output_dir(&'a self) -> Option<&'a str> {
self.current_platform()
.directories
.output
.as_deref()
.or_else(|| self.base.directories.output.as_deref())
}
pub fn protocol_associations(&'a self) -> Vec<&ProtocolAssociation> {
self.current_platform()
.protocols
.or(&self.base.protocols)
.as_vec()
}
pub fn file_associations(&'a self) -> Vec<&FileAssociation> {
self.current_platform()
.file_associations
.or(&self.base.file_associations)
.as_vec()
}
pub fn desktop_categories(&'a self) -> Vec<&'a str> {
self.current_platform().category.as_vec_str()
}
pub(crate) fn icon_locations(&'a self) -> Vec<&'a str> {
[
self.linux.icon.as_deref(),
self.mac
.icon
.as_deref()
.or(Some("build/icon.icns")),
self.win
.icon
.as_deref()
.or(Some("build/icon.ico")),
self.base.icon.as_deref(),
]
.into_iter()
.filter_map(|i| i)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::EBuilderConfig;
use crate::config::{CopyDef, FileSet, MightBeSingle};
use anyhow::Result;
use serde_json::json;
#[test]
fn test_parse_empty() -> Result<()> {
let bc: EBuilderConfig = serde_json::from_value(json!({
"files": null,
"asarUnpack": [],
}))?;
assert!(bc.files().is_empty());
assert!(bc.asar_unpack().is_empty());
assert!(bc.extra_resources().is_empty());
Ok(())
}
#[test]
fn test_parse_single() -> Result<()> {
let bc: EBuilderConfig = serde_json::from_value(json!({
"files": "file.aoeu",
"asarUnpack": "*.aoeu",
"extraResources": {
"from": "dir",
},
}))?;
assert_eq!(bc.files(), vec![&CopyDef::Simple("file.aoeu".to_owned())]);
assert_eq!(bc.asar_unpack(), vec!["*.aoeu"]);
assert_eq!(
bc.extra_resources(),
vec![&CopyDef::Set(FileSet {
from: Some("dir".to_owned()),
to: None,
filter: MightBeSingle::None,
})]
);
Ok(())
}
#[test]
fn test_parse_multiple() -> Result<()> {
let bc: EBuilderConfig = serde_json::from_value(json!({
"files": ["file.aoeu", "bestand.aoeu"],
"asarUnpack": ["*.aoeu", "dir/"],
"extraResources": [{
"from": "source",
"filter": "*",
}, "dir1", "dir2", {
"from": "hx",
"to": "mz",
"filter": ["**/*", "!foo/*.js"],
}, {
"filter": ["LICENSE.txt"],
}],
}))?;
assert_eq!(
bc.files(),
vec![
&CopyDef::Simple("file.aoeu".to_owned()),
&CopyDef::Simple("bestand.aoeu".to_owned()),
],
);
assert_eq!(bc.asar_unpack(), vec!["*.aoeu", "dir/"]);
assert_eq!(
bc.extra_resources(),
vec![
&CopyDef::Set(FileSet {
from: Some("source".to_owned()),
to: None,
filter: MightBeSingle::One("*".to_owned()),
}),
&CopyDef::Simple("dir1".to_owned()),
&CopyDef::Simple("dir2".to_owned()),
&CopyDef::Set(FileSet {
from: Some("hx".to_owned()),
to: Some("mz".to_owned()),
filter: MightBeSingle::Multiple(vec![
"**/*".to_owned(),
"!foo/*.js".to_owned(),
]),
}),
&CopyDef::Set(FileSet {
from: None,
to: None,
filter: MightBeSingle::Multiple(vec!["LICENSE.txt".to_owned()]),
}),
],
);
Ok(())
}
}