use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use serde::de::{Deserializer, Error as _};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::compat::arg_shape::ArgShape;
use crate::errors::{NameError, SnapshotError};
use crate::model::app_manifest::AppManifest;
use crate::model::macro_decl::MacroDecl;
use crate::model::names::{
ApplicationName, Arity, CommitSha, DependencyName, DependencyVersion, ExtractionSettingsDigest,
FieldName, FunctionName, HostFactsDigest, MacroName, ModuleName, ProjectName, RecordName,
RelativePath, TagName, TypeName,
};
use crate::model::non_empty::NonEmpty;
use crate::model::normalised::NormalisedDefault;
use crate::model::schema_files::{PartialFile, SchemaFile};
use crate::snapshot::sort::{canonicalize_parts, missing_app, out_of_order};
pub mod state {
mod private {
pub trait Sealed {}
}
pub trait SnapshotState: private::Sealed {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Unsorted;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Canonical;
impl private::Sealed for Unsorted {}
impl private::Sealed for Canonical {}
impl SnapshotState for Unsorted {}
impl SnapshotState for Canonical {}
}
pub const FORMAT_VERSION: u32 = 5;
pub const LAST_MIGRATABLE_FORMAT_VERSION: u32 = 4;
pub const SUPPORTED_FORMAT_VERSIONS: &[u32] = &[1, 2, 3, 4, 5];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotHeader {
pub project: ProjectName,
pub tag: TagName,
pub branch: Option<String>,
pub commit: CommitSha,
pub scanned_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub apps_scanned: Vec<ApplicationName>,
pub generated_by: String,
#[serde(with = "time::serde::rfc3339")]
pub generated_at: OffsetDateTime,
#[serde(default)]
pub extractor_version: String,
#[serde(default)]
pub format_version: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dep_pins: Vec<VendoredDep>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_facts: Option<HostReleaseFacts>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extraction_settings: Option<ExtractionSettingsDigest>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostReleaseFacts {
pub release_roots: NonEmpty<ApplicationName>,
pub digest: HostFactsDigest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
Hidden,
TestOnly,
}
impl Visibility {
pub fn keyword(self) -> &'static str {
match self {
Self::Public => "public",
Self::Hidden => "hidden",
Self::TestOnly => "test_only",
}
}
pub fn from_keyword(s: &str) -> Option<Self> {
match s {
"public" => Some(Self::Public),
"hidden" => Some(Self::Hidden),
"test_only" => Some(Self::TestOnly),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Module {
pub name: ModuleName,
pub visibility: Visibility,
pub behaviours: Vec<ModuleName>,
pub exports: Vec<FunArity>,
pub export_types: Vec<TypeArity>,
pub callbacks: Vec<CallbackSig>,
pub optional_callbacks: Vec<FunArity>,
pub specs: Vec<SpecSig>,
pub types: Vec<TypeDecl>,
pub opaques: Vec<TypeArity>,
pub deprecations: Vec<Deprecation>,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
with = "crate::model::serde_util::fun_arity_keyed_map"
)]
pub clause_heads: BTreeMap<FunArity, Vec<Vec<ArgShape>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<RelativePath>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app: Option<ApplicationName>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub records: Vec<RecordDecl>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub test_only_exports: Vec<TestOnlyExport>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ifdef_macros: Vec<IfdefMacro>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub variant_c_blocks: Vec<VariantCBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub versioned_machine_version: Option<VersionedMachineVersion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub wire_constants: Vec<WireConstantBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TestOnlyExport {
pub function: FunctionName,
pub arity: Arity,
pub export_line: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body_line: Option<usize>,
pub variant: TestExportVariant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TestExportVariant {
A,
B,
}
impl TestExportVariant {
pub fn as_label(self) -> &'static str {
match self {
Self::A => "a",
Self::B => "b",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s {
"a" => Some(Self::A),
"b" => Some(Self::B),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IfdefMacro {
pub name: String,
pub line: usize,
pub guard_kind: IfdefGuardKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IfdefGuardKind {
Test,
NotTest,
Other,
}
impl IfdefGuardKind {
pub fn as_label(self) -> &'static str {
match self {
Self::Test => "test",
Self::NotTest => "not_test",
Self::Other => "other",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s {
"test" => Some(Self::Test),
"not_test" => Some(Self::NotTest),
"other" => Some(Self::Other),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VariantCBlock {
pub guard: String,
pub start_line: usize,
pub end_line: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub else_line: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VersionedMachineVersion {
pub function: FunctionName,
pub arity: Arity,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<u64>,
pub provenance: Provenance,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Provenance {
Literal,
MacroBody {
macro_name: MacroName,
#[serde(default, skip_serializing_if = "Option::is_none")]
defined_in: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WireConstantBinding {
pub macro_name: MacroName,
pub value: WireValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub defined_in: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
pub enum WireValue {
U64(u64),
Bytes(Vec<u8>),
Opaque(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct VendoredDep {
pub name: DependencyName,
pub version: DependencyVersion,
pub source: VendoredDepSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VendoredDepSource {
Hex,
Git,
GitRmq,
}
impl VendoredDepSource {
pub fn as_label(self) -> &'static str {
match self {
Self::Hex => "hex",
Self::Git => "git",
Self::GitRmq => "git_rmq",
}
}
pub fn from_label(s: &str) -> Option<Self> {
match s {
"hex" => Some(Self::Hex),
"git" => Some(Self::Git),
"git_rmq" => Some(Self::GitRmq),
_ => None,
}
}
}
impl Module {
pub fn new(name: ModuleName) -> Self {
Self {
name,
visibility: Visibility::Public,
behaviours: Vec::new(),
exports: Vec::new(),
export_types: Vec::new(),
callbacks: Vec::new(),
optional_callbacks: Vec::new(),
specs: Vec::new(),
types: Vec::new(),
opaques: Vec::new(),
deprecations: Vec::new(),
clause_heads: BTreeMap::new(),
path: None,
app: None,
records: Vec::new(),
test_only_exports: Vec::new(),
ifdef_macros: Vec::new(),
variant_c_blocks: Vec::new(),
versioned_machine_version: None,
wire_constants: Vec::new(),
}
}
pub fn exports_function(&self, function: &FunctionName, arity: Arity) -> bool {
let key = FunArity {
name: function.clone(),
arity,
};
self.exports.binary_search(&key).is_ok()
}
pub fn exports_type(&self, name: &TypeName, arity: Arity) -> bool {
self.export_types.contains(&TypeArity {
name: name.clone(),
arity,
})
}
pub fn missing_clause_bodies(&self) -> Vec<FunArity> {
if self.clause_heads.is_empty() {
return Vec::new();
}
self.exports
.iter()
.filter(|fa| !self.clause_heads.contains_key(fa))
.cloned()
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct HeaderRef {
pub app: ApplicationName,
pub app_path: RelativePath,
}
impl fmt::Display for HeaderRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.app, self.app_path)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HrlFile {
pub path: RelativePath,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app: Option<ApplicationName>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub includes: Vec<HeaderRef>,
pub types: Vec<TypeDecl>,
pub opaques: Vec<TypeArity>,
pub records: Vec<RecordDecl>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub defines: Vec<MacroDecl>,
}
impl HrlFile {
pub fn new(path: RelativePath) -> Self {
Self {
path,
app: None,
includes: Vec::new(),
types: Vec::new(),
opaques: Vec::new(),
records: Vec::new(),
defines: Vec::new(),
}
}
#[must_use]
pub fn app_path(&self) -> Option<RelativePath> {
let app = self.app.as_ref()?;
let segments: Vec<&str> = self.path.as_str().split('/').collect();
let (_file, dirs) = segments.split_last()?;
let rest = match dirs.iter().position(|d| *d == app.as_str()) {
Some(root) => segments[root + 1..].join("/"),
None => self.path.as_str().to_owned(),
};
RelativePath::new(rest).ok()
}
#[must_use]
pub fn header_ref(&self) -> Option<HeaderRef> {
Some(HeaderRef {
app: self.app.clone()?,
app_path: self.app_path()?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FunArity {
pub name: FunctionName,
pub arity: Arity,
}
impl fmt::Display for FunArity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.name, self.arity)
}
}
impl FromStr for FunArity {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
let (name, arity) = split_name_arity("fun_arity", s)?;
Ok(Self {
name: name.parse()?,
arity,
})
}
}
fn split_name_arity<'a>(kind: &'static str, s: &'a str) -> Result<(&'a str, Arity), NameError> {
let invalid = || NameError::Invalid {
kind,
value: s.to_owned(),
reason: "expected 'name/arity'",
};
let (name, arity) = s.rsplit_once('/').ok_or_else(invalid)?;
let arity = arity.parse().map_err(|_| invalid())?;
Ok((name, arity))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TypeArity {
pub name: TypeName,
pub arity: Arity,
}
impl fmt::Display for TypeArity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.name, self.arity)
}
}
impl FromStr for TypeArity {
type Err = NameError;
fn from_str(s: &str) -> Result<Self, NameError> {
let (name, arity) = split_name_arity("type_arity", s)?;
Ok(Self {
name: name.parse()?,
arity,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CallbackSig {
pub name: FunctionName,
pub arity: Arity,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpecSig {
pub name: FunctionName,
pub arity: Arity,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeDecl {
pub name: TypeName,
pub arity: Arity,
pub rhs: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordDecl {
pub name: RecordName,
pub fields: Vec<RecordField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordField {
pub name: FieldName,
pub type_repr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<NormalisedDefault>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deprecation {
pub function: Option<FunctionName>,
pub arity_match: ArityMatch,
pub since: Option<TagName>,
pub replacement: Option<DeprecationReplacement>,
pub reason: Option<String>,
pub module_wide: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum ArityMatch {
Exact { arity: Arity },
Any,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeprecationReplacement {
pub function: FunctionName,
pub arity: Arity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SnapshotParts {
pub(crate) header: SnapshotHeader,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) apps: Vec<AppManifest>,
pub(crate) modules: Vec<Module>,
pub(crate) headers: Vec<HrlFile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) schemas: Vec<SchemaFile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) partials: Vec<PartialFile>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(bound = "")]
pub struct Snapshot<S: state::SnapshotState = state::Unsorted> {
#[serde(flatten)]
parts: SnapshotParts,
#[serde(skip)]
_state: PhantomData<S>,
}
impl TryFrom<SnapshotParts> for Snapshot<state::Canonical> {
type Error = SnapshotError;
fn try_from(parts: SnapshotParts) -> Result<Self, SnapshotError> {
if let Some(detail) = out_of_order(&parts) {
return Err(SnapshotError::NotCanonicalValue { detail });
}
if parts.header.format_version >= 5
&& let Some(detail) = missing_app(&parts)
{
return Err(SnapshotError::NotCanonicalValue { detail });
}
Ok(Self::from_canonical_parts(parts))
}
}
impl<'de> Deserialize<'de> for Snapshot<state::Canonical> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let parts = SnapshotParts::deserialize(deserializer)?;
Snapshot::try_from(parts).map_err(D::Error::custom)
}
}
impl Snapshot<state::Unsorted> {
pub fn from_extracted(
header: SnapshotHeader,
modules: Vec<Module>,
headers: Vec<HrlFile>,
) -> Self {
Self {
parts: SnapshotParts {
header,
apps: Vec::new(),
modules,
headers,
schemas: Vec::new(),
partials: Vec::new(),
},
_state: PhantomData,
}
}
#[must_use]
pub fn with_apps(mut self, apps: Vec<AppManifest>) -> Self {
self.parts.apps = apps;
self
}
#[must_use]
pub fn with_cuttlefish(mut self, schemas: Vec<SchemaFile>, partials: Vec<PartialFile>) -> Self {
self.parts.schemas = schemas;
self.parts.partials = partials;
self
}
#[must_use]
pub fn into_canonical(mut self) -> Snapshot<state::Canonical> {
canonicalize_parts(&mut self.parts);
Snapshot {
parts: self.parts,
_state: PhantomData,
}
}
}
impl<S: state::SnapshotState> Snapshot<S> {
pub fn header(&self) -> &SnapshotHeader {
&self.parts.header
}
pub fn modules(&self) -> &[Module] {
&self.parts.modules
}
pub fn headers(&self) -> &[HrlFile] {
&self.parts.headers
}
pub fn apps(&self) -> &[AppManifest] {
&self.parts.apps
}
pub fn schemas(&self) -> &[SchemaFile] {
&self.parts.schemas
}
pub fn partials(&self) -> &[PartialFile] {
&self.parts.partials
}
pub fn records(&self) -> impl Iterator<Item = &RecordDecl> {
self.parts
.headers
.iter()
.flat_map(|h| h.records.iter())
.chain(self.parts.modules.iter().flat_map(|m| m.records.iter()))
}
}
impl Snapshot<state::Canonical> {
pub fn lookup_export(
&self,
module: &ModuleName,
function: &FunctionName,
arity: Arity,
) -> bool {
self.module_named(module)
.is_some_and(|m| m.exports_function(function, arity))
}
pub fn module_named(&self, name: &ModuleName) -> Option<&Module> {
self.parts
.modules
.binary_search_by(|m| m.name.cmp(name))
.ok()
.map(|idx| &self.parts.modules[idx])
}
pub fn implementers_of(&self, behaviour: &ModuleName) -> Vec<&Module> {
self.parts
.modules
.iter()
.filter(|m| m.behaviours.contains(behaviour))
.collect()
}
pub(crate) fn from_canonical_parts(parts: SnapshotParts) -> Self {
Self {
parts,
_state: PhantomData,
}
}
#[must_use]
pub fn written_format_version(&self) -> u32 {
self.parts
.header
.format_version
.clamp(LAST_MIGRATABLE_FORMAT_VERSION, FORMAT_VERSION)
}
#[must_use]
pub fn unwritable_reason(&self) -> Option<String> {
if self.written_format_version() < 5 {
return None;
}
missing_app(&self.parts)
}
}
impl Default for Snapshot<state::Unsorted> {
fn default() -> Self {
Self::from_extracted(
SnapshotHeader {
project: ProjectName::new("placeholder").expect("valid"),
tag: TagName::new("v0.0.0").expect("valid"),
branch: None,
commit: CommitSha::new("0".repeat(40)).expect("valid"),
scanned_paths: Vec::new(),
apps_scanned: Vec::new(),
generated_by: format!("backhopper {}", env!("CARGO_PKG_VERSION")),
generated_at: OffsetDateTime::UNIX_EPOCH,
extractor_version: String::new(),
format_version: FORMAT_VERSION,
dep_pins: Vec::new(),
host_facts: None,
extraction_settings: None,
},
Vec::new(),
Vec::new(),
)
}
}