Skip to main content

iac_forge/
ir.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Platform-independent type representation.
6///
7/// Richer than any single platform's type system -- preserves Object structure
8/// and Enum values needed by Pulumi schemas, Crossplane CRDs, etc.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum IacType {
12    String,
13    Integer,
14    Float,
15    /// Platform-independent numeric type.
16    ///
17    /// Terraform's `number` type accepts both integers and floats. This variant
18    /// preserves that ambiguity rather than forcing a choice between `Integer`
19    /// and `Float` at schema-import time.
20    Numeric,
21    Boolean,
22    List(Box<IacType>),
23    Set(Box<IacType>),
24    Map(Box<IacType>),
25    Object {
26        name: String,
27        fields: Vec<IacAttribute>,
28    },
29    /// Enum constraint wrapping an underlying type.
30    ///
31    /// `values` lists the allowed enum variants. `underlying` is the base type
32    /// (typically `IacType::String`).
33    Enum {
34        values: Vec<String>,
35        underlying: Box<IacType>,
36    },
37    Any,
38}
39
40impl IacType {
41    /// Whether this type is a scalar (String, Integer, Float, Numeric, Boolean).
42    #[must_use]
43    pub fn is_scalar(&self) -> bool {
44        matches!(
45            self,
46            Self::String | Self::Integer | Self::Float | Self::Numeric | Self::Boolean
47        )
48    }
49
50    /// Whether this type is a composite (List, Set, Map, Object).
51    #[must_use]
52    pub fn is_composite(&self) -> bool {
53        matches!(
54            self,
55            Self::List(_) | Self::Set(_) | Self::Map(_) | Self::Object { .. }
56        )
57    }
58
59    /// Returns the inner (element) type for container types.
60    ///
61    /// - `List(T)` → `Some(&T)`
62    /// - `Set(T)` → `Some(&T)`
63    /// - `Map(T)` → `Some(&T)` (value type; keys are always strings)
64    /// - `Enum { underlying, .. }` → `Some(&underlying)`
65    /// - All others → `None`
66    #[must_use]
67    pub fn inner_type(&self) -> Option<&IacType> {
68        match self {
69            Self::List(inner) | Self::Set(inner) | Self::Map(inner) => Some(inner),
70            Self::Enum { underlying, .. } => Some(underlying),
71            _ => None,
72        }
73    }
74}
75
76impl std::fmt::Display for IacType {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Self::String => write!(f, "string"),
80            Self::Integer => write!(f, "integer"),
81            Self::Float => write!(f, "float"),
82            Self::Numeric => write!(f, "numeric"),
83            Self::Boolean => write!(f, "boolean"),
84            Self::List(inner) => write!(f, "list<{inner}>"),
85            Self::Set(inner) => write!(f, "set<{inner}>"),
86            Self::Map(inner) => write!(f, "map<string, {inner}>"),
87            Self::Object { name, .. } => write!(f, "object<{name}>"),
88            Self::Enum { underlying, .. } => write!(f, "enum<{underlying}>"),
89            Self::Any => write!(f, "any"),
90        }
91    }
92}
93
94/// A resolved attribute in the platform-independent IR.
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
96#[allow(clippy::struct_excessive_bools)]
97pub struct IacAttribute {
98    /// Original API field name (e.g., "bound-aws-account-id").
99    pub api_name: String,
100    /// Normalized name with underscores (e.g., `bound_aws_account_id`).
101    pub canonical_name: String,
102    /// Human-readable description.
103    pub description: String,
104    /// Platform-independent type.
105    pub iac_type: IacType,
106    /// Whether the field is required on create.
107    pub required: bool,
108    /// Whether the field is optional (user can set it, but it is not required).
109    ///
110    /// In Terraform schema terms:
111    /// - `optional: true, computed: false` — user can set, no server default
112    /// - `optional: true, computed: true`  — user can set, server has default
113    /// - `optional: false, computed: true` — purely server-generated
114    #[serde(default)]
115    pub optional: bool,
116    /// Whether the field is computed (server-side generated).
117    pub computed: bool,
118    /// Whether the field contains sensitive data.
119    pub sensitive: bool,
120    /// Whether the field value is a JSON-encoded string.
121    ///
122    /// When `true`, the underlying Terraform type is still `String`, but the
123    /// value carries structured JSON (e.g., IAM policy documents). Code
124    /// generators can use this to accept both `String` and `Hash` inputs.
125    #[serde(default)]
126    pub json_encoded: bool,
127    /// Whether changing this field forces resource replacement.
128    pub immutable: bool,
129    /// Default value, if any.
130    pub default_value: Option<serde_json::Value>,
131    /// Enum constraint values, if any.
132    pub enum_values: Option<Vec<String>>,
133    /// JSON path in API response for reading this field back
134    /// (e.g., `item_name` maps to the API response key).
135    pub read_path: Option<String>,
136    /// Whether this field exists only in the update schema (not in create).
137    pub update_only: bool,
138}
139
140impl Default for IacAttribute {
141    fn default() -> Self {
142        Self {
143            api_name: String::new(),
144            canonical_name: String::new(),
145            description: String::new(),
146            iac_type: IacType::String,
147            required: false,
148            optional: false,
149            computed: false,
150            sensitive: false,
151            json_encoded: false,
152            immutable: false,
153            default_value: None,
154            enum_values: None,
155            read_path: None,
156            update_only: false,
157        }
158    }
159}
160
161impl IacAttribute {
162    /// Whether this attribute is a user-provided input.
163    ///
164    /// An attribute is an input if it is required, optional, or not computed.
165    /// Purely computed attributes (computed=true, optional=false, required=false)
166    /// are excluded.
167    #[must_use]
168    pub fn is_input(&self) -> bool {
169        self.required || self.optional || !self.computed
170    }
171
172    /// Whether this attribute appears in the output/state.
173    ///
174    /// An attribute is an output if it is computed or required.
175    #[must_use]
176    pub fn is_output(&self) -> bool {
177        self.computed || self.required
178    }
179}
180
181impl std::fmt::Display for IacAttribute {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        write!(
184            f,
185            "{}: {} ({})",
186            self.canonical_name,
187            self.iac_type,
188            if self.required {
189                "required"
190            } else {
191                "optional"
192            }
193        )
194    }
195}
196
197/// CRUD endpoint information for a resource.
198///
199/// Each field maps to an API endpoint path and its corresponding `OpenAPI` schema
200/// name used for request/response serialization.
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct CrudInfo {
203    /// API path for the create operation (e.g., "/create-secret").
204    pub create_endpoint: String,
205    /// `OpenAPI` schema name for the create request body.
206    pub create_schema: String,
207    /// API path for the update operation, if separate from create.
208    pub update_endpoint: Option<String>,
209    /// `OpenAPI` schema name for the update request body.
210    pub update_schema: Option<String>,
211    /// API path for the read operation.
212    pub read_endpoint: String,
213    /// `OpenAPI` schema name for the read request body.
214    pub read_schema: String,
215    /// `OpenAPI` schema name for the read response, if different from the request.
216    pub read_response_schema: Option<String>,
217    /// API path for the delete operation.
218    pub delete_endpoint: String,
219    /// `OpenAPI` schema name for the delete request body.
220    pub delete_schema: String,
221}
222
223impl From<&crate::spec::CrudMapping> for CrudInfo {
224    fn from(crud: &crate::spec::CrudMapping) -> Self {
225        Self {
226            create_endpoint: crud.create_endpoint.clone(),
227            create_schema: crud.create_schema.clone(),
228            update_endpoint: crud.update_endpoint.clone(),
229            update_schema: crud.update_schema.clone(),
230            read_endpoint: crud.read_endpoint.clone(),
231            read_schema: crud.read_schema.clone(),
232            read_response_schema: crud.read_response_schema.clone(),
233            delete_endpoint: crud.delete_endpoint.clone(),
234            delete_schema: crud.delete_schema.clone(),
235        }
236    }
237}
238
239/// Identity and import configuration for a resource.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct IdentityInfo {
242    /// Primary identifier field name (e.g., "name" or "id").
243    pub id_field: String,
244    /// Field used for Terraform import (defaults to `id_field`).
245    pub import_field: String,
246    /// Fields that force resource replacement when changed.
247    pub force_replace_fields: Vec<String>,
248}
249
250impl From<&crate::spec::IdentityConfig> for IdentityInfo {
251    fn from(config: &crate::spec::IdentityConfig) -> Self {
252        Self {
253            id_field: config.id_field.clone(),
254            import_field: config
255                .import_field
256                .clone()
257                .unwrap_or_else(|| config.id_field.clone()),
258            force_replace_fields: config.force_new_fields.clone(),
259        }
260    }
261}
262
263/// Common attribute queries shared by both resources and data sources.
264///
265/// Provides default implementations for filtering attributes by various
266/// properties. Implementors only need to supply `attributes()`.
267pub trait HasAttributes {
268    /// The full list of attributes on this IR type.
269    fn attributes(&self) -> &[IacAttribute];
270
271    /// Attributes that are user-provided inputs (not purely computed).
272    ///
273    /// Includes required, optional, and optional+computed attributes.
274    /// Excludes purely computed attributes (computed=true, optional=false, required=false).
275    #[must_use]
276    fn input_attributes(&self) -> Vec<&IacAttribute> {
277        self.attributes().iter().filter(|a| a.is_input()).collect()
278    }
279
280    /// Attributes that appear in the output/state (computed or required).
281    #[must_use]
282    fn output_attributes(&self) -> Vec<&IacAttribute> {
283        self.attributes().iter().filter(|a| a.is_output()).collect()
284    }
285
286    /// Required attribute canonical names.
287    #[must_use]
288    fn required_attribute_names(&self) -> Vec<&str> {
289        self.attributes()
290            .iter()
291            .filter(|a| a.required)
292            .map(|a| a.canonical_name.as_str())
293            .collect()
294    }
295
296    /// Sensitive attribute canonical names.
297    #[must_use]
298    fn sensitive_attribute_names(&self) -> Vec<&str> {
299        self.attributes()
300            .iter()
301            .filter(|a| a.sensitive)
302            .map(|a| a.canonical_name.as_str())
303            .collect()
304    }
305}
306
307/// A fully resolved resource in the platform-independent IR.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct IacResource {
310    pub name: String,
311    pub description: String,
312    pub category: String,
313    pub crud: CrudInfo,
314    pub attributes: Vec<IacAttribute>,
315    pub identity: IdentityInfo,
316    /// Mapping from API response JSON paths to canonical field names.
317    ///
318    /// Direction matches the TOML source: `json_response_key -> canonical_param_name`.
319    /// Used by backends to render an honest diff during update against a read response.
320    #[serde(default)]
321    pub read_mapping: std::collections::BTreeMap<String, String>,
322}
323
324impl std::fmt::Display for IacResource {
325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326        write!(
327            f,
328            "resource<{}> ({} attrs)",
329            self.name,
330            self.attributes.len()
331        )
332    }
333}
334
335impl HasAttributes for IacResource {
336    fn attributes(&self) -> &[IacAttribute] {
337        &self.attributes
338    }
339}
340
341impl IacResource {
342    /// Immutable attribute canonical names.
343    #[must_use]
344    pub fn immutable_attribute_names(&self) -> Vec<&str> {
345        self.attributes
346            .iter()
347            .filter(|a| a.immutable)
348            .map(|a| a.canonical_name.as_str())
349            .collect()
350    }
351}
352
353/// A fully resolved RPC-style action in the platform-independent IR.
354///
355/// Actions are one-shot calls without create/read/update/delete semantics —
356/// e.g. `/uid-generate-token`, `/encrypt`, `/revoke-certificate`. Each
357/// action carries enough metadata for backends to emit an Ansible action
358/// module, a Terraform `*_action` resource, or any other one-shot wrapper.
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct IacAction {
361    /// Action identifier (e.g., `akeyless_uid_generate_token`).
362    pub name: String,
363    /// Human-readable description.
364    pub description: String,
365    /// Category grouping (e.g., `"uid"`, `"crypto"`).
366    pub category: String,
367    /// API path (e.g., `/uid-generate-token`).
368    pub endpoint: String,
369    /// Request `OpenAPI` schema name (camelCase form).
370    pub schema: String,
371    /// Optional response schema name.
372    pub response_schema: Option<String>,
373    /// Whether the action mutates server state.
374    ///
375    /// Used by backends to decide whether to flag `changed=true` (Ansible)
376    /// or run the call inside a transaction (Terraform). Defaults to
377    /// `true` when the spec leaves it unspecified.
378    pub mutating: bool,
379    /// Response fields whose values should be masked (`***`) before being
380    /// echoed back to the user.
381    pub sensitive_response_fields: Vec<String>,
382    /// Resolved input attributes (request body fields).
383    pub attributes: Vec<IacAttribute>,
384    /// Optional SDK method-name override.
385    ///
386    /// When `None`, backends derive the method from `schema`. When
387    /// `Some(name)`, they should use `name` verbatim. Used for batch
388    /// endpoints where the request body schema (e.g.
389    /// `BatchEncryptionRequestLine`) doesn't match the SDK method
390    /// (e.g. `encrypt_batch`).
391    #[serde(default)]
392    pub sdk_method: Option<String>,
393}
394
395impl std::fmt::Display for IacAction {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        write!(
398            f,
399            "action<{}> {} ({} attrs)",
400            self.name,
401            self.endpoint,
402            self.attributes.len()
403        )
404    }
405}
406
407impl HasAttributes for IacAction {
408    fn attributes(&self) -> &[IacAttribute] {
409        &self.attributes
410    }
411}
412
413/// A fully resolved data source in the platform-independent IR.
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct IacDataSource {
416    pub name: String,
417    pub description: String,
418    pub read_endpoint: String,
419    pub read_schema: String,
420    pub read_response_schema: Option<String>,
421    pub attributes: Vec<IacAttribute>,
422    /// Mapping from API response JSON paths to canonical field names.
423    ///
424    /// Direction matches the TOML source: `json_response_key -> canonical_param_name`.
425    #[serde(default)]
426    pub read_mapping: std::collections::BTreeMap<String, String>,
427}
428
429impl std::fmt::Display for IacDataSource {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        write!(
432            f,
433            "data_source<{}> ({} attrs)",
434            self.name,
435            self.attributes.len()
436        )
437    }
438}
439
440impl HasAttributes for IacDataSource {
441    fn attributes(&self) -> &[IacAttribute] {
442        &self.attributes
443    }
444}
445
446impl IacDataSource {
447    /// Computed attribute names.
448    #[must_use]
449    pub fn computed_attribute_names(&self) -> Vec<&str> {
450        self.attributes
451            .iter()
452            .filter(|a| a.computed)
453            .map(|a| a.canonical_name.as_str())
454            .collect()
455    }
456}
457
458/// Provider-level configuration in the platform-independent IR.
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct IacProvider {
461    pub name: String,
462    pub description: String,
463    pub version: String,
464    pub auth: AuthInfo,
465    pub skip_fields: Vec<String>,
466    pub platform_config: BTreeMap<String, toml::Value>,
467}
468
469/// Authentication configuration for a provider.
470///
471/// Empty strings mean "not configured". Use the `has_token()` and
472/// `has_gateway()` helpers to check.
473#[derive(Debug, Clone, Default, Serialize, Deserialize)]
474pub struct AuthInfo {
475    /// API field name for the authentication token (e.g., "token").
476    pub token_field: String,
477    /// Environment variable that supplies the token (e.g., `AKEYLESS_ACCESS_TOKEN`).
478    pub env_var: String,
479    /// API field name for the gateway URL (e.g., `api_gateway_address`).
480    pub gateway_url_field: String,
481    /// Environment variable that supplies the gateway URL (e.g., `AKEYLESS_GATEWAY`).
482    pub gateway_env_var: String,
483}
484
485impl From<&crate::spec::AuthConfig> for AuthInfo {
486    fn from(auth: &crate::spec::AuthConfig) -> Self {
487        Self {
488            token_field: auth.token_field.clone(),
489            env_var: auth.env_var.clone(),
490            gateway_url_field: auth.gateway_url_field.clone(),
491            gateway_env_var: auth.gateway_env_var.clone(),
492        }
493    }
494}
495
496impl AuthInfo {
497    /// Returns `true` if a token field is configured.
498    #[must_use]
499    pub fn has_token(&self) -> bool {
500        !self.token_field.is_empty()
501    }
502
503    /// Returns `true` if a gateway URL field is configured.
504    #[must_use]
505    pub fn has_gateway(&self) -> bool {
506        !self.gateway_url_field.is_empty()
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::testing::TestAttributeBuilder;
514
515    #[test]
516    fn attribute_is_input_required() {
517        let attr = TestAttributeBuilder::new("name", IacType::String)
518            .required()
519            .build();
520        assert!(attr.is_input());
521    }
522
523    #[test]
524    fn attribute_is_input_optional() {
525        let attr = TestAttributeBuilder::new("tag", IacType::String)
526            .optional()
527            .build();
528        assert!(attr.is_input());
529    }
530
531    #[test]
532    fn attribute_is_input_optional_computed() {
533        let attr = TestAttributeBuilder::new("default_val", IacType::String)
534            .optional()
535            .computed()
536            .build();
537        assert!(attr.is_input());
538    }
539
540    #[test]
541    fn attribute_is_not_input_purely_computed() {
542        let attr = TestAttributeBuilder::new("id", IacType::String)
543            .computed()
544            .build();
545        assert!(!attr.is_input());
546    }
547
548    #[test]
549    fn attribute_is_output_computed() {
550        let attr = TestAttributeBuilder::new("id", IacType::String)
551            .computed()
552            .build();
553        assert!(attr.is_output());
554    }
555
556    #[test]
557    fn attribute_is_output_required() {
558        let attr = TestAttributeBuilder::new("name", IacType::String)
559            .required()
560            .build();
561        assert!(attr.is_output());
562    }
563
564    #[test]
565    fn attribute_is_not_output_plain_optional() {
566        let attr = TestAttributeBuilder::new("tag", IacType::String).build();
567        assert!(!attr.is_output());
568    }
569
570    #[test]
571    fn iac_type_display() {
572        assert_eq!(IacType::String.to_string(), "string");
573        assert_eq!(IacType::Integer.to_string(), "integer");
574        assert_eq!(IacType::Float.to_string(), "float");
575        assert_eq!(IacType::Numeric.to_string(), "numeric");
576        assert_eq!(IacType::Boolean.to_string(), "boolean");
577        assert_eq!(
578            IacType::List(Box::new(IacType::String)).to_string(),
579            "list<string>"
580        );
581        assert_eq!(
582            IacType::Set(Box::new(IacType::Integer)).to_string(),
583            "set<integer>"
584        );
585        assert_eq!(
586            IacType::Map(Box::new(IacType::Boolean)).to_string(),
587            "map<string, boolean>"
588        );
589        assert_eq!(
590            IacType::Object {
591                name: "User".to_string(),
592                fields: vec![]
593            }
594            .to_string(),
595            "object<User>"
596        );
597        assert_eq!(
598            IacType::Enum {
599                values: vec!["a".into()],
600                underlying: Box::new(IacType::String)
601            }
602            .to_string(),
603            "enum<string>"
604        );
605        assert_eq!(IacType::Any.to_string(), "any");
606    }
607
608    #[test]
609    fn iac_type_equality() {
610        assert_eq!(IacType::String, IacType::String);
611        assert_ne!(IacType::String, IacType::Integer);
612        assert_eq!(
613            IacType::List(Box::new(IacType::String)),
614            IacType::List(Box::new(IacType::String))
615        );
616        assert_ne!(
617            IacType::List(Box::new(IacType::String)),
618            IacType::List(Box::new(IacType::Integer))
619        );
620    }
621
622    #[test]
623    fn iac_type_hash() {
624        use std::collections::HashSet;
625        let mut set = HashSet::new();
626        set.insert(IacType::String);
627        set.insert(IacType::Integer);
628        set.insert(IacType::String);
629        assert_eq!(set.len(), 2);
630    }
631
632    #[test]
633    fn iac_attribute_display() {
634        let attr = IacAttribute {
635            api_name: "name".to_string(),
636            canonical_name: "name".to_string(),
637            description: String::new(),
638            iac_type: IacType::String,
639            required: true,
640            optional: false,
641            computed: false,
642            sensitive: false,
643            json_encoded: false,
644            immutable: false,
645            default_value: None,
646            enum_values: None,
647            read_path: None,
648            update_only: false,
649        };
650        assert_eq!(attr.to_string(), "name: string (required)");
651
652        let optional = IacAttribute {
653            required: false,
654            optional: true,
655            ..attr
656        };
657        assert_eq!(optional.to_string(), "name: string (optional)");
658    }
659
660    #[test]
661    fn auth_info_helpers() {
662        let empty = AuthInfo::default();
663        assert!(!empty.has_token());
664        assert!(!empty.has_gateway());
665
666        let configured = AuthInfo {
667            token_field: "token".to_string(),
668            env_var: "TOKEN".to_string(),
669            gateway_url_field: "gateway".to_string(),
670            gateway_env_var: "GW".to_string(),
671        };
672        assert!(configured.has_token());
673        assert!(configured.has_gateway());
674    }
675
676    #[test]
677    fn iac_resource_serialize_roundtrip() {
678        let resource = IacResource {
679            name: "test_resource".to_string(),
680            description: "A test".to_string(),
681            category: "test".to_string(),
682            crud: CrudInfo {
683                create_endpoint: "/create".to_string(),
684                create_schema: "Create".to_string(),
685                update_endpoint: None,
686                update_schema: None,
687                read_endpoint: "/read".to_string(),
688                read_schema: "Read".to_string(),
689                read_response_schema: None,
690                delete_endpoint: "/delete".to_string(),
691                delete_schema: "Delete".to_string(),
692            },
693            attributes: vec![IacAttribute {
694                api_name: "name".to_string(),
695                canonical_name: "name".to_string(),
696                description: "The name".to_string(),
697                iac_type: IacType::String,
698                required: true,
699                optional: false,
700                computed: false,
701                sensitive: false,
702                json_encoded: false,
703                immutable: true,
704                default_value: None,
705                enum_values: None,
706                read_path: None,
707                update_only: false,
708            }],
709            identity: IdentityInfo {
710                id_field: "name".to_string(),
711                import_field: "name".to_string(),
712                force_replace_fields: vec!["name".to_string()],
713            },
714            read_mapping: BTreeMap::new(),
715        };
716
717        let json = serde_json::to_string(&resource).expect("serialize");
718        let deserialized: IacResource = serde_json::from_str(&json).expect("deserialize");
719        assert_eq!(deserialized.name, resource.name);
720        assert_eq!(deserialized.attributes.len(), 1);
721        assert_eq!(deserialized.attributes[0].iac_type, IacType::String);
722    }
723
724    /// Helper to build a resource with mixed attribute flags for filter tests.
725    fn resource_with_mixed_attrs() -> IacResource {
726        IacResource {
727            name: "mixed".to_string(),
728            description: String::new(),
729            category: "test".to_string(),
730            crud: CrudInfo {
731                create_endpoint: "/create".to_string(),
732                create_schema: "Create".to_string(),
733                update_endpoint: None,
734                update_schema: None,
735                read_endpoint: "/read".to_string(),
736                read_schema: "Read".to_string(),
737                read_response_schema: None,
738                delete_endpoint: "/delete".to_string(),
739                delete_schema: "Delete".to_string(),
740            },
741            attributes: vec![
742                TestAttributeBuilder::new("name", IacType::String)
743                    .required()
744                    .immutable()
745                    .build(),
746                TestAttributeBuilder::new("secret", IacType::String)
747                    .required()
748                    .sensitive()
749                    .build(),
750                TestAttributeBuilder::new("computed_id", IacType::String)
751                    .computed()
752                    .build(),
753                TestAttributeBuilder::new("tags", IacType::List(Box::new(IacType::String))).build(),
754            ],
755            identity: IdentityInfo {
756                id_field: "name".to_string(),
757                import_field: "name".to_string(),
758                force_replace_fields: vec!["name".to_string()],
759            },
760            read_mapping: BTreeMap::new(),
761        }
762    }
763
764    #[test]
765    fn input_attributes_excludes_purely_computed() {
766        let r = resource_with_mixed_attrs();
767        let inputs = r.input_attributes();
768        // "name" (required), "secret" (required), "tags" (not computed) = 3
769        // "computed_id" is computed and not required, so excluded
770        assert_eq!(inputs.len(), 3);
771        assert!(inputs.iter().all(|a| a.canonical_name != "computed_id"));
772    }
773
774    #[test]
775    fn output_attributes_includes_computed_and_required() {
776        let r = resource_with_mixed_attrs();
777        let outputs = r.output_attributes();
778        // "name" (required), "secret" (required), "computed_id" (computed) = 3
779        // "tags" is neither computed nor required
780        assert_eq!(outputs.len(), 3);
781        assert!(outputs.iter().all(|a| a.canonical_name != "tags"));
782    }
783
784    #[test]
785    fn required_attribute_names_returns_correct_set() {
786        let r = resource_with_mixed_attrs();
787        let names = r.required_attribute_names();
788        assert_eq!(names, vec!["name", "secret"]);
789    }
790
791    #[test]
792    fn sensitive_attribute_names_returns_correct_set() {
793        let r = resource_with_mixed_attrs();
794        let names = r.sensitive_attribute_names();
795        assert_eq!(names, vec!["secret"]);
796    }
797
798    #[test]
799    fn immutable_attribute_names_returns_correct_set() {
800        let r = resource_with_mixed_attrs();
801        let names = r.immutable_attribute_names();
802        assert_eq!(names, vec!["name"]);
803    }
804
805    #[test]
806    fn data_source_input_and_output_attributes() {
807        let ds = crate::testing::test_data_source("cfg");
808        // "name" is required (not computed) -> input
809        // "value" is computed -> output
810        let inputs = ds.input_attributes();
811        assert_eq!(inputs.len(), 1);
812        assert_eq!(inputs[0].canonical_name, "name");
813
814        let outputs = ds.output_attributes();
815        assert_eq!(outputs.len(), 2);
816
817        let computed = ds.computed_attribute_names();
818        assert_eq!(computed, vec!["value"]);
819    }
820
821    #[test]
822    fn data_source_required_and_sensitive() {
823        let ds = IacDataSource {
824            name: "test".to_string(),
825            description: String::new(),
826            read_endpoint: "/read".to_string(),
827            read_schema: "Read".to_string(),
828            read_response_schema: None,
829            attributes: vec![
830                TestAttributeBuilder::new("id", IacType::String)
831                    .required()
832                    .build(),
833                TestAttributeBuilder::new("password", IacType::String)
834                    .sensitive()
835                    .computed()
836                    .build(),
837            ],
838            read_mapping: BTreeMap::new(),
839        };
840        assert_eq!(ds.required_attribute_names(), vec!["id"]);
841        assert_eq!(ds.sensitive_attribute_names(), vec!["password"]);
842    }
843
844    #[test]
845    fn resource_input_attributes_mix() {
846        let r = IacResource {
847            name: "mix".to_string(),
848            description: String::new(),
849            category: "test".to_string(),
850            crud: CrudInfo {
851                create_endpoint: "/create".to_string(),
852                create_schema: "Create".to_string(),
853                update_endpoint: None,
854                update_schema: None,
855                read_endpoint: "/read".to_string(),
856                read_schema: "Read".to_string(),
857                read_response_schema: None,
858                delete_endpoint: "/delete".to_string(),
859                delete_schema: "Delete".to_string(),
860            },
861            attributes: vec![
862                TestAttributeBuilder::new("req_only", IacType::String)
863                    .required()
864                    .build(),
865                TestAttributeBuilder::new("comp_only", IacType::String)
866                    .computed()
867                    .build(),
868                TestAttributeBuilder::new("comp_and_req", IacType::String)
869                    .computed()
870                    .required()
871                    .build(),
872                TestAttributeBuilder::new("optional", IacType::String).build(),
873            ],
874            identity: IdentityInfo {
875                id_field: "req_only".to_string(),
876                import_field: "req_only".to_string(),
877                force_replace_fields: vec![],
878            },
879            read_mapping: BTreeMap::new(),
880        };
881
882        let inputs = r.input_attributes();
883        let input_names: Vec<&str> = inputs.iter().map(|a| a.canonical_name.as_str()).collect();
884        // Input: not computed OR required
885        // req_only: not computed -> yes
886        // comp_only: computed and not required -> no
887        // comp_and_req: computed but required -> yes
888        // optional: not computed -> yes
889        assert_eq!(input_names, vec!["req_only", "comp_and_req", "optional"]);
890
891        let outputs = r.output_attributes();
892        let output_names: Vec<&str> = outputs.iter().map(|a| a.canonical_name.as_str()).collect();
893        // Output: computed OR required
894        // req_only: required -> yes
895        // comp_only: computed -> yes
896        // comp_and_req: both -> yes
897        // optional: neither -> no
898        assert_eq!(output_names, vec!["req_only", "comp_only", "comp_and_req"]);
899    }
900
901    #[test]
902    fn data_source_input_and_output_full() {
903        let ds = IacDataSource {
904            name: "full_ds".to_string(),
905            description: String::new(),
906            read_endpoint: "/read".to_string(),
907            read_schema: "Read".to_string(),
908            read_response_schema: None,
909            attributes: vec![
910                TestAttributeBuilder::new("input_key", IacType::String)
911                    .required()
912                    .build(),
913                TestAttributeBuilder::new("output_val", IacType::String)
914                    .computed()
915                    .build(),
916                TestAttributeBuilder::new("both", IacType::String)
917                    .required()
918                    .computed()
919                    .build(),
920                TestAttributeBuilder::new("neither", IacType::String).build(),
921            ],
922            read_mapping: BTreeMap::new(),
923        };
924
925        let inputs = ds.input_attributes();
926        let input_names: Vec<&str> = inputs.iter().map(|a| a.canonical_name.as_str()).collect();
927        // Input: not computed OR required
928        assert_eq!(input_names, vec!["input_key", "both", "neither"]);
929
930        let outputs = ds.output_attributes();
931        let output_names: Vec<&str> = outputs.iter().map(|a| a.canonical_name.as_str()).collect();
932        // Output: computed OR required
933        assert_eq!(output_names, vec!["input_key", "output_val", "both"]);
934
935        let computed = ds.computed_attribute_names();
936        assert_eq!(computed, vec!["output_val", "both"]);
937    }
938
939    #[test]
940    fn auth_info_has_token_only() {
941        let auth = AuthInfo {
942            token_field: "token".to_string(),
943            env_var: "TOKEN".to_string(),
944            gateway_url_field: String::new(),
945            gateway_env_var: String::new(),
946        };
947        assert!(auth.has_token());
948        assert!(!auth.has_gateway());
949    }
950
951    #[test]
952    fn auth_info_has_gateway_only() {
953        let auth = AuthInfo {
954            token_field: String::new(),
955            env_var: String::new(),
956            gateway_url_field: "gateway".to_string(),
957            gateway_env_var: "GW".to_string(),
958        };
959        assert!(!auth.has_token());
960        assert!(auth.has_gateway());
961    }
962
963    #[test]
964    fn resource_no_attributes() {
965        let r = IacResource {
966            name: "empty".to_string(),
967            description: String::new(),
968            category: "test".to_string(),
969            crud: CrudInfo {
970                create_endpoint: "/create".to_string(),
971                create_schema: "Create".to_string(),
972                update_endpoint: None,
973                update_schema: None,
974                read_endpoint: "/read".to_string(),
975                read_schema: "Read".to_string(),
976                read_response_schema: None,
977                delete_endpoint: "/delete".to_string(),
978                delete_schema: "Delete".to_string(),
979            },
980            attributes: vec![],
981            identity: IdentityInfo {
982                id_field: "id".to_string(),
983                import_field: "id".to_string(),
984                force_replace_fields: vec![],
985            },
986            read_mapping: BTreeMap::new(),
987        };
988
989        assert!(r.input_attributes().is_empty());
990        assert!(r.output_attributes().is_empty());
991        assert!(r.required_attribute_names().is_empty());
992        assert!(r.sensitive_attribute_names().is_empty());
993        assert!(r.immutable_attribute_names().is_empty());
994    }
995
996    #[test]
997    fn data_source_no_attributes() {
998        let ds = IacDataSource {
999            name: "empty".to_string(),
1000            description: String::new(),
1001            read_endpoint: "/read".to_string(),
1002            read_schema: "Read".to_string(),
1003            read_response_schema: None,
1004            attributes: vec![],
1005            read_mapping: BTreeMap::new(),
1006        };
1007
1008        assert!(ds.input_attributes().is_empty());
1009        assert!(ds.output_attributes().is_empty());
1010        assert!(ds.required_attribute_names().is_empty());
1011        assert!(ds.sensitive_attribute_names().is_empty());
1012        assert!(ds.computed_attribute_names().is_empty());
1013    }
1014
1015    #[test]
1016    fn iac_type_display_nested() {
1017        assert_eq!(
1018            IacType::List(Box::new(IacType::List(Box::new(IacType::String)))).to_string(),
1019            "list<list<string>>"
1020        );
1021        assert_eq!(
1022            IacType::Map(Box::new(IacType::List(Box::new(IacType::Integer)))).to_string(),
1023            "map<string, list<integer>>"
1024        );
1025        assert_eq!(
1026            IacType::Set(Box::new(IacType::Set(Box::new(IacType::Boolean)))).to_string(),
1027            "set<set<boolean>>"
1028        );
1029        assert_eq!(
1030            IacType::Set(Box::new(IacType::Map(Box::new(IacType::Float)))).to_string(),
1031            "set<map<string, float>>"
1032        );
1033    }
1034
1035    #[test]
1036    fn iac_attribute_display_optional() {
1037        let attr = IacAttribute {
1038            api_name: "tags".to_string(),
1039            canonical_name: "tags".to_string(),
1040            description: String::new(),
1041            iac_type: IacType::List(Box::new(IacType::String)),
1042            required: false,
1043            optional: true,
1044            computed: false,
1045            sensitive: false,
1046            json_encoded: false,
1047            immutable: false,
1048            default_value: None,
1049            enum_values: None,
1050            read_path: None,
1051            update_only: false,
1052        };
1053        assert_eq!(attr.to_string(), "tags: list<string> (optional)");
1054    }
1055
1056    #[test]
1057    fn iac_type_set_serde_roundtrip() {
1058        let set_type = IacType::Set(Box::new(IacType::String));
1059        let json = serde_json::to_string(&set_type).expect("serialize");
1060        let rt: IacType = serde_json::from_str(&json).expect("deserialize");
1061        assert_eq!(set_type, rt);
1062    }
1063
1064    #[test]
1065    fn iac_type_set_display() {
1066        assert_eq!(
1067            IacType::Set(Box::new(IacType::String)).to_string(),
1068            "set<string>"
1069        );
1070        assert_eq!(
1071            IacType::Set(Box::new(IacType::Integer)).to_string(),
1072            "set<integer>"
1073        );
1074    }
1075
1076    #[test]
1077    fn iac_type_set_equality() {
1078        let a = IacType::Set(Box::new(IacType::String));
1079        let b = IacType::Set(Box::new(IacType::String));
1080        let c = IacType::Set(Box::new(IacType::Integer));
1081        assert_eq!(a, b);
1082        assert_ne!(a, c);
1083    }
1084
1085    #[test]
1086    fn iac_type_set_hash() {
1087        use std::collections::HashSet;
1088        let mut set = HashSet::new();
1089        set.insert(IacType::Set(Box::new(IacType::String)));
1090        set.insert(IacType::Set(Box::new(IacType::String)));
1091        set.insert(IacType::Set(Box::new(IacType::Integer)));
1092        assert_eq!(set.len(), 2);
1093    }
1094
1095    #[test]
1096    fn iac_type_serde_roundtrip_all_variants() {
1097        let types = vec![
1098            IacType::String,
1099            IacType::Integer,
1100            IacType::Float,
1101            IacType::Numeric,
1102            IacType::Boolean,
1103            IacType::Any,
1104            IacType::List(Box::new(IacType::Integer)),
1105            IacType::Set(Box::new(IacType::Boolean)),
1106            IacType::Map(Box::new(IacType::Float)),
1107            IacType::Object {
1108                name: "Nested".to_string(),
1109                fields: vec![],
1110            },
1111            IacType::Enum {
1112                values: vec!["x".into(), "y".into()],
1113                underlying: Box::new(IacType::String),
1114            },
1115        ];
1116        for t in &types {
1117            let json = serde_json::to_string(t).expect("serialize");
1118            let roundtripped: IacType = serde_json::from_str(&json).expect("deserialize");
1119            assert_eq!(*t, roundtripped, "roundtrip failed for {t}");
1120        }
1121    }
1122
1123    #[test]
1124    fn iac_type_serde_roundtrip_deeply_nested() {
1125        let deep = IacType::List(Box::new(IacType::Map(Box::new(IacType::Set(Box::new(
1126            IacType::Object {
1127                name: "Inner".to_string(),
1128                fields: vec![
1129                    crate::testing::TestAttributeBuilder::new("f", IacType::String)
1130                        .required()
1131                        .build(),
1132                ],
1133            },
1134        ))))));
1135        let json = serde_json::to_string(&deep).expect("serialize");
1136        let roundtripped: IacType = serde_json::from_str(&json).expect("deserialize");
1137        assert_eq!(deep, roundtripped);
1138    }
1139
1140    #[test]
1141    fn iac_attribute_serde_with_all_fields_set() {
1142        let attr = IacAttribute {
1143            api_name: "my-key".to_string(),
1144            canonical_name: "my_key".to_string(),
1145            description: "A key".to_string(),
1146            iac_type: IacType::String,
1147            required: true,
1148            optional: false,
1149            computed: true,
1150            sensitive: true,
1151            json_encoded: true,
1152            immutable: true,
1153            default_value: Some(serde_json::json!("default_val")),
1154            enum_values: Some(vec!["a".into(), "b".into()]),
1155            read_path: Some("response_key".to_string()),
1156            update_only: true,
1157        };
1158        let json = serde_json::to_string(&attr).expect("serialize");
1159        let roundtripped: IacAttribute = serde_json::from_str(&json).expect("deserialize");
1160        assert_eq!(attr, roundtripped);
1161    }
1162
1163    #[test]
1164    fn iac_attribute_json_encoded_default_false() {
1165        let json_str = r#"{
1166            "api_name":"f","canonical_name":"f","description":"","iac_type":"String",
1167            "required":false,"optional":false,"computed":false,"sensitive":false,
1168            "immutable":false,"default_value":null,"enum_values":null,
1169            "read_path":null,"update_only":false
1170        }"#;
1171        let attr: IacAttribute = serde_json::from_str(json_str).expect("deserialize");
1172        assert!(!attr.json_encoded, "json_encoded should default to false");
1173    }
1174
1175    #[test]
1176    fn iac_attribute_optional_default_false() {
1177        let json_str = r#"{
1178            "api_name":"f","canonical_name":"f","description":"","iac_type":"String",
1179            "required":false,"computed":false,"sensitive":false,
1180            "immutable":false,"default_value":null,"enum_values":null,
1181            "read_path":null,"update_only":false
1182        }"#;
1183        let attr: IacAttribute = serde_json::from_str(json_str).expect("deserialize");
1184        assert!(!attr.optional, "optional should default to false");
1185    }
1186
1187    #[test]
1188    fn resource_optional_computed_is_input() {
1189        let r = IacResource {
1190            name: "opt_comp".to_string(),
1191            description: String::new(),
1192            category: "test".to_string(),
1193            crud: CrudInfo {
1194                create_endpoint: "/create".to_string(),
1195                create_schema: "Create".to_string(),
1196                update_endpoint: None,
1197                update_schema: None,
1198                read_endpoint: "/read".to_string(),
1199                read_schema: "Read".to_string(),
1200                read_response_schema: None,
1201                delete_endpoint: "/delete".to_string(),
1202                delete_schema: "Delete".to_string(),
1203            },
1204            attributes: vec![
1205                TestAttributeBuilder::new("opt_comp_field", IacType::String)
1206                    .optional()
1207                    .computed()
1208                    .build(),
1209            ],
1210            identity: IdentityInfo {
1211                id_field: "id".to_string(),
1212                import_field: "id".to_string(),
1213                force_replace_fields: vec![],
1214            },
1215            read_mapping: BTreeMap::new(),
1216        };
1217        let inputs = r.input_attributes();
1218        assert_eq!(inputs.len(), 1, "optional+computed should be an input");
1219        assert_eq!(inputs[0].canonical_name, "opt_comp_field");
1220    }
1221
1222    #[test]
1223    fn data_source_optional_computed_is_input() {
1224        let ds = IacDataSource {
1225            name: "opt_comp_ds".to_string(),
1226            description: String::new(),
1227            read_endpoint: "/read".to_string(),
1228            read_schema: "Read".to_string(),
1229            read_response_schema: None,
1230            attributes: vec![
1231                TestAttributeBuilder::new("opt_comp_field", IacType::String)
1232                    .optional()
1233                    .computed()
1234                    .build(),
1235            ],
1236            read_mapping: BTreeMap::new(),
1237        };
1238        let inputs = ds.input_attributes();
1239        assert_eq!(
1240            inputs.len(),
1241            1,
1242            "optional+computed should be an input for data source"
1243        );
1244    }
1245
1246    #[test]
1247    fn iac_type_clone_equality() {
1248        let original = IacType::Object {
1249            name: "Config".to_string(),
1250            fields: vec![crate::testing::TestAttributeBuilder::new("k", IacType::String).build()],
1251        };
1252        let cloned = original.clone();
1253        assert_eq!(original, cloned);
1254    }
1255
1256    #[test]
1257    fn crud_info_serde_roundtrip_with_optionals() {
1258        let crud = CrudInfo {
1259            create_endpoint: "/create".to_string(),
1260            create_schema: "Create".to_string(),
1261            update_endpoint: Some("/update".to_string()),
1262            update_schema: Some("Update".to_string()),
1263            read_endpoint: "/read".to_string(),
1264            read_schema: "Read".to_string(),
1265            read_response_schema: Some("ReadResp".to_string()),
1266            delete_endpoint: "/delete".to_string(),
1267            delete_schema: "Delete".to_string(),
1268        };
1269        let json = serde_json::to_string(&crud).expect("serialize");
1270        let rt: CrudInfo = serde_json::from_str(&json).expect("deserialize");
1271        assert_eq!(rt.update_endpoint, Some("/update".to_string()));
1272        assert_eq!(rt.update_schema, Some("Update".to_string()));
1273        assert_eq!(rt.read_response_schema, Some("ReadResp".to_string()));
1274    }
1275
1276    #[test]
1277    fn identity_info_serde_roundtrip() {
1278        let id = IdentityInfo {
1279            id_field: "name".to_string(),
1280            import_field: "path".to_string(),
1281            force_replace_fields: vec!["name".to_string(), "region".to_string()],
1282        };
1283        let json = serde_json::to_string(&id).expect("serialize");
1284        let rt: IdentityInfo = serde_json::from_str(&json).expect("deserialize");
1285        assert_eq!(rt.id_field, "name");
1286        assert_eq!(rt.import_field, "path");
1287        assert_eq!(rt.force_replace_fields.len(), 2);
1288    }
1289
1290    #[test]
1291    fn iac_provider_empty_auth_serializes() {
1292        let provider = IacProvider {
1293            name: "minimal".to_string(),
1294            description: String::new(),
1295            version: "0.1.0".to_string(),
1296            auth: AuthInfo::default(),
1297            skip_fields: vec![],
1298            platform_config: BTreeMap::new(),
1299        };
1300        let json = serde_json::to_string(&provider).expect("serialize");
1301        let rt: IacProvider = serde_json::from_str(&json).expect("deserialize");
1302        assert!(!rt.auth.has_token());
1303        assert!(!rt.auth.has_gateway());
1304        assert!(rt.skip_fields.is_empty());
1305        assert!(rt.platform_config.is_empty());
1306    }
1307
1308    #[test]
1309    fn iac_provider_serialize_roundtrip() {
1310        let provider = IacProvider {
1311            name: "test".to_string(),
1312            description: "Test provider".to_string(),
1313            version: "1.0.0".to_string(),
1314            auth: AuthInfo {
1315                token_field: "token".to_string(),
1316                env_var: "TOKEN".to_string(),
1317                gateway_url_field: "gw".to_string(),
1318                gateway_env_var: "GW".to_string(),
1319            },
1320            skip_fields: vec!["token".to_string()],
1321            platform_config: {
1322                let mut m = BTreeMap::new();
1323                m.insert(
1324                    "terraform".to_string(),
1325                    toml::Value::String("sdk".to_string()),
1326                );
1327                m
1328            },
1329        };
1330        let json = serde_json::to_string(&provider).expect("serialize");
1331        let deserialized: IacProvider = serde_json::from_str(&json).expect("deserialize");
1332        assert_eq!(deserialized.name, "test");
1333        assert_eq!(deserialized.auth.token_field, "token");
1334        assert_eq!(deserialized.skip_fields, vec!["token"]);
1335    }
1336
1337    #[test]
1338    fn iac_data_source_serialize_roundtrip() {
1339        let ds = IacDataSource {
1340            name: "ds".to_string(),
1341            description: "A data source".to_string(),
1342            read_endpoint: "/read".to_string(),
1343            read_schema: "Read".to_string(),
1344            read_response_schema: Some("ReadOutput".to_string()),
1345            attributes: vec![
1346                TestAttributeBuilder::new("key", IacType::String)
1347                    .required()
1348                    .build(),
1349            ],
1350            read_mapping: BTreeMap::new(),
1351        };
1352        let json = serde_json::to_string(&ds).expect("serialize");
1353        let deserialized: IacDataSource = serde_json::from_str(&json).expect("deserialize");
1354        assert_eq!(deserialized.name, "ds");
1355        assert_eq!(
1356            deserialized.read_response_schema,
1357            Some("ReadOutput".to_string())
1358        );
1359        assert_eq!(deserialized.attributes.len(), 1);
1360    }
1361
1362    #[test]
1363    fn crud_info_from_crud_mapping() {
1364        let mapping = crate::spec::CrudMapping {
1365            create_endpoint: "/create".to_string(),
1366            create_schema: "Create".to_string(),
1367            update_endpoint: Some("/update".to_string()),
1368            update_schema: Some("Update".to_string()),
1369            read_endpoint: "/read".to_string(),
1370            read_schema: "Read".to_string(),
1371            read_response_schema: Some("ReadResp".to_string()),
1372            delete_endpoint: "/delete".to_string(),
1373            delete_schema: "Delete".to_string(),
1374        };
1375        let info = CrudInfo::from(&mapping);
1376        assert_eq!(info.create_endpoint, "/create");
1377        assert_eq!(info.update_endpoint, Some("/update".to_string()));
1378        assert_eq!(info.read_response_schema, Some("ReadResp".to_string()));
1379    }
1380
1381    #[test]
1382    fn auth_info_from_auth_config() {
1383        let config = crate::spec::AuthConfig {
1384            token_field: "token".to_string(),
1385            env_var: "TOKEN".to_string(),
1386            gateway_url_field: "gw".to_string(),
1387            gateway_env_var: "GW".to_string(),
1388        };
1389        let info = AuthInfo::from(&config);
1390        assert_eq!(info.token_field, "token");
1391        assert_eq!(info.env_var, "TOKEN");
1392        assert!(info.has_token());
1393        assert!(info.has_gateway());
1394    }
1395
1396    #[test]
1397    fn auth_info_from_default_auth_config() {
1398        let config = crate::spec::AuthConfig::default();
1399        let info = AuthInfo::from(&config);
1400        assert!(!info.has_token());
1401        assert!(!info.has_gateway());
1402    }
1403
1404    #[test]
1405    fn iac_resource_display() {
1406        let r = crate::testing::test_resource("secret");
1407        assert_eq!(r.to_string(), "resource<secret> (3 attrs)");
1408    }
1409
1410    #[test]
1411    fn iac_data_source_display() {
1412        let ds = crate::testing::test_data_source("config");
1413        assert_eq!(ds.to_string(), "data_source<config> (2 attrs)");
1414    }
1415
1416    #[test]
1417    fn identity_info_from_identity_config() {
1418        let config = crate::spec::IdentityConfig {
1419            id_field: "name".to_string(),
1420            import_field: Some("path".to_string()),
1421            force_new_fields: vec!["name".to_string()],
1422        };
1423        let info = IdentityInfo::from(&config);
1424        assert_eq!(info.id_field, "name");
1425        assert_eq!(info.import_field, "path");
1426        assert_eq!(info.force_replace_fields, vec!["name"]);
1427    }
1428
1429    #[test]
1430    fn identity_info_from_config_defaults_import_field() {
1431        let config = crate::spec::IdentityConfig {
1432            id_field: "my_id".to_string(),
1433            import_field: None,
1434            force_new_fields: vec![],
1435        };
1436        let info = IdentityInfo::from(&config);
1437        assert_eq!(info.import_field, "my_id");
1438    }
1439
1440    #[test]
1441    fn iac_attribute_default() {
1442        let attr = IacAttribute::default();
1443        assert!(attr.api_name.is_empty());
1444        assert!(attr.canonical_name.is_empty());
1445        assert!(attr.description.is_empty());
1446        assert_eq!(attr.iac_type, IacType::String);
1447        assert!(!attr.required);
1448        assert!(!attr.optional);
1449        assert!(!attr.computed);
1450        assert!(!attr.sensitive);
1451        assert!(!attr.json_encoded);
1452        assert!(!attr.immutable);
1453        assert!(attr.default_value.is_none());
1454        assert!(attr.enum_values.is_none());
1455        assert!(attr.read_path.is_none());
1456        assert!(!attr.update_only);
1457    }
1458
1459    #[test]
1460    fn iac_type_is_scalar() {
1461        assert!(IacType::String.is_scalar());
1462        assert!(IacType::Integer.is_scalar());
1463        assert!(IacType::Float.is_scalar());
1464        assert!(IacType::Numeric.is_scalar());
1465        assert!(IacType::Boolean.is_scalar());
1466        assert!(!IacType::List(Box::new(IacType::String)).is_scalar());
1467        assert!(!IacType::Set(Box::new(IacType::String)).is_scalar());
1468        assert!(!IacType::Map(Box::new(IacType::String)).is_scalar());
1469        assert!(
1470            !IacType::Object {
1471                name: "X".into(),
1472                fields: vec![]
1473            }
1474            .is_scalar()
1475        );
1476        assert!(!IacType::Any.is_scalar());
1477    }
1478
1479    #[test]
1480    fn iac_type_is_composite() {
1481        assert!(IacType::List(Box::new(IacType::String)).is_composite());
1482        assert!(IacType::Set(Box::new(IacType::Integer)).is_composite());
1483        assert!(IacType::Map(Box::new(IacType::Boolean)).is_composite());
1484        assert!(
1485            IacType::Object {
1486                name: "X".into(),
1487                fields: vec![]
1488            }
1489            .is_composite()
1490        );
1491        assert!(!IacType::String.is_composite());
1492        assert!(!IacType::Integer.is_composite());
1493        assert!(!IacType::Any.is_composite());
1494    }
1495
1496    #[test]
1497    fn iac_type_enum_is_neither_scalar_nor_composite() {
1498        let enum_type = IacType::Enum {
1499            values: vec!["a".into()],
1500            underlying: Box::new(IacType::String),
1501        };
1502        assert!(!enum_type.is_scalar());
1503        assert!(!enum_type.is_composite());
1504    }
1505
1506    #[test]
1507    fn iac_type_inner_type_list() {
1508        let t = IacType::List(Box::new(IacType::String));
1509        assert_eq!(t.inner_type(), Some(&IacType::String));
1510    }
1511
1512    #[test]
1513    fn iac_type_inner_type_set() {
1514        let t = IacType::Set(Box::new(IacType::Integer));
1515        assert_eq!(t.inner_type(), Some(&IacType::Integer));
1516    }
1517
1518    #[test]
1519    fn iac_type_inner_type_map() {
1520        let t = IacType::Map(Box::new(IacType::Boolean));
1521        assert_eq!(t.inner_type(), Some(&IacType::Boolean));
1522    }
1523
1524    #[test]
1525    fn iac_type_inner_type_enum() {
1526        let t = IacType::Enum {
1527            values: vec!["a".into()],
1528            underlying: Box::new(IacType::String),
1529        };
1530        assert_eq!(t.inner_type(), Some(&IacType::String));
1531    }
1532
1533    #[test]
1534    fn iac_type_inner_type_scalar_none() {
1535        assert!(IacType::String.inner_type().is_none());
1536        assert!(IacType::Integer.inner_type().is_none());
1537        assert!(IacType::Boolean.inner_type().is_none());
1538        assert!(IacType::Any.inner_type().is_none());
1539    }
1540
1541    #[test]
1542    fn iac_type_any_is_neither_scalar_nor_composite() {
1543        assert!(!IacType::Any.is_scalar());
1544        assert!(!IacType::Any.is_composite());
1545    }
1546}