#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum ContributorConfigEntry {
Preset(crate::presets::ContributorPreset),
Explicit(Box<ContributorConfig>),
}
impl Default for ContributorConfigEntry {
fn default() -> Self {
ContributorConfigEntry::Explicit(Box::default())
}
}
impl ContributorConfigEntry {
pub fn resolve(&self) -> ContributorConfig {
match self {
ContributorConfigEntry::Preset(preset) => preset.config(),
ContributorConfigEntry::Explicit(config) => *config.clone(),
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct ContributorConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub display_as_sort: Option<DisplayAsSort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initialize_with: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initialize_with_hyphen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shorten: Option<ShortenListOptions>,
#[serde(
default = "default_contributor_delimiter",
skip_serializing_if = "is_default_contributor_delimiter"
)]
pub delimiter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub and: Option<AndOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter_precedes_last: Option<DelimiterPrecedesLast>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter_precedes_et_al: Option<DelimiterPrecedesLast>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_role_options",
default
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<RoleOptionsEntry>"))]
pub role: Option<RoleOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub demote_non_dropping_particle: Option<DemoteNonDroppingParticle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_separator: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_form: Option<NameForm>,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<HashMap<String, serde_json::Value>>,
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
impl ContributorConfig {
pub fn merge(&mut self, other: &ContributorConfig) {
if other.display_as_sort.is_some() {
self.display_as_sort = other.display_as_sort;
}
if other.initialize_with.is_some() {
self.initialize_with = other.initialize_with.clone();
}
if other.initialize_with_hyphen.is_some() {
self.initialize_with_hyphen = other.initialize_with_hyphen;
}
if other.shorten.is_some() {
self.shorten = other.shorten.clone();
}
if other.delimiter.is_some() {
self.delimiter = other.delimiter.clone();
}
if other.and.is_some() {
self.and = other.and.clone();
}
if other.delimiter_precedes_last.is_some() {
self.delimiter_precedes_last = other.delimiter_precedes_last;
}
if other.delimiter_precedes_et_al.is_some() {
self.delimiter_precedes_et_al = other.delimiter_precedes_et_al;
}
if other.role.is_some() {
self.role = other.role.clone();
}
if other.demote_non_dropping_particle.is_some() {
self.demote_non_dropping_particle = other.demote_non_dropping_particle;
}
if other.sort_separator.is_some() {
self.sort_separator = other.sort_separator.clone();
}
if other.name_form.is_some() {
self.name_form = other.name_form;
}
}
pub fn role_rendering(
&self,
role: &crate::template::ContributorRole,
) -> Option<&RoleRendering> {
self.role.as_ref()?.role_rendering(role)
}
pub fn effective_role_label_preset(
&self,
role: &crate::template::ContributorRole,
) -> Option<RoleLabelPreset> {
self.role
.as_ref()
.and_then(|role_options| role_options.effective_label_preset(role))
}
pub fn effective_role_name_order(
&self,
role: &crate::template::ContributorRole,
) -> Option<&crate::template::NameOrder> {
self.role_rendering(role)
.and_then(|rendering| rendering.name_order.as_ref())
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum RoleLabelPreset {
None,
VerbPrefix,
VerbShortPrefix,
ShortSuffix,
LongSuffix,
}
impl RoleLabelPreset {
pub fn from_form_str(form: &str) -> Option<Self> {
match form {
"none" => Some(Self::None),
"verb" => Some(Self::VerbPrefix),
"verb-short" => Some(Self::VerbShortPrefix),
"short" => Some(Self::ShortSuffix),
"long" => Some(Self::LongSuffix),
_ => None,
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum DemoteNonDroppingParticle {
Never,
SortOnly,
#[default]
DisplayAndSort,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum DisplayAsSort {
All,
First,
#[default]
None,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum NameForm {
#[default]
Full,
FamilyOnly,
Initials,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum AndOptions {
Text,
Symbol,
#[default]
None,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum RoleOptionsEntry {
Preset(RoleLabelPreset),
Explicit(Box<RoleOptions>),
}
impl RoleOptionsEntry {
pub fn resolve(self) -> RoleOptions {
match self {
RoleOptionsEntry::Preset(preset) => RoleOptions {
preset: Some(preset),
..Default::default()
},
RoleOptionsEntry::Explicit(opts) => *opts,
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct RoleOptions {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub omit: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preset: Option<RoleLabelPreset>,
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<HashMap<String, RoleRendering>>,
}
impl RoleOptions {
pub fn role_rendering(
&self,
role: &crate::template::ContributorRole,
) -> Option<&RoleRendering> {
self.roles.as_ref()?.get(role.as_str())
}
pub fn effective_label_preset(
&self,
role: &crate::template::ContributorRole,
) -> Option<RoleLabelPreset> {
self.role_rendering(role)
.and_then(|rendering| rendering.preset)
.or(self.preset)
.or_else(|| {
self.form
.as_deref()
.and_then(RoleLabelPreset::from_form_str)
})
}
}
#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct RoleRendering {
#[serde(skip_serializing_if = "Option::is_none")]
pub preset: Option<RoleLabelPreset>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub emph: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strong: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub small_caps: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_order: Option<crate::template::NameOrder>,
}
impl RoleRendering {
pub fn to_rendering(&self) -> crate::template::Rendering {
crate::template::Rendering {
emph: self.emph,
strong: self.strong,
small_caps: self.small_caps,
prefix: self.prefix.clone(),
suffix: self.suffix.clone(),
..Default::default()
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum DelimiterPrecedesLast {
AfterInvertedName,
Always,
Never,
#[default]
Contextual,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct ShortenListOptions {
pub min: u8,
pub use_first: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub use_last: Option<u8>,
#[serde(default)]
pub and_others: AndOtherOptions,
#[serde(default)]
pub delimiter_precedes_last: DelimiterPrecedesLast,
#[serde(skip_serializing_if = "Option::is_none")]
pub subsequent_min: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subsequent_use_first: Option<u8>,
}
impl Default for ShortenListOptions {
fn default() -> Self {
Self {
min: 4,
use_first: 1,
use_last: None,
and_others: AndOtherOptions::default(),
delimiter_precedes_last: DelimiterPrecedesLast::default(),
subsequent_min: None,
subsequent_use_first: None,
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum AndOtherOptions {
#[default]
EtAl,
Text,
}
fn default_contributor_delimiter() -> Option<String> {
Some(", ".to_string())
}
fn is_default_contributor_delimiter(v: &Option<String>) -> bool {
v.as_deref() == Some(", ")
}
fn deserialize_role_options<'de, D>(deserializer: D) -> Result<Option<RoleOptions>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: Option<RoleOptionsEntry> = Option::deserialize(deserializer)?;
Ok(value.map(|entry| entry.resolve()))
}