Skip to main content

iac_forge/
backend.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::IacForgeError;
4use crate::ir::{IacAction, IacDataSource, IacProvider, IacResource};
5
6/// Kind of generated artifact.
7#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
8#[non_exhaustive]
9pub enum ArtifactKind {
10    #[default]
11    Resource,
12    DataSource,
13    Provider,
14    Test,
15    Schema,
16    /// Type signature companion file (e.g., RBS alongside a Ruby types file,
17    /// `.d.ts` alongside a `.js`).
18    Signature,
19    Module,
20    Metadata,
21    /// Reconciler / state-machine code that drives a resource between
22    /// its declared spec and observed cluster state. Emitted by the
23    /// Crossplane backend (Go controllers implementing the
24    /// `crossplane-runtime` ExternalClient interface) and any future
25    /// backend that needs a runtime control loop alongside the schema.
26    Controller,
27    /// `ProviderConfig` CRD authoring surface — the typed contract a
28    /// cluster admin uses to plug credentials / endpoints / per-tenant
29    /// settings into a provider. Distinct from `Provider` (which is the
30    /// provider-level registration code) because `ProviderConfig` is
31    /// authored per-cluster, not per-codegen-run, and follows a
32    /// platform-specific convention (e.g., Crossplane's `ProviderConfig`
33    /// kind under `<group>.crossplane.io`).
34    ProviderConfig,
35    /// Helm chart deliverable — Chart.yaml, templates/, values.yaml,
36    /// and any chart-level RBAC / namespacing scaffolding. Emitted by
37    /// backends that ship their generated artifacts as a Helm chart
38    /// (Crossplane provider chart, Helm-rendered IaC, etc.).
39    HelmChart,
40}
41
42impl std::fmt::Display for ArtifactKind {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Resource => write!(f, "resource"),
46            Self::DataSource => write!(f, "data_source"),
47            Self::Provider => write!(f, "provider"),
48            Self::Test => write!(f, "test"),
49            Self::Schema => write!(f, "schema"),
50            Self::Signature => write!(f, "signature"),
51            Self::Module => write!(f, "module"),
52            Self::Metadata => write!(f, "metadata"),
53            Self::Controller => write!(f, "controller"),
54            Self::ProviderConfig => write!(f, "provider_config"),
55            Self::HelmChart => write!(f, "helm_chart"),
56        }
57    }
58}
59
60impl std::str::FromStr for ArtifactKind {
61    type Err = IacForgeError;
62
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        match s {
65            "resource" => Ok(Self::Resource),
66            "data_source" => Ok(Self::DataSource),
67            "provider" => Ok(Self::Provider),
68            "test" => Ok(Self::Test),
69            "schema" => Ok(Self::Schema),
70            "signature" => Ok(Self::Signature),
71            "module" => Ok(Self::Module),
72            "metadata" => Ok(Self::Metadata),
73            "controller" => Ok(Self::Controller),
74            "provider_config" => Ok(Self::ProviderConfig),
75            "helm_chart" => Ok(Self::HelmChart),
76            _ => Err(IacForgeError::ValidationError(format!(
77                "unknown artifact kind: {s}"
78            ))),
79        }
80    }
81}
82
83/// A single generated file from a backend.
84///
85/// The `source_hash` and `morphism_chain` fields carry provenance —
86/// they answer "where did this file come from?" in a machine-readable
87/// form. They are populated automatically by the blanket
88/// `Morphism<ResourceInput, Vec<GeneratedArtifact>>` impl on every
89/// `Backend`, so handwritten backend code rarely has to set them.
90///
91/// Both default to empty so the addition is non-breaking for
92/// consumers that construct artifacts directly.
93#[must_use]
94#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
95pub struct GeneratedArtifact {
96    /// Relative output path for the file.
97    pub path: String,
98    /// File content.
99    pub content: String,
100    /// What kind of artifact this is.
101    pub kind: ArtifactKind,
102    /// Content hash (lowercase hex, 64 chars) of the source IR value
103    /// this artifact was rendered from. Empty string when unattached.
104    #[serde(default)]
105    pub source_hash: String,
106    /// Ordered list of morphism names applied to produce this artifact.
107    /// First element is the outermost morphism (typically the Backend
108    /// name); last is the innermost. Empty when unattached.
109    #[serde(default)]
110    pub morphism_chain: Vec<String>,
111}
112
113impl GeneratedArtifact {
114    /// Create a new artifact with the given path, content, and kind.
115    /// Provenance fields default to empty.
116    pub fn new(path: impl Into<String>, content: impl Into<String>, kind: ArtifactKind) -> Self {
117        Self {
118            path: path.into(),
119            content: content.into(),
120            kind,
121            source_hash: String::new(),
122            morphism_chain: Vec::new(),
123        }
124    }
125
126    /// Attach provenance: record the source IR's content hash and the
127    /// morphism chain that produced this artifact. Returns `self` for
128    /// method chaining.
129    pub fn with_provenance(
130        mut self,
131        source_hash: impl Into<String>,
132        morphism_chain: Vec<String>,
133    ) -> Self {
134        self.source_hash = source_hash.into();
135        self.morphism_chain = morphism_chain;
136        self
137    }
138
139    /// Whether this artifact carries a source-hash attestation.
140    #[must_use]
141    pub fn has_provenance(&self) -> bool {
142        !self.source_hash.is_empty()
143    }
144}
145
146impl std::fmt::Display for GeneratedArtifact {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(f, "[{}] {}", self.kind, self.path)
149    }
150}
151
152/// Naming convention for a specific platform.
153///
154/// Each backend defines how API names are transformed to platform-idiomatic names.
155/// Default implementations delegate to `resource_type_name` where possible,
156/// so backends only need to override methods where their convention differs.
157pub trait NamingConvention {
158    /// Generate the platform resource type name from a resource name and provider.
159    #[must_use]
160    fn resource_type_name(&self, resource_name: &str, provider_name: &str) -> String;
161
162    /// Generate the platform data source type name.
163    ///
164    /// Defaults to the same as `resource_type_name`. Override for platforms
165    /// where data sources have different naming (e.g., Pulumi uses `get` prefix).
166    #[must_use]
167    fn data_source_type_name(&self, ds_name: &str, provider_name: &str) -> String {
168        self.resource_type_name(ds_name, provider_name)
169    }
170
171    /// Generate the output file name for an artifact.
172    #[must_use]
173    fn file_name(&self, resource_name: &str, kind: &ArtifactKind) -> String;
174
175    /// Transform an API field name to the platform's convention.
176    #[must_use]
177    fn field_name(&self, api_name: &str) -> String;
178}
179
180/// Backend trait -- each `IaC` platform implements this.
181///
182/// The trait operates on platform-independent IR types, producing
183/// platform-specific code as `GeneratedArtifact` values.
184pub trait Backend {
185    /// Platform identifier (e.g., "terraform", "pulumi", "crossplane").
186    #[must_use]
187    fn platform(&self) -> &str;
188
189    /// Generate artifacts for a single resource.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if code generation fails.
194    fn generate_resource(
195        &self,
196        resource: &IacResource,
197        provider: &IacProvider,
198    ) -> Result<Vec<GeneratedArtifact>, IacForgeError>;
199
200    /// Generate artifacts for a single data source.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if code generation fails.
205    fn generate_data_source(
206        &self,
207        ds: &IacDataSource,
208        provider: &IacProvider,
209    ) -> Result<Vec<GeneratedArtifact>, IacForgeError>;
210
211    /// Generate provider-level artifacts (registration, configuration).
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if code generation fails.
216    fn generate_provider(
217        &self,
218        provider: &IacProvider,
219        resources: &[IacResource],
220        data_sources: &[IacDataSource],
221    ) -> Result<Vec<GeneratedArtifact>, IacForgeError>;
222
223    /// Generate test artifacts for a resource.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if code generation fails.
228    fn generate_test(
229        &self,
230        resource: &IacResource,
231        provider: &IacProvider,
232    ) -> Result<Vec<GeneratedArtifact>, IacForgeError>;
233
234    /// Generate artifacts for a single RPC-style action.
235    ///
236    /// Defaults to an empty `Ok(vec![])` so backends that don't model
237    /// actions (Terraform, Pulumi, Crossplane today) can opt out by
238    /// simply inheriting. Backends that do support actions (Ansible)
239    /// override.
240    ///
241    /// # Errors
242    ///
243    /// Returns an error if code generation fails.
244    fn generate_action(
245        &self,
246        _action: &IacAction,
247        _provider: &IacProvider,
248    ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
249        Ok(vec![])
250    }
251
252    /// Get the naming convention for this platform.
253    fn naming(&self) -> &dyn NamingConvention;
254
255    /// Validate an `IacResource` before generation.
256    ///
257    /// Returns a list of human-readable validation messages. An empty list
258    /// means the resource is valid for this backend.
259    fn validate_resource(&self, _resource: &IacResource, _provider: &IacProvider) -> Vec<String> {
260        vec![]
261    }
262
263    /// Generate all artifacts for a complete provider in one batch.
264    ///
265    /// Default implementation loops over resources and data sources, then
266    /// generates provider-level artifacts. Backends can override for
267    /// optimized batch generation (e.g., Pulumi generates a single schema.json).
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if any generation step fails.
272    fn generate_all(
273        &self,
274        provider: &IacProvider,
275        resources: &[IacResource],
276        data_sources: &[IacDataSource],
277    ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
278        let mut artifacts = Vec::new();
279        for r in resources {
280            artifacts.extend(self.generate_resource(r, provider)?);
281        }
282        for ds in data_sources {
283            artifacts.extend(self.generate_data_source(ds, provider)?);
284        }
285        artifacts.extend(self.generate_provider(provider, resources, data_sources)?);
286        for r in resources {
287            artifacts.extend(self.generate_test(r, provider)?);
288        }
289        Ok(artifacts)
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::ir::{AuthInfo, CrudInfo, IdentityInfo};
297    use std::collections::BTreeMap;
298
299    #[test]
300    fn artifact_kind_display() {
301        assert_eq!(ArtifactKind::Resource.to_string(), "resource");
302        assert_eq!(ArtifactKind::Provider.to_string(), "provider");
303        assert_eq!(ArtifactKind::Test.to_string(), "test");
304        assert_eq!(ArtifactKind::Schema.to_string(), "schema");
305        assert_eq!(ArtifactKind::Controller.to_string(), "controller");
306        assert_eq!(ArtifactKind::ProviderConfig.to_string(), "provider_config");
307        assert_eq!(ArtifactKind::HelmChart.to_string(), "helm_chart");
308    }
309
310    #[test]
311    fn artifact_kind_round_trip_for_new_variants() {
312        for s in ["controller", "provider_config", "helm_chart"] {
313            let kind: ArtifactKind = s.parse().expect("parse");
314            assert_eq!(kind.to_string(), s);
315        }
316    }
317
318    #[test]
319    fn artifact_kind_unknown_string_is_error() {
320        let err: Result<ArtifactKind, _> = "not_a_kind".parse();
321        assert!(err.is_err());
322    }
323
324    /// Minimal backend for testing default method implementations.
325    struct TestBackend;
326
327    struct TestNaming;
328
329    impl NamingConvention for TestNaming {
330        fn resource_type_name(&self, resource_name: &str, provider_name: &str) -> String {
331            format!("{provider_name}_{resource_name}")
332        }
333        fn file_name(&self, resource_name: &str, _kind: &ArtifactKind) -> String {
334            format!("{resource_name}.go")
335        }
336        fn field_name(&self, api_name: &str) -> String {
337            api_name.to_string()
338        }
339    }
340
341    impl Backend for TestBackend {
342        fn platform(&self) -> &str {
343            "test"
344        }
345        fn generate_resource(
346            &self,
347            resource: &IacResource,
348            _provider: &IacProvider,
349        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
350            Ok(vec![GeneratedArtifact {
351                path: format!("resource_{}.go", resource.name),
352                content: String::new(),
353                kind: ArtifactKind::Resource,
354                source_hash: String::new(),
355                morphism_chain: Vec::new(),
356            }])
357        }
358        fn generate_data_source(
359            &self,
360            ds: &IacDataSource,
361            _provider: &IacProvider,
362        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
363            Ok(vec![GeneratedArtifact {
364                path: format!("data_source_{}.go", ds.name),
365                content: String::new(),
366                kind: ArtifactKind::DataSource,
367                source_hash: String::new(),
368                morphism_chain: Vec::new(),
369            }])
370        }
371        fn generate_provider(
372            &self,
373            _provider: &IacProvider,
374            _resources: &[IacResource],
375            _data_sources: &[IacDataSource],
376        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
377            Ok(vec![GeneratedArtifact {
378                path: "provider.go".to_string(),
379                content: String::new(),
380                kind: ArtifactKind::Provider,
381                source_hash: String::new(),
382                morphism_chain: Vec::new(),
383            }])
384        }
385        fn generate_test(
386            &self,
387            resource: &IacResource,
388            _provider: &IacProvider,
389        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
390            Ok(vec![GeneratedArtifact {
391                path: format!("resource_{}_test.go", resource.name),
392                content: String::new(),
393                kind: ArtifactKind::Test,
394                source_hash: String::new(),
395                morphism_chain: Vec::new(),
396            }])
397        }
398        fn naming(&self) -> &dyn NamingConvention {
399            &TestNaming
400        }
401    }
402
403    fn make_test_provider() -> IacProvider {
404        IacProvider {
405            name: "test".to_string(),
406            description: "Test provider".to_string(),
407            version: "1.0.0".to_string(),
408            auth: AuthInfo::default(),
409            skip_fields: vec![],
410            platform_config: BTreeMap::new(),
411        }
412    }
413
414    fn make_test_resource(name: &str) -> IacResource {
415        IacResource {
416            name: name.to_string(),
417            description: String::new(),
418            category: "test".to_string(),
419            crud: CrudInfo {
420                create_endpoint: "/create".to_string(),
421                create_schema: "Create".to_string(),
422                update_endpoint: None,
423                update_schema: None,
424                read_endpoint: "/read".to_string(),
425                read_schema: "Read".to_string(),
426                read_response_schema: None,
427                delete_endpoint: "/delete".to_string(),
428                delete_schema: "Delete".to_string(),
429            },
430            attributes: vec![],
431            identity: IdentityInfo {
432                id_field: "id".to_string(),
433                import_field: "id".to_string(),
434                force_replace_fields: vec![],
435            },
436            read_mapping: BTreeMap::new(),
437        }
438    }
439
440    fn make_test_data_source(name: &str) -> IacDataSource {
441        IacDataSource {
442            name: name.to_string(),
443            description: String::new(),
444            read_endpoint: "/read".to_string(),
445            read_schema: "Read".to_string(),
446            read_response_schema: None,
447            attributes: vec![],
448            read_mapping: BTreeMap::new(),
449        }
450    }
451
452    #[test]
453    fn validate_resource_default_returns_empty() {
454        let backend = TestBackend;
455        let provider = make_test_provider();
456        let resource = make_test_resource("foo");
457        let errors = backend.validate_resource(&resource, &provider);
458        assert!(errors.is_empty());
459    }
460
461    #[test]
462    fn generate_all_default_delegates() {
463        let backend = TestBackend;
464        let provider = make_test_provider();
465        let resources = vec![make_test_resource("r1"), make_test_resource("r2")];
466        let data_sources = vec![make_test_data_source("ds1")];
467
468        let artifacts = backend
469            .generate_all(&provider, &resources, &data_sources)
470            .expect("generate_all");
471
472        // 2 resources + 1 data source + 1 provider + 2 tests = 6
473        assert_eq!(artifacts.len(), 6);
474        assert_eq!(
475            artifacts
476                .iter()
477                .filter(|a| a.kind == ArtifactKind::Resource)
478                .count(),
479            2
480        );
481        assert_eq!(
482            artifacts
483                .iter()
484                .filter(|a| a.kind == ArtifactKind::DataSource)
485                .count(),
486            1
487        );
488        assert_eq!(
489            artifacts
490                .iter()
491                .filter(|a| a.kind == ArtifactKind::Provider)
492                .count(),
493            1
494        );
495        assert_eq!(
496            artifacts
497                .iter()
498                .filter(|a| a.kind == ArtifactKind::Test)
499                .count(),
500            2
501        );
502    }
503
504    #[test]
505    fn data_source_type_name_default() {
506        let naming = TestNaming;
507        assert_eq!(
508            naming.data_source_type_name("auth_method", "akeyless"),
509            naming.resource_type_name("auth_method", "akeyless")
510        );
511    }
512
513    #[test]
514    fn generated_artifact_display() {
515        let artifact = GeneratedArtifact {
516            path: "resource_secret.go".to_string(),
517            content: "package main".to_string(),
518            kind: ArtifactKind::Resource,
519            source_hash: String::new(),
520            morphism_chain: Vec::new(),
521        };
522        assert_eq!(artifact.to_string(), "[resource] resource_secret.go");
523    }
524
525    #[test]
526    fn generated_artifact_serialize_roundtrip() {
527        let artifact = GeneratedArtifact {
528            path: "provider.go".to_string(),
529            content: "code here".to_string(),
530            kind: ArtifactKind::Provider,
531            source_hash: String::new(),
532            morphism_chain: Vec::new(),
533        };
534        let json = serde_json::to_string(&artifact).expect("serialize");
535        let deserialized: GeneratedArtifact = serde_json::from_str(&json).expect("deserialize");
536        assert_eq!(artifact, deserialized);
537    }
538
539    #[test]
540    fn artifact_kind_serialize_roundtrip() {
541        let kinds = vec![
542            ArtifactKind::Resource,
543            ArtifactKind::DataSource,
544            ArtifactKind::Provider,
545            ArtifactKind::Test,
546            ArtifactKind::Schema,
547            ArtifactKind::Signature,
548            ArtifactKind::Module,
549            ArtifactKind::Metadata,
550        ];
551        for kind in kinds {
552            let json = serde_json::to_string(&kind).expect("serialize");
553            let deserialized: ArtifactKind = serde_json::from_str(&json).expect("deserialize");
554            assert_eq!(kind, deserialized);
555        }
556    }
557
558    #[test]
559    fn artifact_kind_display_all_variants() {
560        assert_eq!(ArtifactKind::DataSource.to_string(), "data_source");
561        assert_eq!(ArtifactKind::Module.to_string(), "module");
562        assert_eq!(ArtifactKind::Metadata.to_string(), "metadata");
563    }
564
565    /// Backend that returns errors for resource generation.
566    struct FailingBackend;
567
568    struct FailingNaming;
569
570    impl NamingConvention for FailingNaming {
571        fn resource_type_name(&self, resource_name: &str, provider_name: &str) -> String {
572            format!("{provider_name}_{resource_name}")
573        }
574        fn file_name(&self, resource_name: &str, _kind: &ArtifactKind) -> String {
575            format!("{resource_name}.go")
576        }
577        fn field_name(&self, api_name: &str) -> String {
578            api_name.to_string()
579        }
580    }
581
582    impl Backend for FailingBackend {
583        fn platform(&self) -> &str {
584            "failing"
585        }
586        fn generate_resource(
587            &self,
588            _resource: &IacResource,
589            _provider: &IacProvider,
590        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
591            Err(IacForgeError::BackendError(
592                "resource generation failed".to_string(),
593            ))
594        }
595        fn generate_data_source(
596            &self,
597            _ds: &IacDataSource,
598            _provider: &IacProvider,
599        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
600            Ok(vec![])
601        }
602        fn generate_provider(
603            &self,
604            _provider: &IacProvider,
605            _resources: &[IacResource],
606            _data_sources: &[IacDataSource],
607        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
608            Ok(vec![])
609        }
610        fn generate_test(
611            &self,
612            _resource: &IacResource,
613            _provider: &IacProvider,
614        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
615            Ok(vec![])
616        }
617        fn naming(&self) -> &dyn NamingConvention {
618            &FailingNaming
619        }
620    }
621
622    #[test]
623    fn generate_all_propagates_resource_error() {
624        let backend = FailingBackend;
625        let provider = make_test_provider();
626        let resources = vec![make_test_resource("r1")];
627        let data_sources = vec![];
628
629        let result = backend.generate_all(&provider, &resources, &data_sources);
630        assert!(
631            result.is_err(),
632            "should propagate resource generation error"
633        );
634        let err_msg = result.unwrap_err().to_string();
635        assert!(
636            err_msg.contains("resource generation failed"),
637            "error message should contain failure reason"
638        );
639    }
640
641    /// Backend that fails on data source generation.
642    struct FailingDsBackend;
643
644    impl Backend for FailingDsBackend {
645        fn platform(&self) -> &str {
646            "failing_ds"
647        }
648        fn generate_resource(
649            &self,
650            _resource: &IacResource,
651            _provider: &IacProvider,
652        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
653            Ok(vec![])
654        }
655        fn generate_data_source(
656            &self,
657            _ds: &IacDataSource,
658            _provider: &IacProvider,
659        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
660            Err(IacForgeError::BackendError(
661                "data source generation failed".to_string(),
662            ))
663        }
664        fn generate_provider(
665            &self,
666            _provider: &IacProvider,
667            _resources: &[IacResource],
668            _data_sources: &[IacDataSource],
669        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
670            Ok(vec![])
671        }
672        fn generate_test(
673            &self,
674            _resource: &IacResource,
675            _provider: &IacProvider,
676        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
677            Ok(vec![])
678        }
679        fn naming(&self) -> &dyn NamingConvention {
680            &FailingNaming
681        }
682    }
683
684    #[test]
685    fn generate_all_propagates_data_source_error() {
686        let backend = FailingDsBackend;
687        let provider = make_test_provider();
688        let resources = vec![];
689        let data_sources = vec![make_test_data_source("ds1")];
690
691        let result = backend.generate_all(&provider, &resources, &data_sources);
692        assert!(
693            result.is_err(),
694            "should propagate data source generation error"
695        );
696    }
697
698    /// Backend that overrides validate_resource.
699    struct ValidatingBackend;
700
701    impl Backend for ValidatingBackend {
702        fn platform(&self) -> &str {
703            "validating"
704        }
705        fn generate_resource(
706            &self,
707            _resource: &IacResource,
708            _provider: &IacProvider,
709        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
710            Ok(vec![])
711        }
712        fn generate_data_source(
713            &self,
714            _ds: &IacDataSource,
715            _provider: &IacProvider,
716        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
717            Ok(vec![])
718        }
719        fn generate_provider(
720            &self,
721            _provider: &IacProvider,
722            _resources: &[IacResource],
723            _data_sources: &[IacDataSource],
724        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
725            Ok(vec![])
726        }
727        fn generate_test(
728            &self,
729            _resource: &IacResource,
730            _provider: &IacProvider,
731        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
732            Ok(vec![])
733        }
734        fn naming(&self) -> &dyn NamingConvention {
735            &TestNaming
736        }
737        fn validate_resource(
738            &self,
739            resource: &IacResource,
740            _provider: &IacProvider,
741        ) -> Vec<String> {
742            let mut errors = Vec::new();
743            if resource.attributes.is_empty() {
744                errors.push("resource has no attributes".to_string());
745            }
746            errors
747        }
748    }
749
750    #[test]
751    fn validate_resource_override_returns_errors() {
752        let backend = ValidatingBackend;
753        let provider = make_test_provider();
754        let resource = make_test_resource("empty");
755        // resource has no attributes, so validation should report an error
756        let errors = backend.validate_resource(&resource, &provider);
757        assert_eq!(errors.len(), 1);
758        assert_eq!(errors[0], "resource has no attributes");
759    }
760
761    #[test]
762    fn validate_resource_override_returns_empty_for_valid() {
763        let backend = ValidatingBackend;
764        let provider = make_test_provider();
765        let mut resource = make_test_resource("valid");
766        resource.attributes.push(crate::ir::IacAttribute {
767            api_name: "name".to_string(),
768            canonical_name: "name".to_string(),
769            description: String::new(),
770            iac_type: crate::ir::IacType::String,
771            required: true,
772            optional: false,
773            computed: false,
774            sensitive: false,
775            json_encoded: false,
776            immutable: false,
777            default_value: None,
778            enum_values: None,
779            read_path: None,
780            update_only: false,
781        });
782        let errors = backend.validate_resource(&resource, &provider);
783        assert!(errors.is_empty());
784    }
785
786    #[test]
787    fn generate_all_empty_inputs() {
788        let backend = TestBackend;
789        let provider = make_test_provider();
790        let artifacts = backend
791            .generate_all(&provider, &[], &[])
792            .expect("generate_all");
793        // Only provider artifact
794        assert_eq!(artifacts.len(), 1);
795        assert_eq!(artifacts[0].kind, ArtifactKind::Provider);
796    }
797
798    /// Backend that fails on provider generation.
799    struct FailingProviderBackend;
800
801    impl Backend for FailingProviderBackend {
802        fn platform(&self) -> &str {
803            "failing_provider"
804        }
805        fn generate_resource(
806            &self,
807            _resource: &IacResource,
808            _provider: &IacProvider,
809        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
810            Ok(vec![])
811        }
812        fn generate_data_source(
813            &self,
814            _ds: &IacDataSource,
815            _provider: &IacProvider,
816        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
817            Ok(vec![])
818        }
819        fn generate_provider(
820            &self,
821            _provider: &IacProvider,
822            _resources: &[IacResource],
823            _data_sources: &[IacDataSource],
824        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
825            Err(IacForgeError::BackendError(
826                "provider generation failed".to_string(),
827            ))
828        }
829        fn generate_test(
830            &self,
831            _resource: &IacResource,
832            _provider: &IacProvider,
833        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
834            Ok(vec![])
835        }
836        fn naming(&self) -> &dyn NamingConvention {
837            &FailingNaming
838        }
839    }
840
841    #[test]
842    fn generate_all_propagates_provider_error() {
843        let backend = FailingProviderBackend;
844        let provider = make_test_provider();
845        let result = backend.generate_all(&provider, &[], &[]);
846        assert!(
847            result.is_err(),
848            "should propagate provider generation error"
849        );
850    }
851
852    /// Backend that fails on test generation.
853    struct FailingTestBackend;
854
855    impl Backend for FailingTestBackend {
856        fn platform(&self) -> &str {
857            "failing_test"
858        }
859        fn generate_resource(
860            &self,
861            _resource: &IacResource,
862            _provider: &IacProvider,
863        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
864            Ok(vec![])
865        }
866        fn generate_data_source(
867            &self,
868            _ds: &IacDataSource,
869            _provider: &IacProvider,
870        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
871            Ok(vec![])
872        }
873        fn generate_provider(
874            &self,
875            _provider: &IacProvider,
876            _resources: &[IacResource],
877            _data_sources: &[IacDataSource],
878        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
879            Ok(vec![])
880        }
881        fn generate_test(
882            &self,
883            _resource: &IacResource,
884            _provider: &IacProvider,
885        ) -> Result<Vec<GeneratedArtifact>, IacForgeError> {
886            Err(IacForgeError::BackendError(
887                "test generation failed".to_string(),
888            ))
889        }
890        fn naming(&self) -> &dyn NamingConvention {
891            &FailingNaming
892        }
893    }
894
895    #[test]
896    fn generate_all_propagates_test_error() {
897        let backend = FailingTestBackend;
898        let provider = make_test_provider();
899        let resources = vec![make_test_resource("r1")];
900        let result = backend.generate_all(&provider, &resources, &[]);
901        assert!(result.is_err(), "should propagate test generation error");
902    }
903
904    #[test]
905    fn generate_all_ordering_resources_before_datasources_before_provider() {
906        let backend = TestBackend;
907        let provider = make_test_provider();
908        let resources = vec![make_test_resource("r1")];
909        let data_sources = vec![make_test_data_source("ds1")];
910        let artifacts = backend
911            .generate_all(&provider, &resources, &data_sources)
912            .expect("generate_all");
913
914        let resource_idx = artifacts
915            .iter()
916            .position(|a| a.kind == ArtifactKind::Resource)
917            .expect("resource");
918        let ds_idx = artifacts
919            .iter()
920            .position(|a| a.kind == ArtifactKind::DataSource)
921            .expect("data_source");
922        let provider_idx = artifacts
923            .iter()
924            .position(|a| a.kind == ArtifactKind::Provider)
925            .expect("provider");
926        let test_idx = artifacts
927            .iter()
928            .position(|a| a.kind == ArtifactKind::Test)
929            .expect("test");
930
931        assert!(
932            resource_idx < ds_idx,
933            "resources should come before data sources"
934        );
935        assert!(
936            ds_idx < provider_idx,
937            "data sources should come before provider"
938        );
939        assert!(provider_idx < test_idx, "provider should come before tests");
940    }
941
942    #[test]
943    fn generate_all_multiple_resources_produces_correct_count() {
944        let backend = TestBackend;
945        let provider = make_test_provider();
946        let resources = vec![
947            make_test_resource("r1"),
948            make_test_resource("r2"),
949            make_test_resource("r3"),
950        ];
951        let data_sources = vec![make_test_data_source("ds1"), make_test_data_source("ds2")];
952        let artifacts = backend
953            .generate_all(&provider, &resources, &data_sources)
954            .expect("generate_all");
955        // 3 resources + 2 data sources + 1 provider + 3 tests = 9
956        assert_eq!(artifacts.len(), 9);
957    }
958
959    #[test]
960    fn naming_convention_resource_type_name() {
961        let naming = TestNaming;
962        assert_eq!(
963            naming.resource_type_name("secret", "akeyless"),
964            "akeyless_secret"
965        );
966    }
967
968    #[test]
969    fn naming_convention_file_name() {
970        let naming = TestNaming;
971        assert_eq!(
972            naming.file_name("secret", &ArtifactKind::Resource),
973            "secret.go"
974        );
975        assert_eq!(naming.file_name("secret", &ArtifactKind::Test), "secret.go");
976    }
977
978    #[test]
979    fn naming_convention_field_name() {
980        let naming = TestNaming;
981        assert_eq!(naming.field_name("my_field"), "my_field");
982    }
983
984    #[test]
985    fn generated_artifact_display_data_source() {
986        let artifact = GeneratedArtifact {
987            path: "data_source_config.go".to_string(),
988            content: String::new(),
989            kind: ArtifactKind::DataSource,
990            source_hash: String::new(),
991            morphism_chain: Vec::new(),
992        };
993        assert_eq!(artifact.to_string(), "[data_source] data_source_config.go");
994    }
995
996    #[test]
997    fn generated_artifact_display_test() {
998        let artifact = GeneratedArtifact {
999            path: "resource_secret_test.go".to_string(),
1000            content: "test code".to_string(),
1001            kind: ArtifactKind::Test,
1002            source_hash: String::new(),
1003            morphism_chain: Vec::new(),
1004        };
1005        assert_eq!(artifact.to_string(), "[test] resource_secret_test.go");
1006    }
1007
1008    #[test]
1009    fn generated_artifact_equality() {
1010        let a = GeneratedArtifact {
1011            path: "a.go".to_string(),
1012            content: "content".to_string(),
1013            kind: ArtifactKind::Resource,
1014            source_hash: String::new(),
1015            morphism_chain: Vec::new(),
1016        };
1017        let b = GeneratedArtifact {
1018            path: "a.go".to_string(),
1019            content: "content".to_string(),
1020            kind: ArtifactKind::Resource,
1021            source_hash: String::new(),
1022            morphism_chain: Vec::new(),
1023        };
1024        let c = GeneratedArtifact {
1025            path: "a.go".to_string(),
1026            content: "different".to_string(),
1027            kind: ArtifactKind::Resource,
1028            source_hash: String::new(),
1029            morphism_chain: Vec::new(),
1030        };
1031        assert_eq!(a, b);
1032        assert_ne!(a, c);
1033    }
1034
1035    #[test]
1036    fn generated_artifact_display_metadata() {
1037        let artifact = GeneratedArtifact {
1038            path: "metadata.json".to_string(),
1039            content: "{}".to_string(),
1040            kind: ArtifactKind::Metadata,
1041            source_hash: String::new(),
1042            morphism_chain: Vec::new(),
1043        };
1044        assert_eq!(artifact.to_string(), "[metadata] metadata.json");
1045    }
1046
1047    #[test]
1048    fn generated_artifact_display_schema() {
1049        let artifact = GeneratedArtifact {
1050            path: "schema.json".to_string(),
1051            content: "{}".to_string(),
1052            kind: ArtifactKind::Schema,
1053            source_hash: String::new(),
1054            morphism_chain: Vec::new(),
1055        };
1056        assert_eq!(artifact.to_string(), "[schema] schema.json");
1057    }
1058
1059    #[test]
1060    fn generated_artifact_display_module() {
1061        let artifact = GeneratedArtifact {
1062            path: "index.ts".to_string(),
1063            content: String::new(),
1064            kind: ArtifactKind::Module,
1065            source_hash: String::new(),
1066            morphism_chain: Vec::new(),
1067        };
1068        assert_eq!(artifact.to_string(), "[module] index.ts");
1069    }
1070
1071    #[test]
1072    fn backend_platform_name() {
1073        let backend = TestBackend;
1074        assert_eq!(backend.platform(), "test");
1075    }
1076
1077    #[test]
1078    fn generated_artifact_new() {
1079        let artifact = GeneratedArtifact::new("test.go", "package main", ArtifactKind::Resource);
1080        assert_eq!(artifact.path, "test.go");
1081        assert_eq!(artifact.content, "package main");
1082        assert_eq!(artifact.kind, ArtifactKind::Resource);
1083    }
1084
1085    #[test]
1086    fn generated_artifact_new_from_string() {
1087        let path = String::from("test.go");
1088        let content = String::from("code");
1089        let artifact = GeneratedArtifact::new(path, content, ArtifactKind::Test);
1090        assert_eq!(artifact.path, "test.go");
1091    }
1092
1093    #[test]
1094    fn artifact_kind_from_str_roundtrip() {
1095        let kinds = vec![
1096            ArtifactKind::Resource,
1097            ArtifactKind::DataSource,
1098            ArtifactKind::Provider,
1099            ArtifactKind::Test,
1100            ArtifactKind::Schema,
1101            ArtifactKind::Signature,
1102            ArtifactKind::Module,
1103            ArtifactKind::Metadata,
1104        ];
1105        for kind in kinds {
1106            let s = kind.to_string();
1107            let parsed: ArtifactKind = s.parse().expect("parse");
1108            assert_eq!(kind, parsed);
1109        }
1110    }
1111
1112    #[test]
1113    fn artifact_kind_from_str_invalid() {
1114        let result: Result<ArtifactKind, _> = "unknown".parse();
1115        assert!(result.is_err());
1116    }
1117}