1use std::collections::BTreeMap;
13use std::fmt;
14use std::marker::PhantomData;
15use std::str::FromStr;
16
17use serde::de::{Deserializer, Error as _};
18use serde::{Deserialize, Serialize};
19use time::OffsetDateTime;
20
21use crate::compat::arg_shape::ArgShape;
22use crate::errors::{NameError, SnapshotError};
23use crate::model::app_manifest::AppManifest;
24use crate::model::macro_decl::MacroDecl;
25use crate::model::names::{
26 ApplicationName, Arity, CommitSha, DependencyName, DependencyVersion, ExtractionSettingsDigest,
27 FieldName, FunctionName, HostFactsDigest, MacroName, ModuleName, ProjectName, RecordName,
28 RelativePath, TagName, TypeName,
29};
30use crate::model::non_empty::NonEmpty;
31use crate::model::normalised::NormalisedDefault;
32use crate::model::schema_files::{PartialFile, SchemaFile};
33use crate::snapshot::sort::{canonicalize_parts, missing_app, out_of_order};
34
35pub mod state {
36 mod private {
37 pub trait Sealed {}
38 }
39
40 pub trait SnapshotState: private::Sealed {}
43
44 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45 pub struct Unsorted;
46
47 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48 pub struct Canonical;
49
50 impl private::Sealed for Unsorted {}
51 impl private::Sealed for Canonical {}
52 impl SnapshotState for Unsorted {}
53 impl SnapshotState for Canonical {}
54}
55
56pub const FORMAT_VERSION: u32 = 5;
57
58pub const LAST_MIGRATABLE_FORMAT_VERSION: u32 = 4;
61
62pub const SUPPORTED_FORMAT_VERSIONS: &[u32] = &[1, 2, 3, 4, 5];
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct SnapshotHeader {
66 pub project: ProjectName,
67 pub tag: TagName,
68 pub branch: Option<String>,
69 pub commit: CommitSha,
70 pub scanned_paths: Vec<String>,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
74 pub apps_scanned: Vec<ApplicationName>,
75 pub generated_by: String,
76 #[serde(with = "time::serde::rfc3339")]
77 pub generated_at: OffsetDateTime,
78 #[serde(default)]
82 pub extractor_version: String,
83 #[serde(default)]
86 pub format_version: u32,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub dep_pins: Vec<VendoredDep>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub host_facts: Option<HostReleaseFacts>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub extraction_settings: Option<ExtractionSettingsDigest>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct HostReleaseFacts {
106 pub release_roots: NonEmpty<ApplicationName>,
107 pub digest: HostFactsDigest,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum Visibility {
113 Public,
114 Hidden,
115 TestOnly,
116}
117
118impl Visibility {
119 pub fn keyword(self) -> &'static str {
120 match self {
121 Self::Public => "public",
122 Self::Hidden => "hidden",
123 Self::TestOnly => "test_only",
124 }
125 }
126
127 pub fn from_keyword(s: &str) -> Option<Self> {
128 match s {
129 "public" => Some(Self::Public),
130 "hidden" => Some(Self::Hidden),
131 "test_only" => Some(Self::TestOnly),
132 _ => None,
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct Module {
139 pub name: ModuleName,
140 pub visibility: Visibility,
141 pub behaviours: Vec<ModuleName>,
142 pub exports: Vec<FunArity>,
143 pub export_types: Vec<TypeArity>,
144 pub callbacks: Vec<CallbackSig>,
145 pub optional_callbacks: Vec<FunArity>,
146 pub specs: Vec<SpecSig>,
147 pub types: Vec<TypeDecl>,
148 pub opaques: Vec<TypeArity>,
149 pub deprecations: Vec<Deprecation>,
150 #[serde(
154 default,
155 skip_serializing_if = "BTreeMap::is_empty",
156 with = "crate::model::serde_util::fun_arity_keyed_map"
157 )]
158 pub clause_heads: BTreeMap<FunArity, Vec<Vec<ArgShape>>>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub path: Option<RelativePath>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub app: Option<ApplicationName>,
167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub records: Vec<RecordDecl>,
171 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub test_only_exports: Vec<TestOnlyExport>,
177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
183 pub ifdef_macros: Vec<IfdefMacro>,
184 #[serde(default, skip_serializing_if = "Vec::is_empty")]
188 pub variant_c_blocks: Vec<VariantCBlock>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub versioned_machine_version: Option<VersionedMachineVersion>,
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 pub wire_constants: Vec<WireConstantBinding>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
205pub struct TestOnlyExport {
206 pub function: FunctionName,
207 pub arity: Arity,
208 pub export_line: usize,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub body_line: Option<usize>,
216 pub variant: TestExportVariant,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
220#[serde(rename_all = "snake_case")]
221pub enum TestExportVariant {
222 A,
226 B,
229}
230
231impl TestExportVariant {
232 pub fn as_label(self) -> &'static str {
233 match self {
234 Self::A => "a",
235 Self::B => "b",
236 }
237 }
238
239 pub fn from_label(s: &str) -> Option<Self> {
240 match s {
241 "a" => Some(Self::A),
242 "b" => Some(Self::B),
243 _ => None,
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
249pub struct IfdefMacro {
250 pub name: String,
251 pub line: usize,
253 pub guard_kind: IfdefGuardKind,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum IfdefGuardKind {
259 Test,
261 NotTest,
263 Other,
265}
266
267impl IfdefGuardKind {
268 pub fn as_label(self) -> &'static str {
269 match self {
270 Self::Test => "test",
271 Self::NotTest => "not_test",
272 Self::Other => "other",
273 }
274 }
275
276 pub fn from_label(s: &str) -> Option<Self> {
277 match s {
278 "test" => Some(Self::Test),
279 "not_test" => Some(Self::NotTest),
280 "other" => Some(Self::Other),
281 _ => None,
282 }
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
287pub struct VariantCBlock {
288 pub guard: String,
290 pub start_line: usize,
292 pub end_line: usize,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub else_line: Option<usize>,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
304pub struct VersionedMachineVersion {
305 pub function: FunctionName,
306 pub arity: Arity,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub value: Option<u64>,
312 pub provenance: Provenance,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
318#[serde(rename_all = "snake_case", tag = "kind")]
319pub enum Provenance {
320 Literal,
322 MacroBody {
326 macro_name: MacroName,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 defined_in: Option<String>,
329 },
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
336pub struct WireConstantBinding {
337 pub macro_name: MacroName,
338 pub value: WireValue,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub defined_in: Option<String>,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
348#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
349pub enum WireValue {
350 U64(u64),
352 Bytes(Vec<u8>),
357 Opaque(String),
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
364pub struct VendoredDep {
365 pub name: DependencyName,
366 pub version: DependencyVersion,
367 pub source: VendoredDepSource,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum VendoredDepSource {
375 Hex,
377 Git,
379 GitRmq,
381}
382
383impl VendoredDepSource {
384 pub fn as_label(self) -> &'static str {
385 match self {
386 Self::Hex => "hex",
387 Self::Git => "git",
388 Self::GitRmq => "git_rmq",
389 }
390 }
391
392 pub fn from_label(s: &str) -> Option<Self> {
393 match s {
394 "hex" => Some(Self::Hex),
395 "git" => Some(Self::Git),
396 "git_rmq" => Some(Self::GitRmq),
397 _ => None,
398 }
399 }
400}
401
402impl Module {
403 pub fn new(name: ModuleName) -> Self {
404 Self {
405 name,
406 visibility: Visibility::Public,
407 behaviours: Vec::new(),
408 exports: Vec::new(),
409 export_types: Vec::new(),
410 callbacks: Vec::new(),
411 optional_callbacks: Vec::new(),
412 specs: Vec::new(),
413 types: Vec::new(),
414 opaques: Vec::new(),
415 deprecations: Vec::new(),
416 clause_heads: BTreeMap::new(),
417 path: None,
418 app: None,
419 records: Vec::new(),
420 test_only_exports: Vec::new(),
421 ifdef_macros: Vec::new(),
422 variant_c_blocks: Vec::new(),
423 versioned_machine_version: None,
424 wire_constants: Vec::new(),
425 }
426 }
427
428 pub fn exports_function(&self, function: &FunctionName, arity: Arity) -> bool {
431 let key = FunArity {
432 name: function.clone(),
433 arity,
434 };
435 self.exports.binary_search(&key).is_ok()
436 }
437
438 pub fn exports_type(&self, name: &TypeName, arity: Arity) -> bool {
440 self.export_types.contains(&TypeArity {
441 name: name.clone(),
442 arity,
443 })
444 }
445
446 pub fn missing_clause_bodies(&self) -> Vec<FunArity> {
450 if self.clause_heads.is_empty() {
451 return Vec::new();
452 }
453 self.exports
454 .iter()
455 .filter(|fa| !self.clause_heads.contains_key(fa))
456 .cloned()
457 .collect()
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
462pub struct HeaderRef {
463 pub app: ApplicationName,
464 pub app_path: RelativePath,
465}
466
467impl fmt::Display for HeaderRef {
468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469 write!(f, "{}/{}", self.app, self.app_path)
470 }
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474pub struct HrlFile {
475 pub path: RelativePath,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub app: Option<ApplicationName>,
479 #[serde(default, skip_serializing_if = "Vec::is_empty")]
482 pub includes: Vec<HeaderRef>,
483 pub types: Vec<TypeDecl>,
484 pub opaques: Vec<TypeArity>,
485 pub records: Vec<RecordDecl>,
486 #[serde(default, skip_serializing_if = "Vec::is_empty")]
489 pub defines: Vec<MacroDecl>,
490}
491
492impl HrlFile {
493 pub fn new(path: RelativePath) -> Self {
494 Self {
495 path,
496 app: None,
497 includes: Vec::new(),
498 types: Vec::new(),
499 opaques: Vec::new(),
500 records: Vec::new(),
501 defines: Vec::new(),
502 }
503 }
504
505 #[must_use]
510 pub fn app_path(&self) -> Option<RelativePath> {
511 let app = self.app.as_ref()?;
512 let segments: Vec<&str> = self.path.as_str().split('/').collect();
513 let (_file, dirs) = segments.split_last()?;
514 let rest = match dirs.iter().position(|d| *d == app.as_str()) {
515 Some(root) => segments[root + 1..].join("/"),
516 None => self.path.as_str().to_owned(),
517 };
518 RelativePath::new(rest).ok()
519 }
520
521 #[must_use]
522 pub fn header_ref(&self) -> Option<HeaderRef> {
523 Some(HeaderRef {
524 app: self.app.clone()?,
525 app_path: self.app_path()?,
526 })
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
531pub struct FunArity {
532 pub name: FunctionName,
533 pub arity: Arity,
534}
535
536impl fmt::Display for FunArity {
537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538 write!(f, "{}/{}", self.name, self.arity)
539 }
540}
541
542impl FromStr for FunArity {
543 type Err = NameError;
544
545 fn from_str(s: &str) -> Result<Self, NameError> {
546 let (name, arity) = split_name_arity("fun_arity", s)?;
547 Ok(Self {
548 name: name.parse()?,
549 arity,
550 })
551 }
552}
553
554fn split_name_arity<'a>(kind: &'static str, s: &'a str) -> Result<(&'a str, Arity), NameError> {
557 let invalid = || NameError::Invalid {
558 kind,
559 value: s.to_owned(),
560 reason: "expected 'name/arity'",
561 };
562 let (name, arity) = s.rsplit_once('/').ok_or_else(invalid)?;
563 let arity = arity.parse().map_err(|_| invalid())?;
564 Ok((name, arity))
565}
566
567#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
568pub struct TypeArity {
569 pub name: TypeName,
570 pub arity: Arity,
571}
572
573impl fmt::Display for TypeArity {
574 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575 write!(f, "{}/{}", self.name, self.arity)
576 }
577}
578
579impl FromStr for TypeArity {
580 type Err = NameError;
581
582 fn from_str(s: &str) -> Result<Self, NameError> {
583 let (name, arity) = split_name_arity("type_arity", s)?;
584 Ok(Self {
585 name: name.parse()?,
586 arity,
587 })
588 }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
592pub struct CallbackSig {
593 pub name: FunctionName,
594 pub arity: Arity,
595 pub signature: String,
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
599pub struct SpecSig {
600 pub name: FunctionName,
601 pub arity: Arity,
602 pub signature: String,
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
606pub struct TypeDecl {
607 pub name: TypeName,
608 pub arity: Arity,
609 pub rhs: String,
610}
611
612#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
613pub struct RecordDecl {
614 pub name: RecordName,
615 pub fields: Vec<RecordField>,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct RecordField {
620 pub name: FieldName,
621 pub type_repr: Option<String>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub default: Option<NormalisedDefault>,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
630pub struct Deprecation {
631 pub function: Option<FunctionName>,
632 pub arity_match: ArityMatch,
633 pub since: Option<TagName>,
634 pub replacement: Option<DeprecationReplacement>,
635 pub reason: Option<String>,
636 pub module_wide: bool,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
640#[serde(rename_all = "snake_case", tag = "kind")]
641pub enum ArityMatch {
642 Exact { arity: Arity },
643 Any,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647pub struct DeprecationReplacement {
648 pub function: FunctionName,
649 pub arity: Arity,
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
653pub(crate) struct SnapshotParts {
654 pub(crate) header: SnapshotHeader,
655 #[serde(default, skip_serializing_if = "Vec::is_empty")]
656 pub(crate) apps: Vec<AppManifest>,
657 pub(crate) modules: Vec<Module>,
658 pub(crate) headers: Vec<HrlFile>,
659 #[serde(default, skip_serializing_if = "Vec::is_empty")]
660 pub(crate) schemas: Vec<SchemaFile>,
661 #[serde(default, skip_serializing_if = "Vec::is_empty")]
662 pub(crate) partials: Vec<PartialFile>,
663}
664
665#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
666#[serde(bound = "")]
667pub struct Snapshot<S: state::SnapshotState = state::Unsorted> {
668 #[serde(flatten)]
669 parts: SnapshotParts,
670 #[serde(skip)]
671 _state: PhantomData<S>,
672}
673
674impl TryFrom<SnapshotParts> for Snapshot<state::Canonical> {
675 type Error = SnapshotError;
676
677 fn try_from(parts: SnapshotParts) -> Result<Self, SnapshotError> {
678 if let Some(detail) = out_of_order(&parts) {
679 return Err(SnapshotError::NotCanonicalValue { detail });
680 }
681 if parts.header.format_version >= 5
682 && let Some(detail) = missing_app(&parts)
683 {
684 return Err(SnapshotError::NotCanonicalValue { detail });
685 }
686 Ok(Self::from_canonical_parts(parts))
687 }
688}
689
690impl<'de> Deserialize<'de> for Snapshot<state::Canonical> {
691 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
692 where
693 D: Deserializer<'de>,
694 {
695 let parts = SnapshotParts::deserialize(deserializer)?;
696 Snapshot::try_from(parts).map_err(D::Error::custom)
697 }
698}
699
700impl Snapshot<state::Unsorted> {
701 pub fn from_extracted(
702 header: SnapshotHeader,
703 modules: Vec<Module>,
704 headers: Vec<HrlFile>,
705 ) -> Self {
706 Self {
707 parts: SnapshotParts {
708 header,
709 apps: Vec::new(),
710 modules,
711 headers,
712 schemas: Vec::new(),
713 partials: Vec::new(),
714 },
715 _state: PhantomData,
716 }
717 }
718
719 #[must_use]
720 pub fn with_apps(mut self, apps: Vec<AppManifest>) -> Self {
721 self.parts.apps = apps;
722 self
723 }
724
725 #[must_use]
726 pub fn with_cuttlefish(mut self, schemas: Vec<SchemaFile>, partials: Vec<PartialFile>) -> Self {
727 self.parts.schemas = schemas;
728 self.parts.partials = partials;
729 self
730 }
731
732 #[must_use]
733 pub fn into_canonical(mut self) -> Snapshot<state::Canonical> {
734 canonicalize_parts(&mut self.parts);
735 Snapshot {
736 parts: self.parts,
737 _state: PhantomData,
738 }
739 }
740}
741
742impl<S: state::SnapshotState> Snapshot<S> {
743 pub fn header(&self) -> &SnapshotHeader {
744 &self.parts.header
745 }
746
747 pub fn modules(&self) -> &[Module] {
748 &self.parts.modules
749 }
750
751 pub fn headers(&self) -> &[HrlFile] {
752 &self.parts.headers
753 }
754
755 pub fn apps(&self) -> &[AppManifest] {
756 &self.parts.apps
757 }
758
759 pub fn schemas(&self) -> &[SchemaFile] {
760 &self.parts.schemas
761 }
762
763 pub fn partials(&self) -> &[PartialFile] {
764 &self.parts.partials
765 }
766
767 pub fn records(&self) -> impl Iterator<Item = &RecordDecl> {
769 self.parts
770 .headers
771 .iter()
772 .flat_map(|h| h.records.iter())
773 .chain(self.parts.modules.iter().flat_map(|m| m.records.iter()))
774 }
775}
776
777impl Snapshot<state::Canonical> {
778 pub fn lookup_export(
781 &self,
782 module: &ModuleName,
783 function: &FunctionName,
784 arity: Arity,
785 ) -> bool {
786 self.module_named(module)
787 .is_some_and(|m| m.exports_function(function, arity))
788 }
789
790 pub fn module_named(&self, name: &ModuleName) -> Option<&Module> {
793 self.parts
794 .modules
795 .binary_search_by(|m| m.name.cmp(name))
796 .ok()
797 .map(|idx| &self.parts.modules[idx])
798 }
799
800 pub fn implementers_of(&self, behaviour: &ModuleName) -> Vec<&Module> {
805 self.parts
806 .modules
807 .iter()
808 .filter(|m| m.behaviours.contains(behaviour))
809 .collect()
810 }
811
812 pub(crate) fn from_canonical_parts(parts: SnapshotParts) -> Self {
813 Self {
814 parts,
815 _state: PhantomData,
816 }
817 }
818
819 #[must_use]
820 pub fn written_format_version(&self) -> u32 {
821 self.parts
822 .header
823 .format_version
824 .clamp(LAST_MIGRATABLE_FORMAT_VERSION, FORMAT_VERSION)
825 }
826
827 #[must_use]
828 pub fn unwritable_reason(&self) -> Option<String> {
829 if self.written_format_version() < 5 {
830 return None;
831 }
832 missing_app(&self.parts)
833 }
834}
835
836impl Default for Snapshot<state::Unsorted> {
837 fn default() -> Self {
838 Self::from_extracted(
839 SnapshotHeader {
840 project: ProjectName::new("placeholder").expect("valid"),
841 tag: TagName::new("v0.0.0").expect("valid"),
842 branch: None,
843 commit: CommitSha::new("0".repeat(40)).expect("valid"),
844 scanned_paths: Vec::new(),
845 apps_scanned: Vec::new(),
846 generated_by: format!("backhopper {}", env!("CARGO_PKG_VERSION")),
847 generated_at: OffsetDateTime::UNIX_EPOCH,
848 extractor_version: String::new(),
849 format_version: FORMAT_VERSION,
850 dep_pins: Vec::new(),
851 host_facts: None,
852 extraction_settings: None,
853 },
854 Vec::new(),
855 Vec::new(),
856 )
857 }
858}