1use std::marker::PhantomData;
46
47pub trait Morphism<Src, Dst> {
54 fn name(&self) -> &'static str;
56
57 fn apply(&self, src: &Src) -> Dst;
59}
60
61pub trait ProvenMorphism<Src, Dst>: Morphism<Src, Dst> {
72 fn check_invariants(&self, src: &Src, dst: &Dst) -> Vec<String>;
74}
75
76pub struct Composed<A, B, C, M1, M2> {
82 first: M1,
83 second: M2,
84 _src: PhantomData<fn(&A) -> A>,
85 _mid: PhantomData<fn(&B) -> B>,
86 _dst: PhantomData<fn(&C) -> C>,
87}
88
89impl<A, B, C, M1, M2> Composed<A, B, C, M1, M2>
90where
91 M1: Morphism<A, B>,
92 M2: Morphism<B, C>,
93{
94 pub fn new(first: M1, second: M2) -> Self {
96 Self {
97 first,
98 second,
99 _src: PhantomData,
100 _mid: PhantomData,
101 _dst: PhantomData,
102 }
103 }
104}
105
106impl<A, B, C, M1, M2> Morphism<A, C> for Composed<A, B, C, M1, M2>
107where
108 M1: Morphism<A, B>,
109 M2: Morphism<B, C>,
110{
111 fn name(&self) -> &'static str {
112 "Composed"
116 }
117
118 fn apply(&self, src: &A) -> C {
119 let mid = self.first.apply(src);
120 self.second.apply(&mid)
121 }
122}
123
124impl<A, B, C, M1, M2> ProvenMorphism<A, C> for Composed<A, B, C, M1, M2>
125where
126 M1: ProvenMorphism<A, B>,
127 M2: ProvenMorphism<B, C>,
128{
129 fn check_invariants(&self, src: &A, dst: &C) -> Vec<String> {
130 let mid = self.first.apply(src);
135 let mut out = Vec::new();
136 for v in self.first.check_invariants(src, &mid) {
137 out.push(format!("[{}] {}", self.first.name(), v));
138 }
139 for v in self.second.check_invariants(&mid, dst) {
140 out.push(format!("[{}] {}", self.second.name(), v));
141 }
142 out
143 }
144}
145
146pub struct Identity<T>(PhantomData<fn(&T) -> T>);
149
150impl<T> Default for Identity<T> {
151 fn default() -> Self {
152 Self(PhantomData)
153 }
154}
155
156impl<T: Clone> Morphism<T, T> for Identity<T> {
157 fn name(&self) -> &'static str {
158 "Identity"
159 }
160 fn apply(&self, src: &T) -> T {
161 src.clone()
162 }
163}
164
165impl<T: Clone + PartialEq> ProvenMorphism<T, T> for Identity<T> {
166 fn check_invariants(&self, src: &T, dst: &T) -> Vec<String> {
167 if src == dst {
168 Vec::new()
169 } else {
170 vec!["identity: src != dst".to_string()]
171 }
172 }
173}
174
175#[derive(Debug, Clone)]
200pub struct ResourceInput<'a> {
201 pub resource: &'a crate::ir::IacResource,
202 pub provider: &'a crate::ir::IacProvider,
203}
204
205impl<B: crate::backend::Backend> Morphism<ResourceInput<'_>, Vec<crate::backend::GeneratedArtifact>>
206 for B
207{
208 fn name(&self) -> &'static str {
209 "Backend::generate_resource"
210 }
211
212 fn apply(&self, src: &ResourceInput<'_>) -> Vec<crate::backend::GeneratedArtifact> {
213 use crate::sexpr::ToSExpr;
214 let mut artifacts =
219 <B as crate::backend::Backend>::generate_resource(self, src.resource, src.provider)
220 .unwrap_or_default();
221
222 let source_hash = src.resource.content_hash().to_hex();
227 let platform = <B as crate::backend::Backend>::platform(self);
228 let chain = vec![
229 format!("Backend::{platform}"),
230 "generate_resource".to_string(),
231 ];
232 for a in &mut artifacts {
233 if a.source_hash.is_empty() {
234 a.source_hash.clone_from(&source_hash);
235 }
236 if a.morphism_chain.is_empty() {
237 a.morphism_chain = chain.clone();
238 }
239 }
240 artifacts
241 }
242}
243
244impl<B: crate::backend::Backend>
245 ProvenMorphism<ResourceInput<'_>, Vec<crate::backend::GeneratedArtifact>> for B
246{
247 fn check_invariants(
248 &self,
249 src: &ResourceInput<'_>,
250 dst: &Vec<crate::backend::GeneratedArtifact>,
251 ) -> Vec<String> {
252 let mut violations = Vec::new();
253
254 let rerun = <B as Morphism<_, _>>::apply(self, src);
257 if &rerun != dst {
258 violations.push(format!(
259 "backend {}: non-deterministic — re-run differs",
260 <B as crate::backend::Backend>::platform(self),
261 ));
262 }
263
264 if dst.is_empty() {
267 violations.push(format!(
268 "backend {}: empty artifact list",
269 <B as crate::backend::Backend>::platform(self),
270 ));
271 }
272
273 for a in dst {
275 if a.path.is_empty() {
276 violations.push(format!(
277 "backend {}: artifact with empty path",
278 <B as crate::backend::Backend>::platform(self),
279 ));
280 }
281 if a.content.is_empty() {
282 violations.push(format!(
283 "backend {}: artifact '{}' has empty content",
284 <B as crate::backend::Backend>::platform(self),
285 a.path,
286 ));
287 }
288 }
289
290 let mut paths: Vec<&str> = dst.iter().map(|a| a.path.as_str()).collect();
292 paths.sort_unstable();
293 for pair in paths.windows(2) {
294 if pair[0] == pair[1] {
295 violations.push(format!(
296 "backend {}: duplicate artifact path '{}'",
297 <B as crate::backend::Backend>::platform(self),
298 pair[0],
299 ));
300 }
301 }
302
303 violations
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 struct Double;
312 impl Morphism<i64, i64> for Double {
313 fn name(&self) -> &'static str {
314 "Double"
315 }
316 fn apply(&self, x: &i64) -> i64 {
317 x * 2
318 }
319 }
320 impl ProvenMorphism<i64, i64> for Double {
321 fn check_invariants(&self, src: &i64, dst: &i64) -> Vec<String> {
322 if *dst == src * 2 {
323 Vec::new()
324 } else {
325 vec!["double: dst != 2 * src".into()]
326 }
327 }
328 }
329
330 struct AddOne;
331 impl Morphism<i64, i64> for AddOne {
332 fn name(&self) -> &'static str {
333 "AddOne"
334 }
335 fn apply(&self, x: &i64) -> i64 {
336 x + 1
337 }
338 }
339 impl ProvenMorphism<i64, i64> for AddOne {
340 fn check_invariants(&self, src: &i64, dst: &i64) -> Vec<String> {
341 if *dst == src + 1 {
342 Vec::new()
343 } else {
344 vec!["addone: dst != src + 1".into()]
345 }
346 }
347 }
348
349 #[test]
350 fn apply_composes_left_to_right() {
351 let c = Composed::new(Double, AddOne);
352 assert_eq!(c.apply(&3), 7); }
354
355 #[test]
356 fn proof_composes() {
357 let c = Composed::new(Double, AddOne);
358 let src = 5_i64;
359 let dst = c.apply(&src);
360 let violations = c.check_invariants(&src, &dst);
361 assert!(
362 violations.is_empty(),
363 "valid composition should yield zero violations: {violations:?}",
364 );
365 }
366
367 #[test]
368 fn proof_composes_identifies_source_of_violation() {
369 struct BadDouble;
370 impl Morphism<i64, i64> for BadDouble {
371 fn name(&self) -> &'static str {
372 "BadDouble"
373 }
374 fn apply(&self, x: &i64) -> i64 {
375 x * 3 }
377 }
378 impl ProvenMorphism<i64, i64> for BadDouble {
379 fn check_invariants(&self, src: &i64, dst: &i64) -> Vec<String> {
380 if *dst == src * 2 {
381 Vec::new()
382 } else {
383 vec!["double: dst != 2 * src".into()]
384 }
385 }
386 }
387
388 let c = Composed::new(BadDouble, AddOne);
389 let src = 5_i64;
390 let dst = c.apply(&src);
391 let violations = c.check_invariants(&src, &dst);
392 assert_eq!(violations.len(), 1);
393 assert!(violations[0].starts_with("[BadDouble]"));
394 }
395
396 #[test]
397 fn identity_name_and_apply() {
398 let id = Identity::<i64>::default();
399 assert_eq!(id.name(), "Identity");
400 assert_eq!(id.apply(&42), 42);
401 }
402
403 #[test]
404 fn identity_proves_itself() {
405 let id = Identity::<i64>::default();
406 assert!(id.check_invariants(&42, &42).is_empty());
407 }
408
409 #[test]
410 fn identity_is_right_unit_of_composition() {
411 let c = Composed::new(Double, Identity::<i64>::default());
412 assert_eq!(c.apply(&5), 10);
413 assert!(c.check_invariants(&5, &10).is_empty());
414 }
415
416 #[test]
417 fn identity_is_left_unit_of_composition() {
418 let c = Composed::new(Identity::<i64>::default(), Double);
419 assert_eq!(c.apply(&5), 10);
420 assert!(c.check_invariants(&5, &10).is_empty());
421 }
422
423 #[test]
424 fn triple_composition_proof_chain() {
425 let c = Composed::new(Composed::new(Double, AddOne), Double);
426 let src = 3_i64;
427 let dst = c.apply(&src);
428 assert_eq!(dst, 14); assert!(c.check_invariants(&src, &dst).is_empty());
430 }
431
432 #[test]
433 fn composition_apply_is_deterministic() {
434 let c = Composed::new(Double, AddOne);
435 let a = c.apply(&7);
436 let b = c.apply(&7);
437 assert_eq!(a, b);
438 }
439
440 use crate::backend::{ArtifactKind, Backend, GeneratedArtifact, NamingConvention};
443 use crate::error::IacForgeError;
444 use crate::ir::{IacDataSource, IacProvider, IacResource};
445
446 struct GoodBackend;
447 struct GoodNaming;
448 impl NamingConvention for GoodNaming {
449 fn resource_type_name(&self, r: &str, p: &str) -> String {
450 format!("{p}_{r}")
451 }
452 fn file_name(&self, r: &str, _k: &ArtifactKind) -> String {
453 format!("{r}.out")
454 }
455 fn field_name(&self, n: &str) -> String {
456 n.to_string()
457 }
458 }
459 impl Backend for GoodBackend {
460 fn platform(&self) -> &str {
461 "good"
462 }
463 fn naming(&self) -> &dyn NamingConvention {
464 &GoodNaming
465 }
466 fn generate_resource(
467 &self,
468 _r: &IacResource,
469 _p: &IacProvider,
470 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
471 Ok(vec![GeneratedArtifact {
472 path: "main.out".into(),
473 content: "body".into(),
474 kind: ArtifactKind::Resource,
475 source_hash: String::new(),
476 morphism_chain: Vec::new(),
477 }])
478 }
479 fn generate_data_source(
480 &self,
481 _d: &IacDataSource,
482 _p: &IacProvider,
483 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
484 Ok(vec![])
485 }
486 fn generate_provider(
487 &self,
488 _p: &IacProvider,
489 _r: &[IacResource],
490 _d: &[IacDataSource],
491 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
492 Ok(vec![])
493 }
494 fn generate_test(
495 &self,
496 _r: &IacResource,
497 _p: &IacProvider,
498 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
499 Ok(vec![])
500 }
501 }
502
503 fn sample_input() -> (crate::ir::IacResource, crate::ir::IacProvider) {
504 (
505 crate::testing::test_resource("widget"),
506 crate::testing::test_provider("acme"),
507 )
508 }
509
510 #[test]
511 fn backend_is_a_morphism() {
512 let (r, p) = sample_input();
513 let input = ResourceInput {
514 resource: &r,
515 provider: &p,
516 };
517 let out = <GoodBackend as Morphism<_, _>>::apply(&GoodBackend, &input);
518 assert_eq!(out.len(), 1);
519 assert_eq!(out[0].path, "main.out");
520 }
521
522 #[test]
523 fn backend_morphism_proofs_hold_on_good_backend() {
524 let (r, p) = sample_input();
525 let input = ResourceInput {
526 resource: &r,
527 provider: &p,
528 };
529 let out = <GoodBackend as Morphism<_, _>>::apply(&GoodBackend, &input);
530 let violations =
531 <GoodBackend as ProvenMorphism<_, _>>::check_invariants(&GoodBackend, &input, &out);
532 assert!(violations.is_empty(), "violations: {violations:?}");
533 }
534
535 struct DupBackend;
537 impl Backend for DupBackend {
538 fn platform(&self) -> &str {
539 "dup"
540 }
541 fn naming(&self) -> &dyn NamingConvention {
542 &GoodNaming
543 }
544 fn generate_resource(
545 &self,
546 _r: &IacResource,
547 _p: &IacProvider,
548 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
549 Ok(vec![
550 GeneratedArtifact {
551 path: "dup.out".into(),
552 content: "a".into(),
553 kind: ArtifactKind::Resource,
554 source_hash: String::new(),
555 morphism_chain: Vec::new(),
556 },
557 GeneratedArtifact {
558 path: "dup.out".into(),
559 content: "b".into(),
560 kind: ArtifactKind::Resource,
561 source_hash: String::new(),
562 morphism_chain: Vec::new(),
563 },
564 ])
565 }
566 fn generate_data_source(
567 &self,
568 _d: &IacDataSource,
569 _p: &IacProvider,
570 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
571 Ok(vec![])
572 }
573 fn generate_provider(
574 &self,
575 _p: &IacProvider,
576 _r: &[IacResource],
577 _d: &[IacDataSource],
578 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
579 Ok(vec![])
580 }
581 fn generate_test(
582 &self,
583 _r: &IacResource,
584 _p: &IacProvider,
585 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
586 Ok(vec![])
587 }
588 }
589
590 #[test]
591 fn backend_morphism_catches_duplicate_paths() {
592 let (r, p) = sample_input();
593 let input = ResourceInput {
594 resource: &r,
595 provider: &p,
596 };
597 let out = <DupBackend as Morphism<_, _>>::apply(&DupBackend, &input);
598 let violations =
599 <DupBackend as ProvenMorphism<_, _>>::check_invariants(&DupBackend, &input, &out);
600 assert!(
601 violations
602 .iter()
603 .any(|v| v.contains("duplicate artifact path")),
604 "expected duplicate-path violation: {violations:?}",
605 );
606 }
607
608 struct EmptyBackend;
610 impl Backend for EmptyBackend {
611 fn platform(&self) -> &str {
612 "empty"
613 }
614 fn naming(&self) -> &dyn NamingConvention {
615 &GoodNaming
616 }
617 fn generate_resource(
618 &self,
619 _r: &IacResource,
620 _p: &IacProvider,
621 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
622 Ok(vec![])
623 }
624 fn generate_data_source(
625 &self,
626 _d: &IacDataSource,
627 _p: &IacProvider,
628 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
629 Ok(vec![])
630 }
631 fn generate_provider(
632 &self,
633 _p: &IacProvider,
634 _r: &[IacResource],
635 _d: &[IacDataSource],
636 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
637 Ok(vec![])
638 }
639 fn generate_test(
640 &self,
641 _r: &IacResource,
642 _p: &IacProvider,
643 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
644 Ok(vec![])
645 }
646 }
647
648 #[test]
651 fn backend_morphism_populates_source_hash() {
652 use crate::sexpr::ToSExpr;
653 let (r, p) = sample_input();
654 let input = ResourceInput {
655 resource: &r,
656 provider: &p,
657 };
658 let out = <GoodBackend as Morphism<_, _>>::apply(&GoodBackend, &input);
659 assert_eq!(out.len(), 1);
660 let expected_hash = r.content_hash().to_hex();
661 assert_eq!(out[0].source_hash, expected_hash);
662 assert!(out[0].has_provenance());
663 }
664
665 #[test]
666 fn backend_morphism_populates_morphism_chain() {
667 let (r, p) = sample_input();
668 let input = ResourceInput {
669 resource: &r,
670 provider: &p,
671 };
672 let out = <GoodBackend as Morphism<_, _>>::apply(&GoodBackend, &input);
673 assert_eq!(out[0].morphism_chain.len(), 2);
674 assert_eq!(out[0].morphism_chain[0], "Backend::good");
675 assert_eq!(out[0].morphism_chain[1], "generate_resource");
676 }
677
678 #[test]
679 fn backend_morphism_respects_pre_set_provenance() {
680 struct PreSetBackend;
683 impl Backend for PreSetBackend {
684 fn platform(&self) -> &str {
685 "preset"
686 }
687 fn naming(&self) -> &dyn NamingConvention {
688 &GoodNaming
689 }
690 fn generate_resource(
691 &self,
692 _r: &IacResource,
693 _p: &IacProvider,
694 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
695 Ok(vec![GeneratedArtifact {
696 path: "main.out".into(),
697 content: "body".into(),
698 kind: ArtifactKind::Resource,
699 source_hash: "manually-set".into(),
700 morphism_chain: vec!["custom".into()],
701 }])
702 }
703 fn generate_data_source(
704 &self,
705 _d: &IacDataSource,
706 _p: &IacProvider,
707 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
708 Ok(vec![])
709 }
710 fn generate_provider(
711 &self,
712 _p: &IacProvider,
713 _r: &[IacResource],
714 _d: &[IacDataSource],
715 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
716 Ok(vec![])
717 }
718 fn generate_test(
719 &self,
720 _r: &IacResource,
721 _p: &IacProvider,
722 ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
723 Ok(vec![])
724 }
725 }
726
727 let (r, p) = sample_input();
728 let input = ResourceInput {
729 resource: &r,
730 provider: &p,
731 };
732 let out = <PreSetBackend as Morphism<_, _>>::apply(&PreSetBackend, &input);
733 assert_eq!(out[0].source_hash, "manually-set");
734 assert_eq!(out[0].morphism_chain, vec!["custom".to_string()]);
735 }
736
737 #[test]
738 fn backend_morphism_source_hash_is_deterministic() {
739 let (r, p) = sample_input();
741 let input = ResourceInput {
742 resource: &r,
743 provider: &p,
744 };
745 let a = <GoodBackend as Morphism<_, _>>::apply(&GoodBackend, &input);
746 let b = <GoodBackend as Morphism<_, _>>::apply(&GoodBackend, &input);
747 assert_eq!(a[0].source_hash, b[0].source_hash);
748 }
749
750 #[test]
751 fn backend_morphism_different_resources_different_hashes() {
752 let p = crate::testing::test_provider("acme");
753 let r1 = crate::testing::test_resource("widget");
754 let r2 = crate::testing::test_resource("gadget");
755 let out1 = <GoodBackend as Morphism<_, _>>::apply(
756 &GoodBackend,
757 &ResourceInput {
758 resource: &r1,
759 provider: &p,
760 },
761 );
762 let out2 = <GoodBackend as Morphism<_, _>>::apply(
763 &GoodBackend,
764 &ResourceInput {
765 resource: &r2,
766 provider: &p,
767 },
768 );
769 assert_ne!(out1[0].source_hash, out2[0].source_hash);
770 }
771
772 #[test]
773 fn backend_morphism_catches_empty_artifact_list() {
774 let (r, p) = sample_input();
775 let input = ResourceInput {
776 resource: &r,
777 provider: &p,
778 };
779 let out = <EmptyBackend as Morphism<_, _>>::apply(&EmptyBackend, &input);
780 let violations =
781 <EmptyBackend as ProvenMorphism<_, _>>::check_invariants(&EmptyBackend, &input, &out);
782 assert!(
783 violations.iter().any(|v| v.contains("empty artifact list")),
784 "expected empty-list violation: {violations:?}",
785 );
786 }
787}