use std::io::{self, Write};
use time::format_description::well_known::Rfc3339;
use crate::errors::SnapshotError;
use crate::model::names::ModuleName;
use crate::model::snapshot::{
ArityMatch, Deprecation, FORMAT_VERSION, HrlFile, Module, Provenance, Snapshot, SnapshotHeader,
VendoredDep, VersionedMachineVersion, Visibility, WireConstantBinding, WireValue, state,
};
pub const HEADER_PREFIX: &str = "# ";
pub fn write<W: Write>(
snapshot: &Snapshot<state::Canonical>,
w: &mut W,
) -> Result<(), SnapshotError> {
write_header(snapshot.header(), w)?;
let mut first = true;
for module in snapshot.modules() {
if !first {
writeln!(w)?;
}
first = false;
write_module(module, w)?;
}
for hrl in snapshot.headers() {
if !first {
writeln!(w)?;
}
first = false;
write_hrl(hrl, w)?;
}
Ok(())
}
pub fn to_string(snapshot: &Snapshot<state::Canonical>) -> Result<String, SnapshotError> {
let mut buf = Vec::new();
write(snapshot, &mut buf)?;
String::from_utf8(buf).map_err(|e| SnapshotError::InvalidUtf8 {
offset: e.utf8_error().valid_up_to(),
})
}
pub fn write_module_filtered<W: Write>(
snapshot: &Snapshot<state::Canonical>,
module: &ModuleName,
w: &mut W,
) -> Result<bool, SnapshotError> {
write_header(snapshot.header(), w)?;
let Some(m) = snapshot.module_named(module) else {
return Ok(false);
};
write_module(m, w)?;
Ok(true)
}
fn write_header<W: Write>(header: &SnapshotHeader, w: &mut W) -> io::Result<()> {
writeln!(w, "# backhopper snapshot")?;
writeln!(w, "# format-version: {FORMAT_VERSION}")?;
writeln!(w, "# project: {}", header.project)?;
writeln!(w, "# tag: {}", header.tag)?;
if let Some(branch) = &header.branch {
writeln!(w, "# branch: {branch}")?;
}
writeln!(w, "# commit: {}", header.commit)?;
writeln!(w, "# scanned-paths: {}", header.scanned_paths.join(", "))?;
if !header.apps_scanned.is_empty() {
let names: Vec<&str> = header.apps_scanned.iter().map(|a| a.as_str()).collect();
writeln!(w, "# apps-scanned: {}", names.join(", "))?;
}
writeln!(w, "# generated-by: {}", header.generated_by)?;
let formatted = header
.generated_at
.format(&Rfc3339)
.unwrap_or_else(|_| String::from("0"));
writeln!(w, "# generated-at: {formatted}")?;
if !header.extractor_version.is_empty() {
writeln!(w, "# extractor-version: {}", header.extractor_version)?;
}
for pin in &header.dep_pins {
write_dep_pin(w, pin)?;
}
writeln!(w)?;
Ok(())
}
fn write_dep_pin<W: Write>(w: &mut W, pin: &VendoredDep) -> io::Result<()> {
let label = pin.source.as_label();
writeln!(w, "# dep-pin: {} = {} {}", pin.name, label, pin.version)
}
pub fn module_to_string(m: &Module) -> Result<String, SnapshotError> {
let mut buf = Vec::new();
write_module(m, &mut buf)?;
String::from_utf8(buf).map_err(|e| SnapshotError::InvalidUtf8 {
offset: e.utf8_error().valid_up_to(),
})
}
fn write_module<W: Write>(m: &Module, w: &mut W) -> io::Result<()> {
writeln!(w, "module {}", m.name)?;
if m.visibility != Visibility::Public {
writeln!(w, " visibility {}", m.visibility.keyword())?;
}
if let Some(path) = &m.path {
writeln!(w, " path {path}")?;
}
if let Some(app) = &m.app {
writeln!(w, " app {app}")?;
}
for b in &m.behaviours {
writeln!(w, " behaviour {b}")?;
}
for e in &m.exports {
writeln!(w, " export {}/{}", e.name, e.arity)?;
}
for et in &m.export_types {
writeln!(w, " export_type {}/{}", et.name, et.arity)?;
}
for c in &m.callbacks {
write_keyword_signature(
w,
"callback",
&format!("{}/{}", c.name, c.arity),
&c.signature,
)?;
}
for oc in &m.optional_callbacks {
writeln!(w, " optional_callback {}/{}", oc.name, oc.arity)?;
}
for s in &m.specs {
write_keyword_signature(w, "spec", &format!("{}/{}", s.name, s.arity), &s.signature)?;
}
for t in &m.types {
let body = format!("{}/{} :: {}", t.name, t.arity, t.rhs);
write_keyword_signature(w, "type", &body, "")?;
}
for o in &m.opaques {
writeln!(w, " opaque {}/{}", o.name, o.arity)?;
}
for d in &m.deprecations {
write_deprecation(d, w)?;
}
for r in &m.records {
writeln!(w, " record {}", r.name)?;
for f in &r.fields {
if let Some(t) = &f.type_repr {
writeln!(w, " field {} :: {}", f.name, t)?;
} else {
writeln!(w, " field {}", f.name)?;
}
}
}
for t in &m.test_only_exports {
let variant = t.variant.as_label();
match t.body_line {
Some(body) => writeln!(
w,
" test_only_export {} {}/{} export_line={} body_line={}",
variant, t.function, t.arity, t.export_line, body
)?,
None => writeln!(
w,
" test_only_export {} {}/{} export_line={}",
variant, t.function, t.arity, t.export_line
)?,
}
}
for im in &m.ifdef_macros {
let guard = im.guard_kind.as_label();
writeln!(
w,
" ifdef_macro {} guard={} line={}",
im.name, guard, im.line
)?;
}
for vc in &m.variant_c_blocks {
match vc.else_line {
Some(else_line) => writeln!(
w,
" variant_c_block guard={} start_line={} else_line={} end_line={}",
vc.guard, vc.start_line, else_line, vc.end_line
)?,
None => writeln!(
w,
" variant_c_block guard={} start_line={} end_line={}",
vc.guard, vc.start_line, vc.end_line
)?,
}
}
if let Some(v) = &m.versioned_machine_version {
write_versioned_machine(w, v)?;
}
for wc in &m.wire_constants {
write_wire_constant(w, wc)?;
}
Ok(())
}
fn write_versioned_machine<W: Write>(w: &mut W, v: &VersionedMachineVersion) -> io::Result<()> {
let value = match v.value {
Some(n) => n.to_string(),
None => "-".to_owned(),
};
match &v.provenance {
Provenance::Literal => writeln!(
w,
" versioned_machine {}/{} = {} literal",
v.function, v.arity, value
),
Provenance::MacroBody {
macro_name,
defined_in: Some(path),
} => writeln!(
w,
" versioned_machine {}/{} = {} macro_body {} at {}",
v.function, v.arity, value, macro_name, path
),
Provenance::MacroBody {
macro_name,
defined_in: None,
} => writeln!(
w,
" versioned_machine {}/{} = {} macro_body {}",
v.function, v.arity, value, macro_name
),
}
}
fn write_wire_constant<W: Write>(w: &mut W, wc: &WireConstantBinding) -> io::Result<()> {
let (kind, body) = match &wc.value {
WireValue::U64(n) => ("u64", n.to_string()),
WireValue::Bytes(b) => ("bytes", String::from_utf8_lossy(b).into_owned()),
WireValue::Opaque(s) => ("opaque", s.clone()),
};
match &wc.defined_in {
Some(path) => writeln!(
w,
" wire_constant {} = {} {} at {}",
wc.macro_name, kind, body, path
),
None => writeln!(w, " wire_constant {} = {} {}", wc.macro_name, kind, body),
}
}
fn write_hrl<W: Write>(h: &HrlFile, w: &mut W) -> io::Result<()> {
writeln!(w, "header {}", h.path)?;
for t in &h.types {
let body = format!("{}/{} :: {}", t.name, t.arity, t.rhs);
write_keyword_signature(w, "type", &body, "")?;
}
for o in &h.opaques {
writeln!(w, " opaque {}/{}", o.name, o.arity)?;
}
for r in &h.records {
writeln!(w, " record {}", r.name)?;
for f in &r.fields {
if let Some(t) = &f.type_repr {
writeln!(w, " field {} :: {}", f.name, t)?;
} else {
writeln!(w, " field {}", f.name)?;
}
}
}
Ok(())
}
fn write_keyword_signature<W: Write>(
w: &mut W,
keyword: &str,
head: &str,
body: &str,
) -> io::Result<()> {
if body.is_empty() {
writeln!(w, " {keyword} {head}")?;
return Ok(());
}
let mut lines = body.split('\n');
let first = lines.next().unwrap_or("");
writeln!(w, " {keyword} {head} {first}")?;
for cont in lines {
writeln!(w, "{cont}")?;
}
Ok(())
}
fn write_deprecation<W: Write>(d: &Deprecation, w: &mut W) -> io::Result<()> {
write!(w, " ")?;
match (&d.function, &d.arity_match, d.module_wide) {
(_, _, true) => write!(w, "deprecated module")?,
(Some(f), ArityMatch::Exact { arity }, _) => write!(w, "deprecated {f}/{arity}")?,
(Some(f), ArityMatch::Any, _) => write!(w, "deprecated {f}/*")?,
(None, _, _) => write!(w, "deprecated *")?,
}
if let Some(since) = &d.since {
write!(w, " since {since}")?;
}
if let Some(rep) = &d.replacement {
write!(w, " use {}/{}", rep.function, rep.arity)?;
}
if let Some(reason) = &d.reason {
write!(w, " reason {reason:?}")?;
}
writeln!(w)?;
Ok(())
}