1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum IacType {
12 String,
13 Integer,
14 Float,
15 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 {
34 values: Vec<String>,
35 underlying: Box<IacType>,
36 },
37 Any,
38}
39
40impl IacType {
41 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
96#[allow(clippy::struct_excessive_bools)]
97pub struct IacAttribute {
98 pub api_name: String,
100 pub canonical_name: String,
102 pub description: String,
104 pub iac_type: IacType,
106 pub required: bool,
108 #[serde(default)]
115 pub optional: bool,
116 pub computed: bool,
118 pub sensitive: bool,
120 #[serde(default)]
126 pub json_encoded: bool,
127 pub immutable: bool,
129 pub default_value: Option<serde_json::Value>,
131 pub enum_values: Option<Vec<String>>,
133 pub read_path: Option<String>,
136 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 #[must_use]
168 pub fn is_input(&self) -> bool {
169 self.required || self.optional || !self.computed
170 }
171
172 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct CrudInfo {
203 pub create_endpoint: String,
205 pub create_schema: String,
207 pub update_endpoint: Option<String>,
209 pub update_schema: Option<String>,
211 pub read_endpoint: String,
213 pub read_schema: String,
215 pub read_response_schema: Option<String>,
217 pub delete_endpoint: String,
219 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#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct IdentityInfo {
242 pub id_field: String,
244 pub import_field: String,
246 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
263pub trait HasAttributes {
268 fn attributes(&self) -> &[IacAttribute];
270
271 #[must_use]
276 fn input_attributes(&self) -> Vec<&IacAttribute> {
277 self.attributes().iter().filter(|a| a.is_input()).collect()
278 }
279
280 #[must_use]
282 fn output_attributes(&self) -> Vec<&IacAttribute> {
283 self.attributes().iter().filter(|a| a.is_output()).collect()
284 }
285
286 #[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 #[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#[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 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct IacAction {
361 pub name: String,
363 pub description: String,
365 pub category: String,
367 pub endpoint: String,
369 pub schema: String,
371 pub response_schema: Option<String>,
373 pub mutating: bool,
379 pub sensitive_response_fields: Vec<String>,
382 pub attributes: Vec<IacAttribute>,
384 #[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#[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 #[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 #[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#[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
474pub struct AuthInfo {
475 pub token_field: String,
477 pub env_var: String,
479 pub gateway_url_field: String,
481 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 #[must_use]
499 pub fn has_token(&self) -> bool {
500 !self.token_field.is_empty()
501 }
502
503 #[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 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 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 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 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 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 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 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 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}