use std::convert::From;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use color_eyre::eyre::{eyre, Result, WrapErr};
use serde::Deserialize;
use crate::footnote;
const PROGRAM_NAME: &str = env!("CARGO_PKG_NAME");
const LEGACY_NAME: &str = "cizrna";
const DATA_PREFIX: &str = PROGRAM_NAME;
const GENERATED_PREFIX: &str = "generated";
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct TicketQuery {
pub tracker: tracker::Service,
pub using: KeyOrSearch,
pub overrides: Option<Overrides>,
pub references: Vec<Arc<TicketQuery>>,
}
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
pub enum KeyOrSearch {
Key(String),
Search(String),
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct TicketQueryEntry(
tracker::Service,
Identifier,
#[serde(default)] TicketQueryOptions,
);
impl From<TicketQueryEntry> for TicketQuery {
fn from(item: TicketQueryEntry) -> Self {
let (tracker, identifier, options) = (item.0, item.1, item.2);
let references: Vec<Arc<TicketQuery>> = options
.references
.into_iter()
.map(Self::from)
.map(Arc::new)
.collect();
Self {
using: identifier.into(),
tracker,
overrides: options.overrides,
references,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Identifier {
key: Option<KeyFormats>,
search: Option<String>,
}
impl From<Identifier> for KeyOrSearch {
fn from(item: Identifier) -> Self {
match (item.key.clone(), item.search.clone()) {
(Some(key), None) => KeyOrSearch::Key(key.into_string()),
(None, Some(search)) => KeyOrSearch::Search(search),
(Some(_), Some(_)) => panic!("Please specify only one entry:\n{item:#?}"),
(None, None) => panic!("Please specify at least one entry:\n{item:#?}"),
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
enum KeyFormats {
String(String),
Number(i32),
}
impl KeyFormats {
fn into_string(self) -> String {
match self {
Self::String(s) => s,
Self::Number(n) => n.to_string(),
}
}
}
#[derive(Debug, Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
struct TicketQueryOptions {
overrides: Option<Overrides>,
references: Vec<TicketQueryEntry>,
}
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Overrides {
pub doc_type: Option<String>,
pub components: Option<Vec<String>>,
pub subsystems: Option<Vec<String>>,
}
pub mod tracker {
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum Service {
#[serde(alias = "BZ")]
Bugzilla,
Jira,
}
impl fmt::Display for Service {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
Self::Bugzilla => "Bugzilla",
Self::Jira => "Jira",
};
write!(f, "{name}")
}
}
impl Service {
pub fn short_name(self) -> &'static str {
match self {
Self::Bugzilla => "BZ",
Self::Jira => "Jira",
}
}
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BugzillaFields {
pub doc_type: Vec<String>,
pub doc_text: Vec<String>,
pub doc_text_status: Vec<String>,
pub subsystems: Option<Vec<String>>,
pub target_release: Option<Vec<String>>,
pub docs_contact: Option<Vec<String>>,
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JiraFields {
pub doc_type: Vec<String>,
pub doc_text: Vec<String>,
pub doc_text_status: Vec<String>,
pub docs_contact: Vec<String>,
pub subsystems: Option<Vec<String>>,
pub target_release: Option<Vec<String>>,
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BugzillaInstance {
pub host: String,
pub api_key: Option<String>,
pub fields: BugzillaFields,
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JiraInstance {
pub host: String,
pub api_key: Option<String>,
#[serde(default)]
pub private_projects: Vec<String>,
pub fields: JiraFields,
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub jira: JiraInstance,
pub bugzilla: BugzillaInstance,
}
pub trait FieldsConfig {
fn doc_type(&self) -> &[String];
fn doc_text(&self) -> &[String];
fn target_release(&self) -> &[String];
fn subsystems(&self) -> &[String];
fn doc_text_status(&self) -> &[String];
fn docs_contact(&self) -> &[String];
fn host(&self) -> &str;
}
impl FieldsConfig for BugzillaInstance {
fn doc_type(&self) -> &[String] {
&self.fields.doc_type
}
fn doc_text_status(&self) -> &[String] {
&self.fields.doc_text_status
}
fn target_release(&self) -> &[String] {
match &self.fields.target_release {
Some(field) => field,
None => &[],
}
}
fn subsystems(&self) -> &[String] {
match &self.fields.subsystems {
Some(field) => field,
None => &[],
}
}
fn doc_text(&self) -> &[String] {
&self.fields.doc_text
}
fn docs_contact(&self) -> &[String] {
match &self.fields.docs_contact {
Some(field) => field,
None => &[],
}
}
fn host(&self) -> &str {
&self.host
}
}
impl FieldsConfig for JiraInstance {
fn doc_type(&self) -> &[String] {
&self.fields.doc_type
}
fn doc_text_status(&self) -> &[String] {
&self.fields.doc_text_status
}
fn target_release(&self) -> &[String] {
match &self.fields.target_release {
Some(field) => field,
None => &[],
}
}
fn subsystems(&self) -> &[String] {
match &self.fields.subsystems {
Some(field) => field,
None => &[],
}
}
fn doc_text(&self) -> &[String] {
&self.fields.doc_text
}
fn docs_contact(&self) -> &[String] {
&self.fields.docs_contact
}
fn host(&self) -> &str {
&self.host
}
}
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Template {
pub chapters: Vec<Section>,
#[serde(alias = "sections")]
pub subsections: Option<Vec<Section>>,
}
#[derive(Debug, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Section {
pub title: String,
pub intro_abstract: Option<String>,
pub filter: Filter,
#[serde(alias = "sections")]
pub subsections: Option<Vec<Section>>,
}
#[derive(Debug, Eq, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Filter {
pub doc_type: Option<Vec<String>>,
pub subsystem: Option<Vec<String>>,
pub component: Option<Vec<String>>,
}
fn parse_tickets(tickets_file: &Path) -> Result<Vec<TicketQuery>> {
let text =
fs::read_to_string(tickets_file).wrap_err("Cannot read the tickets configuration file.")?;
let config: Vec<TicketQueryEntry> =
serde_yaml::from_str(&text).wrap_err("Cannot parse the tickets configuration file.")?;
log::debug!("{:#?}", config);
let queries = config.into_iter().map(TicketQuery::from).collect();
Ok(queries)
}
fn parse_trackers(trackers_file: &Path) -> Result<tracker::Config> {
let text = fs::read_to_string(trackers_file)
.wrap_err("Cannot read the trackers configuration file.")?;
let trackers: tracker::Config =
serde_yaml::from_str(&text).wrap_err("Cannot parse the trackers configuration file.")?;
log::debug!("{:#?}", trackers);
Ok(trackers)
}
fn parse_templates(template_file: &Path) -> Result<Template> {
let text = fs::read_to_string(template_file).wrap_err("Cannot read the template file.")?;
let templates: Template =
serde_yaml::from_str(&text).wrap_err("Cannot parse the template file.")?;
log::debug!("{:#?}", templates);
Ok(templates)
}
pub struct Project {
pub _base_dir: PathBuf,
pub generated_dir: PathBuf,
pub tickets: Vec<Arc<TicketQuery>>,
pub trackers: tracker::Config,
pub templates: Template,
pub private_footnote: bool,
}
impl Project {
pub fn new(directory: &Path) -> Result<Self> {
let abs_path = directory.canonicalize()?;
let data_dir = locate_data_dir(directory)?;
let generated_dir = data_dir.join(GENERATED_PREFIX);
let tickets_path = data_dir.join("tickets.yaml");
let trackers_path = data_dir.join("trackers.yaml");
let templates_path = data_dir.join("templates.yaml");
log::debug!(
"Configuration files:\n* {}\n* {}\n* {}",
tickets_path.display(),
trackers_path.display(),
templates_path.display()
);
let tickets = parse_tickets(&tickets_path)?
.into_iter()
.map(Arc::new)
.collect();
let trackers = parse_trackers(&trackers_path)?;
let templates = parse_templates(&templates_path)?;
log::info!("Valid release notes project in {}.", abs_path.display());
let private_footnote = footnote::is_footnote_defined(&abs_path)?;
Ok(Self {
_base_dir: abs_path,
generated_dir,
tickets,
trackers,
templates,
private_footnote,
})
}
}
fn locate_data_dir(directory: &Path) -> Result<PathBuf> {
let abs_path = directory.canonicalize()?;
let data_dir = abs_path.join(DATA_PREFIX);
if data_dir.is_dir() {
Ok(data_dir)
} else {
let legacy_data_dir = abs_path.join(LEGACY_NAME);
if legacy_data_dir.is_dir() {
log::warn!(
"Please rename the `{}/` directory to `{}/`.",
LEGACY_NAME,
DATA_PREFIX
);
log::warn!("After renaming, you also have to adjust AsciiDoc include paths.");
Ok(legacy_data_dir)
} else {
Err(eyre!(
"The configuration directory is missing: {}",
data_dir.display()
))
}
}
}