use std::process::ExitCode;
use serde::Serialize;
#[derive(Serialize, Default)]
pub struct Report {
pub command: &'static str,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub created: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub updated: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub skipped: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub removed: Vec<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub pretend: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<Vec<crate::generate::TemplateInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub about: Option<crate::about::About>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub checks: Vec<crate::doctor::Check>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stats: Option<crate::stats::Stats>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<Vec<crate::notes::Note>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<Option<String>>,
#[serde(skip)]
pub failure: Option<CliError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub pending: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ran: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub routes: Option<Vec<crate::routes::Route>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedules: Option<Vec<crate::schedules::Schedule>>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub remote: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub secrets: Option<Vec<crate::secrets::SecretStatus>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vapid: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_zones: Option<Vec<&'static str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domains: Option<Vec<String>>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub secret_created: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub secret_saved: Option<&'static str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub provisioned: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub next: Vec<String>,
#[serde(skip)]
pub rendered: bool,
}
impl Report {
pub fn new(command: &'static str) -> Self {
Self { command, ..Self::default() }
}
}
#[derive(Debug)]
pub struct CliError {
pub message: String,
pub hint: Option<String>,
}
impl CliError {
pub fn new(message: impl Into<String>) -> Self {
Self { message: message.into(), hint: None }
}
pub fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn to_string_with_hint(&self) -> String {
match &self.hint {
Some(hint) => format!("{} ({hint})", self.message),
None => self.message.clone(),
}
}
}
impl From<std::io::Error> for CliError {
fn from(err: std::io::Error) -> Self {
Self::new(err.to_string())
}
}
pub fn finish(result: Result<Report, CliError>, json: bool) -> ExitCode {
match result {
Ok(mut report) => {
let failure = report.failure.take();
if json {
let mut value = serde_json::to_value(&report).expect("report serializes");
value["ok"] = failure.is_none().into();
if let Some(err) = &failure {
value["error"] = err.message.clone().into();
value["hint"] = err.hint.clone().into();
}
println!("{value}");
} else {
if !report.rendered {
print_human(&report);
}
if let Some(err) = &failure {
print_error(err);
}
}
if failure.is_some() { ExitCode::FAILURE } else { ExitCode::SUCCESS }
}
Err(err) => {
if json {
let value = serde_json::json!({ "ok": false, "error": err.message, "hint": err.hint });
println!("{value}");
} else {
print_error(&err);
}
ExitCode::FAILURE
}
}
}
fn print_error(err: &CliError) {
eprintln!("error: {}", err.message);
if let Some(hint) = &err.hint {
eprintln!("hint: {hint}");
}
}
fn print_human(report: &Report) {
if let Some(about) = &report.about {
for (name, value) in about.lines() {
println!("{name:<20}{value}");
}
}
for check in &report.checks {
let status = match check.status {
crate::doctor::Status::Ok => "ok ",
crate::doctor::Status::Warn => "warn",
crate::doctor::Status::Fail => "FAIL",
};
println!(" {status} {:<20}{}", check.name, check.detail);
if let Some(hint) = check.hint.as_ref().filter(|_| check.status != crate::doctor::Status::Ok) {
println!(" {:<20}{hint}", "");
}
}
if let Some(stats) = &report.stats {
print!("{}", crate::stats::table(stats));
}
for note in report.notes.iter().flatten() {
println!("{}:{}: [{}] {}", note.path, note.line, note.tag, note.text);
}
if let Some(secret) = &report.secret {
println!("{secret}");
}
if let Some(vapid) = &report.vapid {
println!(
"VAPID_PUBLIC_KEY={}\nVAPID_PRIVATE_KEY={}",
vapid["public_key"].as_str().unwrap_or_default(),
vapid["private_key"].as_str().unwrap_or_default()
);
}
if let Some(domains) = &report.domains {
if domains.is_empty() {
println!(" no custom domain: the Worker answers on workers.dev");
}
for domain in domains {
println!(" {domain}");
}
}
for zone in report.time_zones.iter().flatten() {
println!("{zone}");
}
for secret in report.secrets.iter().flatten() {
let local = if secret.local { ".dev.vars" } else { "" };
let deployed = match (secret.deployed, &secret.store) {
(true, Some(_)) => "in the Secrets Store",
(false, Some(_)) => "bound, not in the Secrets Store",
(true, None) => "deployed",
(false, None) => "",
};
println!(" {:<32}{local:<12}{deployed}", secret.name);
}
if let Some(version) = &report.version {
println!("{}", version.as_deref().unwrap_or("no migration applied"));
}
for path in &report.created {
println!(" create {path}");
}
for path in &report.updated {
println!(" update {path}");
}
for path in &report.removed {
println!(" remove {path}");
}
for path in &report.skipped {
println!(" skip {path}");
}
if report.pretend {
println!("(--pretend: nothing was written)");
}
if let Some(email) = &report.email {
println!("Logged in to Cloudflare as {email}");
}
for step in &report.ran {
println!(" {step}");
}
for name in &report.pending {
println!(" pending {name}");
}
if let Some(routes) = &report.routes {
print!("{}", crate::routes::table(routes));
}
if let Some(schedules) = &report.schedules {
print!("{}", crate::schedules::table(schedules));
}
for template in report.templates.iter().flatten() {
println!(" {}{}", template.path, if template.overridden { " (overridden in .ocre/templates/)" } else { "" });
}
if report.remote {
println!("Target: remote D1 database on Cloudflare");
}
if report.secret_created {
println!("Created the SECRET_KEY_BASE secret on Cloudflare");
}
if let Some(file) = report.secret_saved {
println!("Saved it in {file} (git-ignored): back it up, Cloudflare never gives it back");
}
for resource in &report.provisioned {
println!("Created {resource} on Cloudflare");
}
if let Some(url) = &report.url {
println!("\n{url}");
}
if !report.next.is_empty() {
println!("\nNext:");
for step in &report.next {
println!(" {step}");
}
}
}
#[cfg(test)]
#[path = "../tests/output.rs"]
mod tests;