1#![forbid(unsafe_code)]
2use std::env;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use riffdb_application::{
9 AdapterConformanceManifest, ApplicationPortabilityManifest,
10 ApplicationPortabilityManifestInput, ApplicationPortabilityValidation,
11 ApplicationReimportReceipt, InstallationArtifactKind, InstallationSymbol, PortableOmission,
12 PortableOmissionClass, PortableOmissionReason, PortableRecordClass, PortableRecordMapping,
13 PortableReimportStrategy, ReimportMappingResult, ReimportObservation,
14 ReimportObservationParameter, ReimportObservationResult,
15};
16use riffdb_contract_ir::ContractBundle;
17use riffdb_types::{
18 ApplicationExportManifestHash, CanonicalValue, DatabaseId, QueryModuleHash,
19 hash_generated_artifact,
20};
21
22struct Domain<'a> {
23 name: &'a str,
24 application_root: &'a str,
25 mappings: &'a [(&'a str, &'a str)],
26 query: &'a str,
27 parameters: &'a [(&'a str, u16)],
28 empty_collection: Option<&'a str>,
29 seed: u8,
30}
31
32const DOMAINS: [Domain<'static>; 5] = [
33 Domain {
34 name: "openfga",
35 application_root: "fixtures/adapters/operational-conformance",
36 mappings: &[("FgaTuple", "ReconstituteFgaTuples")],
37 query: "ListFgaTuples",
38 parameters: &[("store_id", 10)],
39 empty_collection: Some("tuples"),
40 seed: 1,
41 },
42 Domain {
43 name: "better-auth",
44 application_root: "fixtures/adapters/better-auth",
45 mappings: &[
46 ("User", "ReconstituteUsers"),
47 ("Account", "ReconstituteAccounts"),
48 ("Session", "ReconstituteSessions"),
49 ("VerificationToken", "ReconstituteVerificationTokens"),
50 ],
51 query: "GetSession",
52 parameters: &[("organization_id", 30), ("user_id", 31), ("session_id", 32)],
53 empty_collection: None,
54 seed: 2,
55 },
56 Domain {
57 name: "mlflow",
58 application_root: "fixtures/adapters/mlflow",
59 mappings: &[("ScheduledRun", "ReconstituteScheduledRuns")],
60 query: "DueRun",
61 parameters: &[("organization_id", 50), ("run_id", 51)],
62 empty_collection: None,
63 seed: 3,
64 },
65 Domain {
66 name: "woodpecker",
67 application_root: "fixtures/adapters/woodpecker",
68 mappings: &[("ScheduledPipeline", "ReconstituteScheduledPipelines")],
69 query: "GetPipeline",
70 parameters: &[("organization_id", 60), ("pipeline_id", 61)],
71 empty_collection: None,
72 seed: 4,
73 },
74 Domain {
75 name: "payload",
76 application_root: "fixtures/adapters/operational-conformance",
77 mappings: &[("Document", "ReconstituteDocuments")],
78 query: "ListDraftDocuments",
79 parameters: &[("site_id", 40)],
80 empty_collection: Some("documents"),
81 seed: 5,
82 },
83];
84
85fn main() {
86 let write = env::args().nth(1).as_deref() == Some("--write");
87 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
88 for domain in DOMAINS {
89 generate_domain(&root, &domain, write);
90 }
91}
92
93fn generate_domain(root: &Path, domain: &Domain<'_>, write: bool) {
94 let bundle = ContractBundle::decode(
95 &fs::read(
96 root.join(domain.application_root)
97 .join("generated/riffdb.contract.bundle"),
98 )
99 .expect("read adapter contract bundle"),
100 )
101 .expect("decode adapter contract bundle");
102 let adapter = AdapterConformanceManifest::decode_canonical(
103 &fs::read(
104 root.join("fixtures/adapters/conformance")
105 .join(domain.name)
106 .join("adapter.conformance.json"),
107 )
108 .expect("read adapter manifest"),
109 )
110 .expect("decode adapter manifest");
111 let observation_hash = empty_observation_hash(domain.empty_collection);
112 let module_hash = adapter
113 .input()
114 .artifacts
115 .iter()
116 .find(|artifact| artifact.kind() == InstallationArtifactKind::QueryModule)
117 .map(|artifact| QueryModuleHash::from_bytes(*artifact.content_hash().as_bytes()))
118 .expect("adapter query module");
119 let manifest = ApplicationPortabilityManifest::compile(ApplicationPortabilityManifestInput {
120 adapter_manifest_hash: adapter.identity(),
121 contract_lineage: bundle.lineage().clone(),
122 contract_version: bundle.contract_version(),
123 contract_bundle_hash: bundle.bundle_hash(),
124 mappings: domain
125 .mappings
126 .iter()
127 .map(|(entity, command)| {
128 PortableRecordMapping::new(
129 PortableRecordClass::Entity,
130 symbol(entity),
131 PortableReimportStrategy::reimport_command(symbol(command)),
132 )
133 })
134 .collect(),
135 omissions: vec![
136 PortableOmission::new(
137 PortableOmissionClass::Provenance,
138 None,
139 PortableOmissionReason::HistoricalRecordsNotRegenerable,
140 )
141 .expect("provenance omission"),
142 PortableOmission::new(
143 PortableOmissionClass::PublicAudit,
144 None,
145 PortableOmissionReason::HistoricalRecordsNotRegenerable,
146 )
147 .expect("audit omission"),
148 ],
149 observations: vec![
150 ReimportObservation::new_with_parameters(
151 symbol("application_observation"),
152 symbol(domain.query),
153 module_hash,
154 domain
155 .parameters
156 .iter()
157 .map(|(name, suffix)| {
158 ReimportObservationParameter::new(
159 symbol(name),
160 CanonicalValue::Uuid(application_uuid(*suffix)),
161 )
162 .expect("observation parameter")
163 })
164 .collect(),
165 observation_hash,
166 500,
167 )
168 .expect("observation"),
169 ],
170 })
171 .expect("compile portability manifest");
172 manifest
173 .validate_compiled_contract(&bundle)
174 .expect("compiled contract mapping");
175 assert_eq!(manifest.input().adapter_manifest_hash, adapter.identity());
176 assert_eq!(
177 manifest.input().contract_lineage,
178 adapter.input().contract_lineage
179 );
180 assert!(adapter.input().evolution.iter().any(|evolution| {
181 evolution.successor().version() == manifest.input().contract_version
182 && evolution.successor().bundle_hash() == manifest.input().contract_bundle_hash
183 }));
184 manifest
185 .validate_adapter_conformance(&adapter)
186 .unwrap_or_else(|error| panic!("{} adapter mapping: {error:?}", domain.name));
187
188 let outcome_hash = hash_generated_artifact(format!("{}-outcomes-v1", domain.name).as_bytes());
189 let destination = root.join("fixtures/export").join(domain.name);
190 let receipt = reimport_receipt(&manifest, domain, outcome_hash);
191 update(
192 destination.join("portability-manifest-v3.json"),
193 manifest.canonical_bytes(),
194 write,
195 );
196 let legacy_manifest_path = destination.join("portability-manifest-v2.json");
197 if legacy_manifest_path.is_file() {
198 let legacy_manifest = ApplicationPortabilityManifest::decode_canonical(
199 &fs::read(legacy_manifest_path).expect("read V2 portability manifest"),
200 )
201 .expect("decode V2 portability manifest");
202 let legacy_receipt = reimport_receipt(&legacy_manifest, domain, outcome_hash);
203 update(
204 destination.join("reimport-receipt-v2.json"),
205 legacy_receipt.canonical_bytes(),
206 write,
207 );
208 }
209 update(
210 destination.join("reimport-receipt-v3.json"),
211 receipt.canonical_bytes(),
212 write,
213 );
214}
215
216fn reimport_receipt(
217 manifest: &ApplicationPortabilityManifest,
218 domain: &Domain<'_>,
219 outcome_hash: riffdb_types::GeneratedArtifactHash,
220) -> ApplicationReimportReceipt {
221 ApplicationReimportReceipt::reconcile(
222 manifest,
223 ApplicationExportManifestHash::from_bytes(
224 *hash_generated_artifact(format!("{}-export-v1", domain.name).as_bytes()).as_bytes(),
225 ),
226 database_id(domain.seed),
227 manifest
228 .input()
229 .mappings
230 .iter()
231 .map(|mapping| {
232 ReimportMappingResult::new(
233 mapping.class(),
234 mapping.symbol().clone(),
235 2,
236 2,
237 0,
238 outcome_hash,
239 )
240 .expect("mapping result")
241 })
242 .collect(),
243 manifest
244 .input()
245 .observations
246 .iter()
247 .map(|observation| {
248 ReimportObservationResult::new(
249 observation.name().clone(),
250 observation.expected_hash(),
251 )
252 })
253 .collect(),
254 )
255 .expect("reimport receipt")
256}
257
258fn symbol(value: &str) -> InstallationSymbol {
259 InstallationSymbol::new(value).expect("fixture symbol")
260}
261
262fn database_id(seed: u8) -> DatabaseId {
263 DatabaseId::from_bytes([0, 0, 0, 0, 0, seed, 0x70, 0, 0x80, 0, 0, 0, 0, 0, 0, seed])
264 .expect("fixture database ID")
265}
266
267fn application_uuid(suffix: u16) -> [u8; 16] {
268 let mut value = [
269 0x01, 0x8f, 0x0f, 0x8b, 0x7c, 0x6d, 0x7e, 0x31, 0x8a, 0x4f, 0, 0, 0, 0, 0, 0,
270 ];
271 value[14..].copy_from_slice(&suffix.to_be_bytes());
272 value
273}
274
275fn empty_observation_hash(collection: Option<&str>) -> riffdb_types::GeneratedArtifactHash {
276 let mut bytes = b"riffdb.reimport-observation/v1\0".to_vec();
277 let outcome = if collection.is_some() {
278 "Found"
279 } else {
280 "Missing"
281 };
282 push_bytes(&mut bytes, outcome.as_bytes());
283 match collection {
284 Some(name) => {
285 bytes.extend_from_slice(&1_u32.to_be_bytes());
286 push_bytes(&mut bytes, name.as_bytes());
287 bytes.push(4); bytes.extend_from_slice(&0_u32.to_be_bytes());
289 }
290 None => bytes.extend_from_slice(&0_u32.to_be_bytes()),
291 }
292 hash_generated_artifact(&bytes)
293}
294
295fn push_bytes(target: &mut Vec<u8>, value: &[u8]) {
296 target.extend_from_slice(
297 &u32::try_from(value.len())
298 .expect("fixture value length")
299 .to_be_bytes(),
300 );
301 target.extend_from_slice(value);
302}
303
304fn update(path: PathBuf, expected: &[u8], write: bool) {
305 if write {
306 fs::create_dir_all(path.parent().expect("fixture parent")).expect("create fixture parent");
307 fs::write(&path, expected).expect("write fixture");
308 return;
309 }
310 let actual = fs::read(&path).unwrap_or_default();
311 assert_eq!(actual, expected, "stale fixture: {}", path.display());
312}