type_bridge_schema_codegen/
package.rs1use 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
10pub const MIGRATION_HISTORY_BUNDLE_RESOURCE: &str = "typebridge/migration-history.json";
12
13#[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 #[must_use]
50 pub const fn files(&self) -> &BTreeMap<String, Vec<u8>> {
51 &self.files
52 }
53
54 #[must_use]
56 pub fn get(&self, path: &str) -> Option<&[u8]> {
57 self.files.get(path).map(Vec::as_slice)
58 }
59
60 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}