Skip to main content

type_bridge_schema_codegen/
package.rs

1use std::collections::BTreeMap;
2
3use type_bridge_contract::diagnostic::Diagnostic;
4use type_bridge_schema_migration::{
5    VerifiedMigrationHistoryBundle, encode_verified_migration_history_bundle,
6};
7
8use crate::invalid;
9
10/// Fixed language-neutral generated resource containing canonical migration history.
11pub const MIGRATION_HISTORY_BUNDLE_RESOURCE: &str = "typebridge/migration-history.json";
12
13/// An ordered in-memory package with normalized relative paths.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct GeneratedPackage {
16    files: BTreeMap<String, Vec<u8>>,
17}
18
19impl GeneratedPackage {
20    pub(crate) fn try_new(
21        files: impl IntoIterator<Item = (String, Vec<u8>)>,
22    ) -> Result<Self, Diagnostic> {
23        let mut ordered = BTreeMap::new();
24        for (path, bytes) in files {
25            if path.is_empty()
26                || path.starts_with('/')
27                || path.contains('\\')
28                || path.contains('\0')
29                || path
30                    .split('/')
31                    .any(|part| part.is_empty() || matches!(part, "." | ".."))
32            {
33                return Err(invalid(
34                    "invalid_generated_package_path",
35                    "generated package paths must be normalized relative paths",
36                ));
37            }
38            if ordered.insert(path, bytes).is_some() {
39                return Err(invalid(
40                    "duplicate_generated_package_path",
41                    "a generated package path may be emitted only once",
42                ));
43            }
44        }
45        Ok(Self { files: ordered })
46    }
47
48    /// Return files in bytewise path order.
49    #[must_use]
50    pub const fn files(&self) -> &BTreeMap<String, Vec<u8>> {
51        &self.files
52    }
53
54    /// Borrow one generated file.
55    #[must_use]
56    pub fn get(&self, path: &str) -> Option<&[u8]> {
57        self.files.get(path).map(Vec::as_slice)
58    }
59
60    /// Embed one replay-verified canonical migration-history bundle.
61    ///
62    /// The fixed path and canonical encoder prevent emitters or callers from
63    /// substituting binding-specific history bytes or an arbitrary resource.
64    pub fn with_migration_history_bundle(
65        mut self,
66        bundle: &VerifiedMigrationHistoryBundle,
67    ) -> Result<Self, Diagnostic> {
68        let bytes = encode_verified_migration_history_bundle(bundle)?;
69        match self.files.get_mut(MIGRATION_HISTORY_BUNDLE_RESOURCE) {
70            Some(placeholder) if placeholder.is_empty() => *placeholder = bytes,
71            Some(_) => {
72                return Err(invalid(
73                    "duplicate_generated_migration_history_resource",
74                    "an emitter attempted to replace the canonical migration-history resource",
75                ));
76            }
77            None => {
78                self.files
79                    .insert(MIGRATION_HISTORY_BUNDLE_RESOURCE.to_owned(), bytes);
80            }
81        }
82        Ok(self)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use type_bridge_schema_migration::MigrationHistoryGraph;
90
91    #[test]
92    fn verified_history_is_embedded_at_one_fixed_canonical_path() {
93        let graph = MigrationHistoryGraph::from_verified(std::iter::empty::<
94            type_bridge_schema_migration::VerifiedSchemaMigrationManifest,
95        >())
96        .expect("empty verified graph");
97        let bundle = VerifiedMigrationHistoryBundle::from_graph(&graph).expect("empty bundle");
98        let expected = encode_verified_migration_history_bundle(&bundle).expect("canonical bytes");
99        let package = GeneratedPackage::try_new([("src/lib.rs".to_owned(), Vec::new())])
100            .expect("fixture package")
101            .with_migration_history_bundle(&bundle)
102            .expect("embed bundle");
103        assert_eq!(
104            package.get(MIGRATION_HISTORY_BUNDLE_RESOURCE),
105            Some(expected.as_slice())
106        );
107    }
108
109    #[test]
110    fn emitter_cannot_preoccupy_the_history_authority_path() {
111        let graph = MigrationHistoryGraph::from_verified(std::iter::empty::<
112            type_bridge_schema_migration::VerifiedSchemaMigrationManifest,
113        >())
114        .expect("empty verified graph");
115        let bundle = VerifiedMigrationHistoryBundle::from_graph(&graph).expect("empty bundle");
116        let package = GeneratedPackage::try_new([(
117            MIGRATION_HISTORY_BUNDLE_RESOURCE.to_owned(),
118            b"foreign".to_vec(),
119        )])
120        .expect("fixture package");
121        let error = package
122            .with_migration_history_bundle(&bundle)
123            .expect_err("fixed history path cannot be replaced");
124        assert_eq!(
125            error.code().as_str(),
126            "duplicate_generated_migration_history_resource"
127        );
128    }
129
130    #[test]
131    fn emitter_placeholder_is_replaced_only_by_verified_canonical_history() {
132        let graph = MigrationHistoryGraph::from_verified(std::iter::empty::<
133            type_bridge_schema_migration::VerifiedSchemaMigrationManifest,
134        >())
135        .expect("empty verified graph");
136        let bundle = VerifiedMigrationHistoryBundle::from_graph(&graph).expect("empty bundle");
137        let expected = encode_verified_migration_history_bundle(&bundle).expect("canonical bytes");
138        let package =
139            GeneratedPackage::try_new([(MIGRATION_HISTORY_BUNDLE_RESOURCE.to_owned(), Vec::new())])
140                .expect("emitter placeholder")
141                .with_migration_history_bundle(&bundle)
142                .expect("verified history replaces placeholder");
143        assert_eq!(
144            package.get(MIGRATION_HISTORY_BUNDLE_RESOURCE),
145            Some(expected.as_slice())
146        );
147    }
148}