verctl 0.0.3

Stack-agnostic version PRs from Changesets-format fragments
Documentation
use anyhow::{Context, Result};
use minijinja::{Environment, context};
use serde::Serialize;

const RELEASE_TEMPLATE: &str = include_str!("../templates/changelog.jinja");
const DEPENDENCY_TEMPLATE: &str = include_str!("../templates/dependency-changelog.jinja");

#[derive(Debug, Clone, Serialize)]
pub struct PullRequest {
    pub number: u64,
    pub url: String,
    pub user: Option<String>,
    pub user_url: Option<String>,
    pub external_author: bool,
}

#[derive(Debug, Clone, Serialize)]
pub struct Commit {
    pub short: String,
    pub url: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct Dependency {
    pub name: String,
    pub new_version: String,
}

#[derive(Debug, Clone)]
pub struct ReleaseInput {
    pub summary: String,
    pub continuations: Vec<String>,
    pub pull_request: Option<PullRequest>,
    pub commit: Option<Commit>,
}

impl ReleaseInput {
    #[must_use]
    pub fn with_author_filter(mut self, internal_authors: &[String]) -> Self {
        if let Some(pr) = &mut self.pull_request {
            let login = pr.user.as_deref().unwrap_or("");
            pr.external_author =
                !login.is_empty() && !internal_authors.iter().any(|name| name == login);
        }
        self
    }
}

#[must_use]
pub fn summary_has_terminal(summary: &str) -> bool {
    summary
        .trim_end()
        .chars()
        .next_back()
        .is_some_and(|ch| matches!(ch, '.' | '!' | '?'))
}

pub fn render_release(input: &ReleaseInput) -> Result<String> {
    render_release_template(RELEASE_TEMPLATE, input)
}

pub fn render_release_template(template: &str, input: &ReleaseInput) -> Result<String> {
    let mut env = Environment::new();
    env.add_template("changelog", template)
        .context("parse changelog template")?;
    let tmpl = env.get_template("changelog")?;
    let rendered = tmpl
        .render(context! {
            summary => input.summary,
            continuations => input.continuations,
            pull_request => input.pull_request,
            commit => input.commit,
            summary_has_terminal => summary_has_terminal(&input.summary),
        })
        .context("render changelog")?;
    Ok(rendered)
}

pub fn render_dependencies(dependencies: &[Dependency]) -> Result<String> {
    let mut env = Environment::new();
    env.add_template("deps", DEPENDENCY_TEMPLATE)
        .context("parse dependency template")?;
    let tmpl = env.get_template("deps")?;
    tmpl.render(context! { dependencies => dependencies })
        .context("render dependency changelog")
}