use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Changelog {
#[serde(default)]
pub format: crate::Format,
pub metadata: Metadata,
pub releases: Vec<Release>,
pub links: Vec<LinkDefinition>,
#[serde(default)]
pub source: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Metadata {
pub title: Option<String>,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Release {
pub version: Option<String>,
pub date: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub date_span: Option<Span>,
pub yanked: bool,
pub heading: String,
pub link: Option<String>,
pub sections: Vec<Section>,
pub span: Span,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub links: Vec<LinkDefinition>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ReleaseStatus {
Pending,
Draft,
Published,
DatedRecord,
Unversioned,
}
impl Changelog {
#[must_use]
pub fn new(format: crate::Format) -> Self {
Self {
format,
metadata: Metadata::default(),
releases: Vec::new(),
links: Vec::new(),
source: String::new(),
}
}
}
impl Release {
#[must_use]
pub fn new(version: Option<String>) -> Self {
Self {
version,
date: None,
date_span: None,
yanked: false,
heading: String::new(),
link: None,
sections: Vec::new(),
span: Span::default(),
attributes: BTreeMap::new(),
links: Vec::new(),
}
}
#[must_use]
pub fn status(&self) -> ReleaseStatus {
if self
.attributes
.get("draft")
.is_some_and(|value| value == "true")
{
ReleaseStatus::Draft
} else if self
.attributes
.get("record-kind")
.is_some_and(|value| value == "dated-change")
{
ReleaseStatus::DatedRecord
} else if self.version.is_some() {
ReleaseStatus::Published
} else if self
.attributes
.get("version-status")
.is_some_and(|value| value == "unknown")
{
ReleaseStatus::Unversioned
} else {
ReleaseStatus::Pending
}
}
#[must_use]
pub fn is_published(&self) -> bool {
matches!(
self.status(),
ReleaseStatus::Published | ReleaseStatus::DatedRecord
)
}
#[must_use]
pub fn is_pending(&self) -> bool {
matches!(self.status(), ReleaseStatus::Pending | ReleaseStatus::Draft)
}
#[must_use]
pub fn package(&self) -> Option<&str> {
self.attributes.get("package").map(String::as_str)
}
#[must_use]
pub fn is_prerelease(&self) -> bool {
self.attributes
.get("prerelease")
.is_some_and(|value| value == "true")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Section {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub component: Option<String>,
pub category: Option<Category>,
pub heading: Option<String>,
pub entries: Vec<Entry>,
pub span: Span,
}
impl Section {
#[must_use]
pub fn new(category: Option<Category>) -> Self {
Self {
component: None,
category,
heading: None,
entries: Vec::new(),
span: Span::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "name", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Category {
Added,
Changed,
Deprecated,
Removed,
Fixed,
Security,
Custom(String),
}
impl Category {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Added => "Added",
Self::Changed => "Changed",
Self::Deprecated => "Deprecated",
Self::Removed => "Removed",
Self::Fixed => "Fixed",
Self::Security => "Security",
Self::Custom(name) => name,
}
}
pub(crate) fn from_name(name: &str) -> Self {
match name.to_ascii_lowercase().as_str() {
"added" => Self::Added,
"changed" => Self::Changed,
"deprecated" => Self::Deprecated,
"removed" => Self::Removed,
"fixed" => Self::Fixed,
"security" => Self::Security,
_ => Self::Custom(name.to_owned()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Entry {
pub markdown: String,
pub kind: EntryKind,
pub span: Span,
}
impl Entry {
#[must_use]
pub fn new(markdown: String, kind: EntryKind) -> Self {
Self {
markdown,
kind,
span: Span::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EntryKind {
ListItem,
Markdown,
Text,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LinkDefinition {
pub label: String,
pub destination: String,
pub title: Option<String>,
pub span: Span,
}
impl LinkDefinition {
#[must_use]
pub fn new(label: String, destination: String) -> Self {
Self {
label,
destination,
title: None,
span: Span::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Span {
pub start: usize,
pub end: usize,
pub line: usize,
}
impl Span {
#[must_use]
pub const fn new(start: usize, end: usize, line: usize) -> Self {
Self { start, end, line }
}
}
impl Default for Span {
fn default() -> Self {
Self::new(0, 0, 1)
}
}