1use std::collections::{BTreeMap, HashSet};
2
3use openapi_forge::{Field, Spec};
4
5use crate::error::IacForgeError;
6use crate::ir::{
7 AuthInfo, CrudInfo, IacAction, IacAttribute, IacDataSource, IacProvider, IacResource,
8 IdentityInfo,
9};
10use crate::naming::to_snake_case;
11use crate::spec::{DataSourceSpec, FieldOverride, ProviderDefaults, ProviderSpec, ResourceSpec};
12use crate::type_map::{apply_enum_constraint, openapi_to_iac};
13
14#[allow(clippy::fn_params_excessive_bools)]
26fn build_attribute(
27 field: &Field,
28 override_cfg: Option<&FieldOverride>,
29 force_new_fields: &[String],
30 reverse_mapping: &BTreeMap<String, String>,
31 is_resource: bool,
32 update_only: bool,
33) -> Option<IacAttribute> {
34 if override_cfg.is_some_and(|o| o.skip) {
35 return None;
36 }
37
38 let canonical = to_snake_case(&field.name);
39
40 let computed = override_cfg.is_some_and(|o| o.computed);
41 let sensitive = override_cfg.is_some_and(|o| o.sensitive);
42
43 let immutable = if is_resource {
44 override_cfg.is_some_and(|o| o.force_new) || force_new_fields.contains(&field.name)
45 } else {
46 false
47 };
48
49 let type_override = override_cfg.and_then(|o| o.type_override.as_deref());
50 let iac_type = openapi_to_iac(&field.type_info, type_override);
51 let iac_type = apply_enum_constraint(iac_type, &field.enum_values);
52
53 let (required, computed_final, update_only_final) = if is_resource {
54 let req = if computed { false } else { field.required };
55 (req, computed, update_only)
56 } else {
57 (false, computed || !field.required, false)
59 };
60
61 let description = override_cfg
62 .and_then(|o| o.description.clone())
63 .or_else(|| field.description.clone())
64 .unwrap_or_default();
65
66 let read_path = reverse_mapping.get(&canonical).cloned();
67
68 Some(IacAttribute {
69 api_name: field.name.clone(),
70 canonical_name: canonical,
71 description,
72 iac_type,
73 required,
74 optional: !required,
75 computed: computed_final,
76 sensitive,
77 json_encoded: false,
78 immutable,
79 default_value: field.default.clone(),
80 enum_values: field.enum_values.clone(),
81 read_path,
82 update_only: update_only_final,
83 })
84}
85
86pub fn resolve_resource(
96 resource: &ResourceSpec,
97 api: &Spec,
98 defaults: &ProviderDefaults,
99) -> Result<IacResource, IacForgeError> {
100 if resource.is_action() {
101 return Err(IacForgeError::ValidationError(format!(
102 "resource '{}' is kind = \"action\"; dispatch to resolve_action instead",
103 resource.resource.name
104 )));
105 }
106 let create_fields = api.fields(&resource.crud.create_schema)?;
107
108 let update_required: HashSet<String> =
109 if let Some(ref update_schema) = resource.crud.update_schema {
110 api.fields(update_schema)
111 .unwrap_or_default()
112 .iter()
113 .filter(|f| f.required)
114 .map(|f| f.name.clone())
115 .collect()
116 } else {
117 HashSet::new()
118 };
119
120 let skip_fields: HashSet<&str> = defaults.skip_fields.iter().map(String::as_str).collect();
121
122 let reverse_mapping: BTreeMap<String, String> = resource
124 .read_mapping
125 .iter()
126 .map(|(json_path, tf_name)| (to_snake_case(tf_name), json_path.clone()))
127 .collect();
128
129 let mut attributes = Vec::new();
130
131 for field in &create_fields {
132 if skip_fields.contains(field.name.as_str()) {
133 continue;
134 }
135
136 let override_cfg = resource.fields.get(&field.name);
137
138 let is_update_required = update_required.contains(&field.name);
139 let update_only = is_update_required && !field.required;
140
141 if let Some(attr) = build_attribute(
142 field,
143 override_cfg,
144 &resource.identity.force_new_fields,
145 &reverse_mapping,
146 true,
147 update_only,
148 ) {
149 attributes.push(attr);
150 }
151 }
152
153 Ok(IacResource {
154 name: resource.resource.name.clone(),
155 description: resource.resource.description.clone(),
156 category: resource.resource.category.clone(),
157 crud: CrudInfo::from(&resource.crud),
158 attributes,
159 identity: IdentityInfo::from(&resource.identity),
160 read_mapping: resource.read_mapping.clone(),
161 })
162}
163
164pub fn resolve_action(
180 spec: &ResourceSpec,
181 api: &Spec,
182 defaults: &ProviderDefaults,
183) -> Result<IacAction, IacForgeError> {
184 if !spec.is_action() {
185 return Err(IacForgeError::ValidationError(format!(
186 "resource '{}' has no kind = \"action\"; dispatch to resolve_resource instead",
187 spec.resource.name
188 )));
189 }
190 let action = spec.action.as_ref().ok_or_else(|| {
191 IacForgeError::ValidationError(format!(
192 "resource '{}' has kind = \"action\" but no [action] table",
193 spec.resource.name
194 ))
195 })?;
196
197 let request_fields = api.fields(&action.schema)?;
198
199 let skip_fields: HashSet<&str> = defaults.skip_fields.iter().map(String::as_str).collect();
200
201 let reverse_mapping: BTreeMap<String, String> = BTreeMap::new();
204
205 let mut attributes = Vec::new();
206 for field in &request_fields {
207 if skip_fields.contains(field.name.as_str()) {
208 continue;
209 }
210 let override_cfg = spec.fields.get(&field.name);
211
212 if let Some(attr) = build_attribute(
213 field,
214 override_cfg,
215 &[],
218 &reverse_mapping,
219 true,
222 false,
223 ) {
224 attributes.push(attr);
225 }
226 }
227
228 Ok(IacAction {
229 name: spec.resource.name.clone(),
230 description: spec.resource.description.clone(),
231 category: spec.resource.category.clone(),
232 endpoint: action.endpoint.clone(),
233 schema: action.schema.clone(),
234 response_schema: action.response_schema.clone(),
235 mutating: action.mutating.unwrap_or(true),
236 sensitive_response_fields: action.sensitive_response_fields.clone(),
237 attributes,
238 sdk_method: action.sdk_method.clone(),
239 })
240}
241
242pub fn resolve_data_source(
248 ds: &DataSourceSpec,
249 api: &Spec,
250 defaults: &ProviderDefaults,
251) -> Result<IacDataSource, IacForgeError> {
252 let read_fields = api.fields(&ds.read.schema)?;
253
254 let skip_fields: HashSet<&str> = defaults.skip_fields.iter().map(String::as_str).collect();
255
256 let reverse_mapping: BTreeMap<String, String> = ds
257 .read_mapping
258 .iter()
259 .map(|(json_path, tf_name)| (to_snake_case(tf_name), json_path.clone()))
260 .collect();
261
262 let mut attributes = Vec::new();
263
264 for field in &read_fields {
265 if skip_fields.contains(field.name.as_str()) {
266 continue;
267 }
268
269 let override_cfg = ds.fields.get(&field.name);
270
271 if let Some(attr) =
272 build_attribute(field, override_cfg, &[], &reverse_mapping, false, false)
273 {
274 attributes.push(attr);
275 }
276 }
277
278 Ok(IacDataSource {
279 name: ds.data_source.name.clone(),
280 description: ds.data_source.description.clone(),
281 read_endpoint: ds.read.endpoint.clone(),
282 read_schema: ds.read.schema.clone(),
283 read_response_schema: ds.read.response_schema.clone(),
284 attributes,
285 read_mapping: ds.read_mapping.clone(),
286 })
287}
288
289#[must_use]
291pub fn resolve_provider(provider: &ProviderSpec) -> IacProvider {
292 IacProvider {
293 name: provider.provider.name.clone(),
294 description: provider.provider.description.clone(),
295 version: provider.provider.version.clone(),
296 auth: AuthInfo::from(&provider.auth),
297 skip_fields: provider.defaults.skip_fields.clone(),
298 platform_config: provider.platforms.clone(),
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use std::str::FromStr;
310
311 fn make_test_spec() -> (ResourceSpec, Spec) {
312 let toml_str = r#"
313[resource]
314name = "akeyless_static_secret"
315description = "Static secret"
316category = "secret"
317
318[crud]
319create_endpoint = "/create-secret"
320create_schema = "CreateSecret"
321read_endpoint = "/get-secret-value"
322read_schema = "GetSecretValue"
323delete_endpoint = "/delete-item"
324delete_schema = "DeleteItem"
325
326[identity]
327id_field = "name"
328force_new_fields = ["name"]
329
330[fields]
331token = { skip = true }
332delete_protection = { type_override = "bool" }
333
334[read_mapping]
335"item_name" = "name"
336"item_tags" = "tags"
337"#;
338 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse resource");
339
340 let api_str = r#"
341openapi: "3.0.0"
342info:
343 title: Test
344 version: "1.0"
345paths:
346 /create-secret:
347 post:
348 operationId: createSecret
349 requestBody:
350 content:
351 application/json:
352 schema:
353 $ref: '#/components/schemas/CreateSecret'
354 responses:
355 "200":
356 description: ok
357 /get-secret-value:
358 post:
359 operationId: getSecretValue
360 responses:
361 "200":
362 description: ok
363 /delete-item:
364 post:
365 operationId: deleteItem
366 responses:
367 "200":
368 description: ok
369components:
370 schemas:
371 CreateSecret:
372 type: object
373 required:
374 - name
375 - value
376 properties:
377 name:
378 type: string
379 description: Secret name
380 value:
381 type: string
382 description: Secret value
383 tags:
384 type: array
385 items:
386 type: string
387 token:
388 type: string
389 delete_protection:
390 type: string
391 GetSecretValue:
392 type: object
393 properties:
394 names:
395 type: array
396 items:
397 type: string
398 DeleteItem:
399 type: object
400 properties:
401 name:
402 type: string
403"#;
404 let api = Spec::from_str(api_str).expect("parse api");
405 (resource, api)
406 }
407
408 #[test]
409 fn resolve_resource_basic() {
410 let (resource, api) = make_test_spec();
411 let defaults = ProviderDefaults::default();
412 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
413
414 assert_eq!(iac.name, "akeyless_static_secret");
415 assert_eq!(iac.category, "secret");
416 assert_eq!(iac.identity.id_field, "name");
417 assert_eq!(iac.identity.force_replace_fields, vec!["name"]);
418 }
419
420 #[test]
421 fn resolve_resource_skips_token() {
422 let (resource, api) = make_test_spec();
423 let defaults = ProviderDefaults::default();
424 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
425
426 assert!(iac.attributes.iter().all(|a| a.api_name != "token"));
427 }
428
429 #[test]
430 fn resolve_resource_type_override() {
431 let (resource, api) = make_test_spec();
432 let defaults = ProviderDefaults::default();
433 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
434
435 let dp = iac
436 .attributes
437 .iter()
438 .find(|a| a.canonical_name == "delete_protection")
439 .expect("dp");
440 assert_eq!(dp.iac_type, crate::ir::IacType::Boolean);
441 }
442
443 #[test]
444 fn resolve_resource_required_fields() {
445 let (resource, api) = make_test_spec();
446 let defaults = ProviderDefaults::default();
447 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
448
449 let name = iac
450 .attributes
451 .iter()
452 .find(|a| a.canonical_name == "name")
453 .expect("name");
454 assert!(name.required);
455 assert!(name.immutable);
456 }
457
458 #[test]
459 fn resolve_resource_read_mapping() {
460 let (resource, api) = make_test_spec();
461 let defaults = ProviderDefaults::default();
462 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
463
464 let name = iac
465 .attributes
466 .iter()
467 .find(|a| a.canonical_name == "name")
468 .expect("name");
469 assert_eq!(name.read_path, Some("item_name".to_string()));
470
471 let tags = iac
472 .attributes
473 .iter()
474 .find(|a| a.canonical_name == "tags")
475 .expect("tags");
476 assert_eq!(tags.read_path, Some("item_tags".to_string()));
477 }
478
479 #[test]
480 fn resolve_resource_read_mapping_populates_field() {
481 let (resource, api) = make_test_spec();
482 let defaults = ProviderDefaults::default();
483 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
484
485 assert_eq!(iac.read_mapping.get("item_name"), Some(&"name".to_string()));
487 assert_eq!(iac.read_mapping.get("item_tags"), Some(&"tags".to_string()));
488 assert_eq!(iac.read_mapping.len(), 2);
489 }
490
491 #[test]
492 fn resolve_data_source_read_mapping_populates_field() {
493 let toml_str = r#"
494[data_source]
495name = "mapped_ds"
496description = "DS with read mapping"
497
498[read]
499endpoint = "/read"
500schema = "ReadSchema"
501
502[read_mapping]
503"resp_name" = "name"
504"#;
505 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
506
507 let api_str = r#"
508openapi: "3.0.0"
509info: { title: Test, version: "1.0" }
510paths: {}
511components:
512 schemas:
513 ReadSchema:
514 type: object
515 required: [name]
516 properties:
517 name: { type: string }
518"#;
519 let api = Spec::from_str(api_str).expect("parse");
520 let defaults = ProviderDefaults::default();
521 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
522
523 assert_eq!(iac.read_mapping.get("resp_name"), Some(&"name".to_string()));
524 assert_eq!(iac.read_mapping.len(), 1);
525 }
526
527 #[test]
528 fn resolve_resource_list_type() {
529 let (resource, api) = make_test_spec();
530 let defaults = ProviderDefaults::default();
531 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
532
533 let tags = iac
534 .attributes
535 .iter()
536 .find(|a| a.canonical_name == "tags")
537 .expect("tags");
538 assert_eq!(
539 tags.iac_type,
540 crate::ir::IacType::List(Box::new(crate::ir::IacType::String))
541 );
542 }
543
544 #[test]
545 fn resolve_resource_global_skip() {
546 let (resource, api) = make_test_spec();
547 let defaults = ProviderDefaults {
548 skip_fields: vec!["value".to_string()],
549 };
550 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
551
552 assert!(iac.attributes.iter().all(|a| a.api_name != "value"));
553 }
554
555 #[test]
556 fn resolve_resource_update_only() {
557 let (resource, api) = make_test_spec();
558 let defaults = ProviderDefaults::default();
559 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
560
561 for attr in &iac.attributes {
563 assert!(
564 !attr.update_only,
565 "field {} should not be update_only",
566 attr.canonical_name
567 );
568 }
569 }
570
571 #[test]
572 fn resolve_provider_basic() {
573 let toml_str = r#"
574[provider]
575name = "akeyless"
576description = "Akeyless Vault Provider"
577version = "1.0.0"
578sdk_import = "github.com/akeylesslabs/akeyless-go/v5"
579
580[auth]
581token_field = "token"
582env_var = "AKEYLESS_ACCESS_TOKEN"
583gateway_url_field = "api_gateway_address"
584gateway_env_var = "AKEYLESS_GATEWAY"
585
586[defaults]
587skip_fields = ["token", "uid-token"]
588"#;
589 let provider: ProviderSpec = toml::from_str(toml_str).expect("parse");
590 let iac = resolve_provider(&provider);
591
592 assert_eq!(iac.name, "akeyless");
593 assert_eq!(iac.auth.env_var, "AKEYLESS_ACCESS_TOKEN");
594 assert_eq!(iac.skip_fields.len(), 2);
595 }
596
597 #[test]
598 fn resolve_data_source_basic() {
599 let toml_str = r#"
600[data_source]
601name = "akeyless_auth_method"
602description = "Read an auth method"
603
604[read]
605endpoint = "/get-auth-method"
606schema = "GetAuthMethod"
607
608[fields]
609token = { skip = true }
610
611[read_mapping]
612"auth_method_access_id" = "access_id"
613"#;
614 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
615
616 let api_str = r#"
617openapi: "3.0.0"
618info: { title: Test, version: "1.0" }
619paths:
620 /get-auth-method:
621 post:
622 operationId: getAuthMethod
623 requestBody:
624 content:
625 application/json:
626 schema:
627 $ref: '#/components/schemas/GetAuthMethod'
628 responses:
629 "200": { description: ok }
630components:
631 schemas:
632 GetAuthMethod:
633 type: object
634 required: [name]
635 properties:
636 name: { type: string, description: "Auth method name" }
637 access_id: { type: string, description: "Access ID" }
638 token: { type: string }
639"#;
640 let api = Spec::from_str(api_str).expect("parse");
641 let defaults = ProviderDefaults::default();
642 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
643
644 assert_eq!(iac.name, "akeyless_auth_method");
645 assert!(iac.attributes.iter().all(|a| a.api_name != "token"));
646
647 let access = iac
648 .attributes
649 .iter()
650 .find(|a| a.canonical_name == "access_id")
651 .expect("access_id");
652 assert!(access.computed);
653 assert_eq!(access.read_path, Some("auth_method_access_id".to_string()));
654 }
655
656 #[test]
657 fn resolve_data_source_update_only_always_false() {
658 let toml_str = r#"
659[data_source]
660name = "test_ds"
661description = "Test"
662
663[read]
664endpoint = "/read"
665schema = "ReadSchema"
666"#;
667 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
668
669 let api_str = r#"
670openapi: "3.0.0"
671info: { title: Test, version: "1.0" }
672paths:
673 /read:
674 post:
675 operationId: read
676 responses:
677 "200": { description: ok }
678components:
679 schemas:
680 ReadSchema:
681 type: object
682 properties:
683 name: { type: string }
684"#;
685 let api = Spec::from_str(api_str).expect("parse");
686 let defaults = ProviderDefaults::default();
687 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
688
689 for attr in &iac.attributes {
690 assert!(!attr.update_only);
691 }
692 }
693
694 #[test]
695 fn resolve_resource_missing_schema() {
696 let toml_str = r#"
697[resource]
698name = "test"
699description = "Test"
700category = "test"
701
702[crud]
703create_endpoint = "/create"
704create_schema = "NonExistentSchema"
705read_endpoint = "/read"
706read_schema = "Read"
707delete_endpoint = "/delete"
708delete_schema = "Delete"
709
710[identity]
711id_field = "id"
712"#;
713 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
714
715 let api_str = r#"
716openapi: "3.0.0"
717info: { title: Test, version: "1.0" }
718paths: {}
719components:
720 schemas:
721 Read:
722 type: object
723 properties:
724 id: { type: string }
725 Delete:
726 type: object
727 properties:
728 id: { type: string }
729"#;
730 let api = Spec::from_str(api_str).expect("parse");
731 let defaults = ProviderDefaults::default();
732 let result = resolve_resource(&resource, &api, &defaults);
733
734 assert!(result.is_err(), "should fail when create_schema is missing");
735 }
736
737 #[test]
738 fn resolve_resource_empty_fields() {
739 let toml_str = r#"
740[resource]
741name = "empty_resource"
742description = "Has no fields"
743category = "test"
744
745[crud]
746create_endpoint = "/create"
747create_schema = "EmptySchema"
748read_endpoint = "/read"
749read_schema = "Read"
750delete_endpoint = "/delete"
751delete_schema = "Delete"
752
753[identity]
754id_field = "id"
755"#;
756 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
757
758 let api_str = r#"
759openapi: "3.0.0"
760info: { title: Test, version: "1.0" }
761paths: {}
762components:
763 schemas:
764 EmptySchema:
765 type: object
766 properties: {}
767 Read:
768 type: object
769 properties: {}
770 Delete:
771 type: object
772 properties: {}
773"#;
774 let api = Spec::from_str(api_str).expect("parse");
775 let defaults = ProviderDefaults::default();
776 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
777
778 assert!(
779 iac.attributes.is_empty(),
780 "empty schema should produce zero attributes"
781 );
782 }
783
784 #[test]
785 fn resolve_data_source_missing_schema() {
786 let toml_str = r#"
787[data_source]
788name = "test_ds"
789description = "Test"
790
791[read]
792endpoint = "/read"
793schema = "NonExistentSchema"
794"#;
795 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
796
797 let api_str = r#"
798openapi: "3.0.0"
799info: { title: Test, version: "1.0" }
800paths: {}
801components:
802 schemas: {}
803"#;
804 let api = Spec::from_str(api_str).expect("parse");
805 let defaults = ProviderDefaults::default();
806 let result = resolve_data_source(&ds, &api, &defaults);
807
808 assert!(result.is_err(), "should fail when read schema is missing");
809 }
810
811 #[test]
812 fn resolve_resource_update_schema_different_required_fields() {
813 let toml_str = r#"
814[resource]
815name = "test_update"
816description = "Test update_only"
817category = "test"
818
819[crud]
820create_endpoint = "/create"
821create_schema = "CreateSchema"
822update_endpoint = "/update"
823update_schema = "UpdateSchema"
824read_endpoint = "/read"
825read_schema = "ReadSchema"
826delete_endpoint = "/delete"
827delete_schema = "DeleteSchema"
828
829[identity]
830id_field = "id"
831"#;
832 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
833
834 let api_str = r#"
835openapi: "3.0.0"
836info: { title: Test, version: "1.0" }
837paths: {}
838components:
839 schemas:
840 CreateSchema:
841 type: object
842 required: [name]
843 properties:
844 name: { type: string }
845 extra_field: { type: string }
846 UpdateSchema:
847 type: object
848 required: [extra_field]
849 properties:
850 name: { type: string }
851 extra_field: { type: string }
852 ReadSchema:
853 type: object
854 properties:
855 name: { type: string }
856 DeleteSchema:
857 type: object
858 properties:
859 name: { type: string }
860"#;
861 let api = Spec::from_str(api_str).expect("parse");
862 let defaults = ProviderDefaults::default();
863 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
864
865 let extra = iac
868 .attributes
869 .iter()
870 .find(|a| a.canonical_name == "extra_field")
871 .expect("extra_field");
872 assert!(extra.update_only, "extra_field should be update_only");
873
874 let name = iac
877 .attributes
878 .iter()
879 .find(|a| a.canonical_name == "name")
880 .expect("name");
881 assert!(!name.update_only, "name should NOT be update_only");
882 }
883
884 #[test]
885 fn resolve_resource_computed_and_required_makes_required_false() {
886 let toml_str = r#"
887[resource]
888name = "test_computed_req"
889description = "Test computed+required"
890category = "test"
891
892[crud]
893create_endpoint = "/create"
894create_schema = "Schema"
895read_endpoint = "/read"
896read_schema = "ReadSchema"
897delete_endpoint = "/delete"
898delete_schema = "DeleteSchema"
899
900[identity]
901id_field = "id"
902
903[fields]
904server_id = { computed = true }
905"#;
906 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
907
908 let api_str = r#"
909openapi: "3.0.0"
910info: { title: Test, version: "1.0" }
911paths: {}
912components:
913 schemas:
914 Schema:
915 type: object
916 required: [server_id]
917 properties:
918 server_id: { type: string }
919 ReadSchema:
920 type: object
921 properties:
922 server_id: { type: string }
923 DeleteSchema:
924 type: object
925 properties:
926 server_id: { type: string }
927"#;
928 let api = Spec::from_str(api_str).expect("parse");
929 let defaults = ProviderDefaults::default();
930 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
931
932 let field = iac
933 .attributes
934 .iter()
935 .find(|a| a.canonical_name == "server_id")
936 .expect("server_id");
937 assert!(!field.required, "computed field should have required=false");
939 assert!(field.computed, "field should still be computed");
940 }
941
942 #[test]
943 fn resolve_resource_all_fields_skipped_by_provider_defaults() {
944 let toml_str = r#"
945[resource]
946name = "all_skipped"
947description = "All fields skipped"
948category = "test"
949
950[crud]
951create_endpoint = "/create"
952create_schema = "Schema"
953read_endpoint = "/read"
954read_schema = "ReadSchema"
955delete_endpoint = "/delete"
956delete_schema = "DeleteSchema"
957
958[identity]
959id_field = "id"
960"#;
961 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
962
963 let api_str = r#"
964openapi: "3.0.0"
965info: { title: Test, version: "1.0" }
966paths: {}
967components:
968 schemas:
969 Schema:
970 type: object
971 properties:
972 token: { type: string }
973 uid_token: { type: string }
974 ReadSchema:
975 type: object
976 properties: {}
977 DeleteSchema:
978 type: object
979 properties: {}
980"#;
981 let api = Spec::from_str(api_str).expect("parse");
982 let defaults = ProviderDefaults {
983 skip_fields: vec!["token".to_string(), "uid_token".to_string()],
984 };
985 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
986
987 assert!(
988 iac.attributes.is_empty(),
989 "all fields should be skipped by provider defaults"
990 );
991 }
992
993 #[test]
994 fn resolve_resource_per_field_and_provider_skip_overlap() {
995 let toml_str = r#"
996[resource]
997name = "double_skip"
998description = "Both skip mechanisms"
999category = "test"
1000
1001[crud]
1002create_endpoint = "/create"
1003create_schema = "Schema"
1004read_endpoint = "/read"
1005read_schema = "ReadSchema"
1006delete_endpoint = "/delete"
1007delete_schema = "DeleteSchema"
1008
1009[identity]
1010id_field = "id"
1011
1012[fields]
1013token = { skip = true }
1014"#;
1015 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1016
1017 let api_str = r#"
1018openapi: "3.0.0"
1019info: { title: Test, version: "1.0" }
1020paths: {}
1021components:
1022 schemas:
1023 Schema:
1024 type: object
1025 properties:
1026 token: { type: string }
1027 name: { type: string }
1028 ReadSchema:
1029 type: object
1030 properties: {}
1031 DeleteSchema:
1032 type: object
1033 properties: {}
1034"#;
1035 let api = Spec::from_str(api_str).expect("parse");
1036 let defaults = ProviderDefaults {
1037 skip_fields: vec!["token".to_string()],
1038 };
1039 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1040
1041 assert_eq!(iac.attributes.len(), 1);
1043 assert_eq!(iac.attributes[0].canonical_name, "name");
1044 }
1045
1046 #[test]
1047 fn resolve_data_source_all_fields_computed() {
1048 let toml_str = r#"
1049[data_source]
1050name = "all_computed_ds"
1051description = "All computed"
1052
1053[read]
1054endpoint = "/read"
1055schema = "ReadSchema"
1056"#;
1057 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
1058
1059 let api_str = r#"
1060openapi: "3.0.0"
1061info: { title: Test, version: "1.0" }
1062paths: {}
1063components:
1064 schemas:
1065 ReadSchema:
1066 type: object
1067 properties:
1068 value: { type: string }
1069 count: { type: integer }
1070"#;
1071 let api = Spec::from_str(api_str).expect("parse");
1072 let defaults = ProviderDefaults::default();
1073 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
1074
1075 for attr in &iac.attributes {
1077 assert!(
1078 attr.computed,
1079 "field {} should be computed",
1080 attr.canonical_name
1081 );
1082 assert!(
1083 !attr.required,
1084 "field {} should not be required",
1085 attr.canonical_name
1086 );
1087 assert!(
1088 !attr.update_only,
1089 "data source field should never be update_only"
1090 );
1091 }
1092 }
1093
1094 #[test]
1095 fn resolve_provider_empty_auth() {
1096 let toml_str = r#"
1097[provider]
1098name = "minimal"
1099description = "Minimal provider"
1100version = "0.1.0"
1101"#;
1102 let provider: ProviderSpec = toml::from_str(toml_str).expect("parse");
1103 let iac = resolve_provider(&provider);
1104
1105 assert_eq!(iac.name, "minimal");
1106 assert!(iac.auth.token_field.is_empty());
1107 assert!(iac.auth.env_var.is_empty());
1108 assert!(iac.auth.gateway_url_field.is_empty());
1109 assert!(iac.auth.gateway_env_var.is_empty());
1110 assert!(!iac.auth.has_token());
1111 assert!(!iac.auth.has_gateway());
1112 assert!(iac.skip_fields.is_empty());
1113 assert!(iac.platform_config.is_empty());
1114 }
1115
1116 #[test]
1117 fn resolve_provider_with_platform_config() {
1118 let toml_str = r#"
1119[provider]
1120name = "platformed"
1121description = "With platforms"
1122version = "1.0.0"
1123
1124[auth]
1125token_field = "token"
1126env_var = "TOKEN"
1127
1128[defaults]
1129skip_fields = []
1130
1131[platforms.terraform]
1132sdk_import = "github.com/example/sdk"
1133
1134[platforms.pulumi]
1135module = "index"
1136"#;
1137 let provider: ProviderSpec = toml::from_str(toml_str).expect("parse");
1138 let iac = resolve_provider(&provider);
1139
1140 assert_eq!(iac.platform_config.len(), 2);
1141 assert!(iac.platform_config.contains_key("terraform"));
1142 assert!(iac.platform_config.contains_key("pulumi"));
1143 }
1144
1145 #[test]
1146 fn resolve_resource_import_field_defaults_to_id_field() {
1147 let toml_str = r#"
1148[resource]
1149name = "no_import_field"
1150description = "No import_field set"
1151category = "test"
1152
1153[crud]
1154create_endpoint = "/create"
1155create_schema = "Schema"
1156read_endpoint = "/read"
1157read_schema = "ReadSchema"
1158delete_endpoint = "/delete"
1159delete_schema = "DeleteSchema"
1160
1161[identity]
1162id_field = "my_id"
1163"#;
1164 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1165
1166 let api_str = r#"
1167openapi: "3.0.0"
1168info: { title: Test, version: "1.0" }
1169paths: {}
1170components:
1171 schemas:
1172 Schema:
1173 type: object
1174 properties:
1175 my_id: { type: string }
1176 ReadSchema:
1177 type: object
1178 properties: {}
1179 DeleteSchema:
1180 type: object
1181 properties: {}
1182"#;
1183 let api = Spec::from_str(api_str).expect("parse");
1184 let defaults = ProviderDefaults::default();
1185 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1186
1187 assert_eq!(iac.identity.id_field, "my_id");
1188 assert_eq!(iac.identity.import_field, "my_id");
1189 }
1190
1191 #[test]
1192 fn resolve_resource_explicit_import_field() {
1193 let toml_str = r#"
1194[resource]
1195name = "with_import_field"
1196description = "Explicit import_field"
1197category = "test"
1198
1199[crud]
1200create_endpoint = "/create"
1201create_schema = "Schema"
1202read_endpoint = "/read"
1203read_schema = "ReadSchema"
1204delete_endpoint = "/delete"
1205delete_schema = "DeleteSchema"
1206
1207[identity]
1208id_field = "name"
1209import_field = "path"
1210"#;
1211 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1212
1213 let api_str = r#"
1214openapi: "3.0.0"
1215info: { title: Test, version: "1.0" }
1216paths: {}
1217components:
1218 schemas:
1219 Schema:
1220 type: object
1221 properties:
1222 name: { type: string }
1223 ReadSchema:
1224 type: object
1225 properties: {}
1226 DeleteSchema:
1227 type: object
1228 properties: {}
1229"#;
1230 let api = Spec::from_str(api_str).expect("parse");
1231 let defaults = ProviderDefaults::default();
1232 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1233
1234 assert_eq!(iac.identity.id_field, "name");
1235 assert_eq!(iac.identity.import_field, "path");
1236 }
1237
1238 #[test]
1239 fn resolve_resource_sensitive_field() {
1240 let toml_str = r#"
1241[resource]
1242name = "with_sensitive"
1243description = "Has sensitive field"
1244category = "test"
1245
1246[crud]
1247create_endpoint = "/create"
1248create_schema = "Schema"
1249read_endpoint = "/read"
1250read_schema = "ReadSchema"
1251delete_endpoint = "/delete"
1252delete_schema = "DeleteSchema"
1253
1254[identity]
1255id_field = "name"
1256
1257[fields]
1258password = { sensitive = true }
1259"#;
1260 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1261
1262 let api_str = r#"
1263openapi: "3.0.0"
1264info: { title: Test, version: "1.0" }
1265paths: {}
1266components:
1267 schemas:
1268 Schema:
1269 type: object
1270 properties:
1271 name: { type: string }
1272 password: { type: string }
1273 ReadSchema:
1274 type: object
1275 properties: {}
1276 DeleteSchema:
1277 type: object
1278 properties: {}
1279"#;
1280 let api = Spec::from_str(api_str).expect("parse");
1281 let defaults = ProviderDefaults::default();
1282 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1283
1284 let pw = iac
1285 .attributes
1286 .iter()
1287 .find(|a| a.canonical_name == "password")
1288 .expect("password");
1289 assert!(pw.sensitive);
1290 }
1291
1292 #[test]
1293 fn resolve_resource_force_new_via_field_override() {
1294 let toml_str = r#"
1295[resource]
1296name = "force_new_override"
1297description = "Force new via field override"
1298category = "test"
1299
1300[crud]
1301create_endpoint = "/create"
1302create_schema = "Schema"
1303read_endpoint = "/read"
1304read_schema = "ReadSchema"
1305delete_endpoint = "/delete"
1306delete_schema = "DeleteSchema"
1307
1308[identity]
1309id_field = "name"
1310
1311[fields]
1312region = { force_new = true }
1313"#;
1314 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1315
1316 let api_str = r#"
1317openapi: "3.0.0"
1318info: { title: Test, version: "1.0" }
1319paths: {}
1320components:
1321 schemas:
1322 Schema:
1323 type: object
1324 properties:
1325 name: { type: string }
1326 region: { type: string }
1327 ReadSchema:
1328 type: object
1329 properties: {}
1330 DeleteSchema:
1331 type: object
1332 properties: {}
1333"#;
1334 let api = Spec::from_str(api_str).expect("parse");
1335 let defaults = ProviderDefaults::default();
1336 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1337
1338 let region = iac
1339 .attributes
1340 .iter()
1341 .find(|a| a.canonical_name == "region")
1342 .expect("region");
1343 assert!(
1344 region.immutable,
1345 "region should be immutable via force_new override"
1346 );
1347 }
1348
1349 #[test]
1350 fn resolve_resource_description_override() {
1351 let toml_str = r#"
1352[resource]
1353name = "desc_override"
1354description = "Test description override"
1355category = "test"
1356
1357[crud]
1358create_endpoint = "/create"
1359create_schema = "Schema"
1360read_endpoint = "/read"
1361read_schema = "ReadSchema"
1362delete_endpoint = "/delete"
1363delete_schema = "DeleteSchema"
1364
1365[identity]
1366id_field = "name"
1367
1368[fields]
1369name = { description = "Custom description" }
1370"#;
1371 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1372
1373 let api_str = r#"
1374openapi: "3.0.0"
1375info: { title: Test, version: "1.0" }
1376paths: {}
1377components:
1378 schemas:
1379 Schema:
1380 type: object
1381 properties:
1382 name: { type: string, description: "Original description" }
1383 ReadSchema:
1384 type: object
1385 properties: {}
1386 DeleteSchema:
1387 type: object
1388 properties: {}
1389"#;
1390 let api = Spec::from_str(api_str).expect("parse");
1391 let defaults = ProviderDefaults::default();
1392 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1393
1394 let name = iac
1395 .attributes
1396 .iter()
1397 .find(|a| a.canonical_name == "name")
1398 .expect("name");
1399 assert_eq!(name.description, "Custom description");
1400 }
1401
1402 #[test]
1403 fn resolve_data_source_immutable_always_false() {
1404 let toml_str = r#"
1405[data_source]
1406name = "ds_no_immutable"
1407description = "Data sources never have immutable fields"
1408
1409[read]
1410endpoint = "/read"
1411schema = "ReadSchema"
1412
1413[fields]
1414name = { force_new = true }
1415"#;
1416 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
1417
1418 let api_str = r#"
1419openapi: "3.0.0"
1420info: { title: Test, version: "1.0" }
1421paths: {}
1422components:
1423 schemas:
1424 ReadSchema:
1425 type: object
1426 required: [name]
1427 properties:
1428 name: { type: string }
1429"#;
1430 let api = Spec::from_str(api_str).expect("parse");
1431 let defaults = ProviderDefaults::default();
1432 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
1433
1434 for attr in &iac.attributes {
1435 assert!(
1436 !attr.immutable,
1437 "data source field should never be immutable"
1438 );
1439 }
1440 }
1441
1442 #[test]
1443 fn resolve_data_source_required_input_not_computed() {
1444 let toml_str = r#"
1445[data_source]
1446name = "ds_input"
1447description = "Data source with required input"
1448
1449[read]
1450endpoint = "/read"
1451schema = "ReadSchema"
1452"#;
1453 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
1454
1455 let api_str = r#"
1456openapi: "3.0.0"
1457info: { title: Test, version: "1.0" }
1458paths: {}
1459components:
1460 schemas:
1461 ReadSchema:
1462 type: object
1463 required: [name]
1464 properties:
1465 name: { type: string, description: "Lookup key" }
1466 result: { type: string, description: "Computed result" }
1467"#;
1468 let api = Spec::from_str(api_str).expect("parse");
1469 let defaults = ProviderDefaults::default();
1470 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
1471
1472 let name = iac
1473 .attributes
1474 .iter()
1475 .find(|a| a.canonical_name == "name")
1476 .expect("name");
1477 assert!(!name.computed, "required input should not be computed");
1479
1480 let result = iac
1481 .attributes
1482 .iter()
1483 .find(|a| a.canonical_name == "result")
1484 .expect("result");
1485 assert!(
1486 result.computed,
1487 "optional non-required field should be computed in data source"
1488 );
1489 }
1490
1491 #[test]
1492 fn resolve_data_source_global_skip_fields() {
1493 let toml_str = r#"
1494[data_source]
1495name = "ds_skip"
1496description = "DS with global skip"
1497
1498[read]
1499endpoint = "/read"
1500schema = "ReadSchema"
1501"#;
1502 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
1503
1504 let api_str = r#"
1505openapi: "3.0.0"
1506info: { title: Test, version: "1.0" }
1507paths: {}
1508components:
1509 schemas:
1510 ReadSchema:
1511 type: object
1512 required: [name]
1513 properties:
1514 name: { type: string }
1515 token: { type: string }
1516 uid_token: { type: string }
1517"#;
1518 let api = Spec::from_str(api_str).expect("parse");
1519 let defaults = ProviderDefaults {
1520 skip_fields: vec!["token".to_string(), "uid_token".to_string()],
1521 };
1522 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
1523 assert_eq!(iac.attributes.len(), 1);
1524 assert_eq!(iac.attributes[0].canonical_name, "name");
1525 }
1526
1527 #[test]
1528 fn resolve_resource_enum_field_from_openapi() {
1529 let toml_str = r#"
1530[resource]
1531name = "enum_res"
1532description = "Resource with enum field"
1533category = "test"
1534
1535[crud]
1536create_endpoint = "/create"
1537create_schema = "CreateSchema"
1538read_endpoint = "/read"
1539read_schema = "ReadSchema"
1540delete_endpoint = "/delete"
1541delete_schema = "DeleteSchema"
1542
1543[identity]
1544id_field = "name"
1545"#;
1546 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1547
1548 let api_str = r#"
1549openapi: "3.0.0"
1550info: { title: Test, version: "1.0" }
1551paths: {}
1552components:
1553 schemas:
1554 CreateSchema:
1555 type: object
1556 required: [name]
1557 properties:
1558 name: { type: string }
1559 mode:
1560 type: string
1561 enum: [active, passive, disabled]
1562 ReadSchema:
1563 type: object
1564 properties: {}
1565 DeleteSchema:
1566 type: object
1567 properties: {}
1568"#;
1569 let api = Spec::from_str(api_str).expect("parse");
1570 let defaults = ProviderDefaults::default();
1571 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1572
1573 let mode = iac
1574 .attributes
1575 .iter()
1576 .find(|a| a.canonical_name == "mode")
1577 .expect("mode field");
1578 assert!(
1579 matches!(&mode.iac_type, crate::ir::IacType::Enum { values, .. } if values.len() == 3),
1580 "mode should be an Enum with 3 values, got {:?}",
1581 mode.iac_type
1582 );
1583 assert_eq!(
1584 mode.enum_values,
1585 Some(vec![
1586 "active".to_string(),
1587 "passive".to_string(),
1588 "disabled".to_string()
1589 ])
1590 );
1591 }
1592
1593 #[test]
1594 fn resolve_resource_field_with_default_value() {
1595 let toml_str = r#"
1596[resource]
1597name = "default_val"
1598description = "Resource with default"
1599category = "test"
1600
1601[crud]
1602create_endpoint = "/create"
1603create_schema = "CreateSchema"
1604read_endpoint = "/read"
1605read_schema = "ReadSchema"
1606delete_endpoint = "/delete"
1607delete_schema = "DeleteSchema"
1608
1609[identity]
1610id_field = "name"
1611"#;
1612 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1613
1614 let api_str = r#"
1615openapi: "3.0.0"
1616info: { title: Test, version: "1.0" }
1617paths: {}
1618components:
1619 schemas:
1620 CreateSchema:
1621 type: object
1622 required: [name]
1623 properties:
1624 name: { type: string }
1625 count: { type: integer, default: 5 }
1626 ReadSchema:
1627 type: object
1628 properties: {}
1629 DeleteSchema:
1630 type: object
1631 properties: {}
1632"#;
1633 let api = Spec::from_str(api_str).expect("parse");
1634 let defaults = ProviderDefaults::default();
1635 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1636
1637 let count = iac
1638 .attributes
1639 .iter()
1640 .find(|a| a.canonical_name == "count")
1641 .expect("count field");
1642 assert_eq!(count.default_value, Some(serde_json::json!(5)));
1643 assert!(!count.required, "field with default should not be required");
1644 }
1645
1646 #[test]
1647 fn resolve_resource_hyphenated_field_name_becomes_snake_case() {
1648 let toml_str = r#"
1649[resource]
1650name = "hyphen_res"
1651description = "Hyphenated names"
1652category = "test"
1653
1654[crud]
1655create_endpoint = "/create"
1656create_schema = "CreateSchema"
1657read_endpoint = "/read"
1658read_schema = "ReadSchema"
1659delete_endpoint = "/delete"
1660delete_schema = "DeleteSchema"
1661
1662[identity]
1663id_field = "name"
1664"#;
1665 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1666
1667 let api_str = r#"
1668openapi: "3.0.0"
1669info: { title: Test, version: "1.0" }
1670paths: {}
1671components:
1672 schemas:
1673 CreateSchema:
1674 type: object
1675 properties:
1676 my-field-name: { type: string }
1677 ReadSchema:
1678 type: object
1679 properties: {}
1680 DeleteSchema:
1681 type: object
1682 properties: {}
1683"#;
1684 let api = Spec::from_str(api_str).expect("parse");
1685 let defaults = ProviderDefaults::default();
1686 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1687
1688 let field = iac
1689 .attributes
1690 .iter()
1691 .find(|a| a.api_name == "my-field-name")
1692 .expect("hyphenated field");
1693 assert_eq!(field.canonical_name, "my_field_name");
1694 assert_eq!(field.api_name, "my-field-name");
1695 }
1696
1697 #[test]
1698 fn resolve_resource_optional_flag_set_correctly() {
1699 let (resource, api) = make_test_spec();
1700 let defaults = ProviderDefaults::default();
1701 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1702
1703 let name = iac
1704 .attributes
1705 .iter()
1706 .find(|a| a.canonical_name == "name")
1707 .expect("name");
1708 assert!(name.required);
1709 assert!(!name.optional, "required field should have optional=false");
1710
1711 let tags = iac
1712 .attributes
1713 .iter()
1714 .find(|a| a.canonical_name == "tags")
1715 .expect("tags");
1716 assert!(!tags.required);
1717 assert!(
1718 tags.optional,
1719 "non-required field should have optional=true"
1720 );
1721 }
1722
1723 #[test]
1724 fn resolve_resource_json_encoded_is_false_for_openapi_fields() {
1725 let (resource, api) = make_test_spec();
1726 let defaults = ProviderDefaults::default();
1727 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1728
1729 for attr in &iac.attributes {
1730 assert!(
1731 !attr.json_encoded,
1732 "OpenAPI-resolved fields should have json_encoded=false, but {} was true",
1733 attr.canonical_name
1734 );
1735 }
1736 }
1737
1738 #[test]
1739 fn resolve_data_source_with_computed_override() {
1740 let toml_str = r#"
1741[data_source]
1742name = "comp_override"
1743description = "DS with computed override"
1744
1745[read]
1746endpoint = "/read"
1747schema = "ReadSchema"
1748
1749[fields]
1750extra = { computed = true }
1751"#;
1752 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
1753
1754 let api_str = r#"
1755openapi: "3.0.0"
1756info: { title: Test, version: "1.0" }
1757paths: {}
1758components:
1759 schemas:
1760 ReadSchema:
1761 type: object
1762 required: [name, extra]
1763 properties:
1764 name: { type: string }
1765 extra: { type: string }
1766"#;
1767 let api = Spec::from_str(api_str).expect("parse");
1768 let defaults = ProviderDefaults::default();
1769 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
1770
1771 let extra = iac
1772 .attributes
1773 .iter()
1774 .find(|a| a.canonical_name == "extra")
1775 .expect("extra");
1776 assert!(
1777 extra.computed,
1778 "explicitly computed field should be computed"
1779 );
1780 }
1781
1782 #[test]
1783 fn resolve_data_source_read_mapping() {
1784 let toml_str = r#"
1785[data_source]
1786name = "mapped_ds"
1787description = "DS with read mapping"
1788
1789[read]
1790endpoint = "/read"
1791schema = "ReadSchema"
1792
1793[read_mapping]
1794"resp_name" = "name"
1795"#;
1796 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
1797
1798 let api_str = r#"
1799openapi: "3.0.0"
1800info: { title: Test, version: "1.0" }
1801paths: {}
1802components:
1803 schemas:
1804 ReadSchema:
1805 type: object
1806 required: [name]
1807 properties:
1808 name: { type: string }
1809"#;
1810 let api = Spec::from_str(api_str).expect("parse");
1811 let defaults = ProviderDefaults::default();
1812 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
1813
1814 let name = iac
1815 .attributes
1816 .iter()
1817 .find(|a| a.canonical_name == "name")
1818 .expect("name");
1819 assert_eq!(name.read_path, Some("resp_name".to_string()));
1820 }
1821
1822 #[test]
1823 fn resolve_resource_array_of_integers() {
1824 let toml_str = r#"
1825[resource]
1826name = "int_array"
1827description = "Array of int"
1828category = "test"
1829
1830[crud]
1831create_endpoint = "/create"
1832create_schema = "CreateSchema"
1833read_endpoint = "/read"
1834read_schema = "ReadSchema"
1835delete_endpoint = "/delete"
1836delete_schema = "DeleteSchema"
1837
1838[identity]
1839id_field = "name"
1840"#;
1841 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1842
1843 let api_str = r#"
1844openapi: "3.0.0"
1845info: { title: Test, version: "1.0" }
1846paths: {}
1847components:
1848 schemas:
1849 CreateSchema:
1850 type: object
1851 properties:
1852 name: { type: string }
1853 ports:
1854 type: array
1855 items:
1856 type: integer
1857 ReadSchema:
1858 type: object
1859 properties: {}
1860 DeleteSchema:
1861 type: object
1862 properties: {}
1863"#;
1864 let api = Spec::from_str(api_str).expect("parse");
1865 let defaults = ProviderDefaults::default();
1866 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1867
1868 let ports = iac
1869 .attributes
1870 .iter()
1871 .find(|a| a.canonical_name == "ports")
1872 .expect("ports");
1873 assert_eq!(
1874 ports.iac_type,
1875 crate::ir::IacType::List(Box::new(crate::ir::IacType::Integer))
1876 );
1877 }
1878
1879 #[test]
1880 fn resolve_resource_description_from_openapi_when_no_override() {
1881 let toml_str = r#"
1882[resource]
1883name = "desc_api"
1884description = "Test"
1885category = "test"
1886
1887[crud]
1888create_endpoint = "/create"
1889create_schema = "Schema"
1890read_endpoint = "/read"
1891read_schema = "ReadSchema"
1892delete_endpoint = "/delete"
1893delete_schema = "DeleteSchema"
1894
1895[identity]
1896id_field = "name"
1897"#;
1898 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1899
1900 let api_str = r#"
1901openapi: "3.0.0"
1902info: { title: Test, version: "1.0" }
1903paths: {}
1904components:
1905 schemas:
1906 Schema:
1907 type: object
1908 properties:
1909 name: { type: string, description: "API description for name" }
1910 ReadSchema:
1911 type: object
1912 properties: {}
1913 DeleteSchema:
1914 type: object
1915 properties: {}
1916"#;
1917 let api = Spec::from_str(api_str).expect("parse");
1918 let defaults = ProviderDefaults::default();
1919 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1920
1921 let name = iac
1922 .attributes
1923 .iter()
1924 .find(|a| a.canonical_name == "name")
1925 .expect("name");
1926 assert_eq!(name.description, "API description for name");
1927 }
1928
1929 #[test]
1930 fn resolve_resource_no_description_defaults_to_empty() {
1931 let toml_str = r#"
1932[resource]
1933name = "no_desc"
1934description = "Test"
1935category = "test"
1936
1937[crud]
1938create_endpoint = "/create"
1939create_schema = "Schema"
1940read_endpoint = "/read"
1941read_schema = "ReadSchema"
1942delete_endpoint = "/delete"
1943delete_schema = "DeleteSchema"
1944
1945[identity]
1946id_field = "name"
1947"#;
1948 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
1949
1950 let api_str = r#"
1951openapi: "3.0.0"
1952info: { title: Test, version: "1.0" }
1953paths: {}
1954components:
1955 schemas:
1956 Schema:
1957 type: object
1958 properties:
1959 name: { type: string }
1960 ReadSchema:
1961 type: object
1962 properties: {}
1963 DeleteSchema:
1964 type: object
1965 properties: {}
1966"#;
1967 let api = Spec::from_str(api_str).expect("parse");
1968 let defaults = ProviderDefaults::default();
1969 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
1970
1971 let name = iac
1972 .attributes
1973 .iter()
1974 .find(|a| a.canonical_name == "name")
1975 .expect("name");
1976 assert!(name.description.is_empty());
1977 }
1978
1979 #[test]
1980 fn resolve_resource_update_schema_missing_gracefully_uses_empty_set() {
1981 let toml_str = r#"
1982[resource]
1983name = "missing_update_schema"
1984description = "Update schema is listed but not in API"
1985category = "test"
1986
1987[crud]
1988create_endpoint = "/create"
1989create_schema = "CreateSchema"
1990update_endpoint = "/update"
1991update_schema = "NonExistentUpdateSchema"
1992read_endpoint = "/read"
1993read_schema = "ReadSchema"
1994delete_endpoint = "/delete"
1995delete_schema = "DeleteSchema"
1996
1997[identity]
1998id_field = "name"
1999"#;
2000 let resource: ResourceSpec = toml::from_str(toml_str).expect("parse");
2001
2002 let api_str = r#"
2003openapi: "3.0.0"
2004info: { title: Test, version: "1.0" }
2005paths: {}
2006components:
2007 schemas:
2008 CreateSchema:
2009 type: object
2010 properties:
2011 name: { type: string }
2012 ReadSchema:
2013 type: object
2014 properties: {}
2015 DeleteSchema:
2016 type: object
2017 properties: {}
2018"#;
2019 let api = Spec::from_str(api_str).expect("parse");
2020 let defaults = ProviderDefaults::default();
2021 let iac = resolve_resource(&resource, &api, &defaults).expect("resolve");
2022
2023 for attr in &iac.attributes {
2024 assert!(
2025 !attr.update_only,
2026 "with missing update schema, nothing should be update_only"
2027 );
2028 }
2029 }
2030
2031 #[test]
2032 fn resolve_data_source_response_schema_passthrough() {
2033 let toml_str = r#"
2034[data_source]
2035name = "resp_schema_ds"
2036description = "DS with response schema"
2037
2038[read]
2039endpoint = "/read"
2040schema = "ReadSchema"
2041response_schema = "ReadResponse"
2042"#;
2043 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
2044
2045 let api_str = r#"
2046openapi: "3.0.0"
2047info: { title: Test, version: "1.0" }
2048paths: {}
2049components:
2050 schemas:
2051 ReadSchema:
2052 type: object
2053 properties:
2054 name: { type: string }
2055"#;
2056 let api = Spec::from_str(api_str).expect("parse");
2057 let defaults = ProviderDefaults::default();
2058 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
2059
2060 assert_eq!(iac.read_response_schema, Some("ReadResponse".to_string()));
2061 }
2062
2063 #[test]
2064 fn resolve_data_source_skip_field_via_override() {
2065 let toml_str = r#"
2066[data_source]
2067name = "skip_ds"
2068description = "DS field skip"
2069
2070[read]
2071endpoint = "/read"
2072schema = "ReadSchema"
2073
2074[fields]
2075internal = { skip = true }
2076"#;
2077 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
2078
2079 let api_str = r#"
2080openapi: "3.0.0"
2081info: { title: Test, version: "1.0" }
2082paths: {}
2083components:
2084 schemas:
2085 ReadSchema:
2086 type: object
2087 required: [name]
2088 properties:
2089 name: { type: string }
2090 internal: { type: string }
2091"#;
2092 let api = Spec::from_str(api_str).expect("parse");
2093 let defaults = ProviderDefaults::default();
2094 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
2095
2096 assert_eq!(iac.attributes.len(), 1);
2097 assert_eq!(iac.attributes[0].canonical_name, "name");
2098 }
2099
2100 #[test]
2101 fn resolve_data_source_sensitive_override() {
2102 let toml_str = r#"
2103[data_source]
2104name = "sens_ds"
2105description = "DS with sensitive"
2106
2107[read]
2108endpoint = "/read"
2109schema = "ReadSchema"
2110
2111[fields]
2112secret = { sensitive = true }
2113"#;
2114 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
2115
2116 let api_str = r#"
2117openapi: "3.0.0"
2118info: { title: Test, version: "1.0" }
2119paths: {}
2120components:
2121 schemas:
2122 ReadSchema:
2123 type: object
2124 properties:
2125 secret: { type: string }
2126"#;
2127 let api = Spec::from_str(api_str).expect("parse");
2128 let defaults = ProviderDefaults::default();
2129 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
2130
2131 let secret = iac
2132 .attributes
2133 .iter()
2134 .find(|a| a.canonical_name == "secret")
2135 .expect("secret");
2136 assert!(secret.sensitive);
2137 }
2138
2139 #[test]
2140 fn resolve_data_source_type_override() {
2141 let toml_str = r#"
2142[data_source]
2143name = "type_ds"
2144description = "DS with type override"
2145
2146[read]
2147endpoint = "/read"
2148schema = "ReadSchema"
2149
2150[fields]
2151flag = { type_override = "bool" }
2152"#;
2153 let ds: DataSourceSpec = toml::from_str(toml_str).expect("parse");
2154
2155 let api_str = r#"
2156openapi: "3.0.0"
2157info: { title: Test, version: "1.0" }
2158paths: {}
2159components:
2160 schemas:
2161 ReadSchema:
2162 type: object
2163 properties:
2164 flag: { type: string }
2165"#;
2166 let api = Spec::from_str(api_str).expect("parse");
2167 let defaults = ProviderDefaults::default();
2168 let iac = resolve_data_source(&ds, &api, &defaults).expect("resolve");
2169
2170 let flag = iac
2171 .attributes
2172 .iter()
2173 .find(|a| a.canonical_name == "flag")
2174 .expect("flag");
2175 assert_eq!(flag.iac_type, crate::ir::IacType::Boolean);
2176 }
2177
2178 #[test]
2179 fn resolve_provider_skip_fields_passthrough() {
2180 let toml_str = r#"
2181[provider]
2182name = "skip_test"
2183description = "Provider with skip fields"
2184version = "1.0.0"
2185
2186[auth]
2187token_field = "token"
2188env_var = "TOK"
2189
2190[defaults]
2191skip_fields = ["token", "uid-token", "json"]
2192"#;
2193 let provider: ProviderSpec = toml::from_str(toml_str).expect("parse");
2194 let iac = resolve_provider(&provider);
2195 assert_eq!(iac.skip_fields, vec!["token", "uid-token", "json"]);
2196 }
2197
2198 fn make_action_test_pair() -> (ResourceSpec, Spec) {
2201 let toml_str = r#"
2202[resource]
2203name = "akeyless_uid_generate_token"
2204description = "Generate a Universal Identity token"
2205category = "uid"
2206kind = "action"
2207
2208[action]
2209endpoint = "/uid-generate-token"
2210schema = "uidGenerateToken"
2211response_schema = "uidGenerateTokenOutput"
2212mutating = true
2213sensitive_response_fields = ["token"]
2214
2215[fields]
2216"auth-method-name" = { required = true }
2217"#;
2218 let spec: ResourceSpec = toml::from_str(toml_str).expect("parse spec");
2219
2220 let api_str = r#"
2221openapi: "3.0.0"
2222info:
2223 title: Test
2224 version: "1.0"
2225paths:
2226 /uid-generate-token:
2227 post:
2228 operationId: uidGenerateToken
2229 requestBody:
2230 content:
2231 application/json:
2232 schema:
2233 $ref: '#/components/schemas/uidGenerateToken'
2234 responses:
2235 "200":
2236 description: ok
2237components:
2238 schemas:
2239 uidGenerateToken:
2240 type: object
2241 required:
2242 - auth-method-name
2243 properties:
2244 auth-method-name:
2245 type: string
2246 description: The universal identity auth method name
2247 uid-token:
2248 type: string
2249 description: The universal identity token
2250 token:
2251 type: string
2252 uidGenerateTokenOutput:
2253 type: object
2254 properties:
2255 token:
2256 type: string
2257"#;
2258 let api = Spec::from_str(api_str).expect("parse api");
2259 (spec, api)
2260 }
2261
2262 #[test]
2263 fn resolve_action_basic() {
2264 let (spec, api) = make_action_test_pair();
2265 let defaults = ProviderDefaults {
2266 skip_fields: vec!["token".into(), "json".into()],
2267 };
2268 let action = resolve_action(&spec, &api, &defaults).expect("resolve_action");
2269 assert_eq!(action.name, "akeyless_uid_generate_token");
2270 assert_eq!(action.endpoint, "/uid-generate-token");
2271 assert_eq!(action.schema, "uidGenerateToken");
2272 assert_eq!(
2273 action.response_schema.as_deref(),
2274 Some("uidGenerateTokenOutput")
2275 );
2276 assert!(action.mutating);
2277 assert_eq!(action.sensitive_response_fields, vec!["token".to_string()]);
2278 let names: Vec<&str> = action
2280 .attributes
2281 .iter()
2282 .map(|a| a.canonical_name.as_str())
2283 .collect();
2284 assert!(names.contains(&"auth_method_name"));
2285 assert!(names.contains(&"uid_token"));
2286 assert!(!names.contains(&"token"));
2287 let amn = action
2288 .attributes
2289 .iter()
2290 .find(|a| a.canonical_name == "auth_method_name")
2291 .expect("auth_method_name attr");
2292 assert!(amn.required);
2293 }
2294
2295 #[test]
2296 fn resolve_action_defaults_mutating_to_true() {
2297 let toml_str = r#"
2298[resource]
2299name = "akeyless_uid_list_children"
2300description = "List UID children"
2301category = "uid"
2302kind = "action"
2303
2304[action]
2305endpoint = "/uid-list-children"
2306schema = "uidListChildren"
2307"#;
2308 let spec: ResourceSpec = toml::from_str(toml_str).expect("parse spec");
2309 let api_str = r#"
2310openapi: "3.0.0"
2311info: { title: t, version: "1.0" }
2312paths:
2313 /uid-list-children:
2314 post:
2315 operationId: uidListChildren
2316 responses:
2317 "200": { description: ok }
2318components:
2319 schemas:
2320 uidListChildren:
2321 type: object
2322 properties:
2323 auth-method-name:
2324 type: string
2325"#;
2326 let api = Spec::from_str(api_str).expect("parse api");
2327 let defaults = ProviderDefaults::default();
2328 let action = resolve_action(&spec, &api, &defaults).expect("resolve");
2329 assert!(action.mutating);
2331 assert!(action.sensitive_response_fields.is_empty());
2332 assert!(action.response_schema.is_none());
2333 }
2334
2335 #[test]
2336 fn resolve_action_rejects_crud_spec() {
2337 let toml_str = r#"
2338[resource]
2339name = "akeyless_static_secret"
2340description = "Static secret"
2341category = "secret"
2342
2343[crud]
2344create_endpoint = "/create-secret"
2345create_schema = "CreateSecret"
2346read_endpoint = "/get-secret-value"
2347read_schema = "GetSecretValue"
2348delete_endpoint = "/delete-item"
2349delete_schema = "DeleteItem"
2350
2351[identity]
2352id_field = "name"
2353"#;
2354 let spec: ResourceSpec = toml::from_str(toml_str).expect("parse");
2355 let api_str = r#"
2356openapi: "3.0.0"
2357info: { title: t, version: "1.0" }
2358paths:
2359 /noop:
2360 post:
2361 operationId: noop
2362 responses:
2363 "200": { description: ok }
2364components:
2365 schemas:
2366 Stub:
2367 type: object
2368"#;
2369 let api = Spec::from_str(api_str).expect("parse api");
2370 let err = resolve_action(&spec, &api, &ProviderDefaults::default()).unwrap_err();
2371 assert!(format!("{err}").contains("no kind = \"action\""));
2372 }
2373
2374 #[test]
2375 fn resolve_resource_rejects_action_spec() {
2376 let toml_str = r#"
2377[resource]
2378name = "akeyless_uid_generate_token"
2379description = "Generate UID token"
2380category = "uid"
2381kind = "action"
2382
2383[action]
2384endpoint = "/uid-generate-token"
2385schema = "uidGenerateToken"
2386"#;
2387 let spec: ResourceSpec = toml::from_str(toml_str).expect("parse");
2388 let api_str = r#"
2389openapi: "3.0.0"
2390info: { title: t, version: "1.0" }
2391paths:
2392 /noop:
2393 post:
2394 operationId: noop
2395 responses:
2396 "200": { description: ok }
2397components:
2398 schemas:
2399 Stub:
2400 type: object
2401"#;
2402 let api = Spec::from_str(api_str).expect("parse api");
2403 let err = resolve_resource(&spec, &api, &ProviderDefaults::default()).unwrap_err();
2404 assert!(format!("{err}").contains("kind = \"action\""));
2405 }
2406}