use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum IacType {
String,
Integer,
Float,
Numeric,
Boolean,
List(Box<IacType>),
Set(Box<IacType>),
Map(Box<IacType>),
Object {
name: String,
fields: Vec<IacAttribute>,
},
Enum {
values: Vec<String>,
underlying: Box<IacType>,
},
Any,
}
impl IacType {
#[must_use]
pub fn is_scalar(&self) -> bool {
matches!(
self,
Self::String | Self::Integer | Self::Float | Self::Numeric | Self::Boolean
)
}
#[must_use]
pub fn is_composite(&self) -> bool {
matches!(
self,
Self::List(_) | Self::Set(_) | Self::Map(_) | Self::Object { .. }
)
}
#[must_use]
pub fn inner_type(&self) -> Option<&IacType> {
match self {
Self::List(inner) | Self::Set(inner) | Self::Map(inner) => Some(inner),
Self::Enum { underlying, .. } => Some(underlying),
_ => None,
}
}
}
impl std::fmt::Display for IacType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String => write!(f, "string"),
Self::Integer => write!(f, "integer"),
Self::Float => write!(f, "float"),
Self::Numeric => write!(f, "numeric"),
Self::Boolean => write!(f, "boolean"),
Self::List(inner) => write!(f, "list<{inner}>"),
Self::Set(inner) => write!(f, "set<{inner}>"),
Self::Map(inner) => write!(f, "map<string, {inner}>"),
Self::Object { name, .. } => write!(f, "object<{name}>"),
Self::Enum { underlying, .. } => write!(f, "enum<{underlying}>"),
Self::Any => write!(f, "any"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct IacAttribute {
pub api_name: String,
pub canonical_name: String,
pub description: String,
pub iac_type: IacType,
pub required: bool,
#[serde(default)]
pub optional: bool,
pub computed: bool,
pub sensitive: bool,
#[serde(default)]
pub json_encoded: bool,
pub immutable: bool,
pub default_value: Option<serde_json::Value>,
pub enum_values: Option<Vec<String>>,
pub read_path: Option<String>,
pub update_only: bool,
}
impl Default for IacAttribute {
fn default() -> Self {
Self {
api_name: String::new(),
canonical_name: String::new(),
description: String::new(),
iac_type: IacType::String,
required: false,
optional: false,
computed: false,
sensitive: false,
json_encoded: false,
immutable: false,
default_value: None,
enum_values: None,
read_path: None,
update_only: false,
}
}
}
impl IacAttribute {
#[must_use]
pub fn is_input(&self) -> bool {
self.required || self.optional || !self.computed
}
#[must_use]
pub fn is_output(&self) -> bool {
self.computed || self.required
}
}
impl std::fmt::Display for IacAttribute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {} ({})",
self.canonical_name,
self.iac_type,
if self.required {
"required"
} else {
"optional"
}
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrudInfo {
pub create_endpoint: String,
pub create_schema: String,
pub update_endpoint: Option<String>,
pub update_schema: Option<String>,
pub read_endpoint: String,
pub read_schema: String,
pub read_response_schema: Option<String>,
pub delete_endpoint: String,
pub delete_schema: String,
}
impl From<&crate::spec::CrudMapping> for CrudInfo {
fn from(crud: &crate::spec::CrudMapping) -> Self {
Self {
create_endpoint: crud.create_endpoint.clone(),
create_schema: crud.create_schema.clone(),
update_endpoint: crud.update_endpoint.clone(),
update_schema: crud.update_schema.clone(),
read_endpoint: crud.read_endpoint.clone(),
read_schema: crud.read_schema.clone(),
read_response_schema: crud.read_response_schema.clone(),
delete_endpoint: crud.delete_endpoint.clone(),
delete_schema: crud.delete_schema.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityInfo {
pub id_field: String,
pub import_field: String,
pub force_replace_fields: Vec<String>,
}
impl From<&crate::spec::IdentityConfig> for IdentityInfo {
fn from(config: &crate::spec::IdentityConfig) -> Self {
Self {
id_field: config.id_field.clone(),
import_field: config
.import_field
.clone()
.unwrap_or_else(|| config.id_field.clone()),
force_replace_fields: config.force_new_fields.clone(),
}
}
}
pub trait HasAttributes {
fn attributes(&self) -> &[IacAttribute];
#[must_use]
fn input_attributes(&self) -> Vec<&IacAttribute> {
self.attributes().iter().filter(|a| a.is_input()).collect()
}
#[must_use]
fn output_attributes(&self) -> Vec<&IacAttribute> {
self.attributes().iter().filter(|a| a.is_output()).collect()
}
#[must_use]
fn required_attribute_names(&self) -> Vec<&str> {
self.attributes()
.iter()
.filter(|a| a.required)
.map(|a| a.canonical_name.as_str())
.collect()
}
#[must_use]
fn sensitive_attribute_names(&self) -> Vec<&str> {
self.attributes()
.iter()
.filter(|a| a.sensitive)
.map(|a| a.canonical_name.as_str())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IacResource {
pub name: String,
pub description: String,
pub category: String,
pub crud: CrudInfo,
pub attributes: Vec<IacAttribute>,
pub identity: IdentityInfo,
#[serde(default)]
pub read_mapping: std::collections::BTreeMap<String, String>,
}
impl std::fmt::Display for IacResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"resource<{}> ({} attrs)",
self.name,
self.attributes.len()
)
}
}
impl HasAttributes for IacResource {
fn attributes(&self) -> &[IacAttribute] {
&self.attributes
}
}
impl IacResource {
#[must_use]
pub fn immutable_attribute_names(&self) -> Vec<&str> {
self.attributes
.iter()
.filter(|a| a.immutable)
.map(|a| a.canonical_name.as_str())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IacAction {
pub name: String,
pub description: String,
pub category: String,
pub endpoint: String,
pub schema: String,
pub response_schema: Option<String>,
pub mutating: bool,
pub sensitive_response_fields: Vec<String>,
pub attributes: Vec<IacAttribute>,
#[serde(default)]
pub sdk_method: Option<String>,
}
impl std::fmt::Display for IacAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"action<{}> {} ({} attrs)",
self.name,
self.endpoint,
self.attributes.len()
)
}
}
impl HasAttributes for IacAction {
fn attributes(&self) -> &[IacAttribute] {
&self.attributes
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IacDataSource {
pub name: String,
pub description: String,
pub read_endpoint: String,
pub read_schema: String,
pub read_response_schema: Option<String>,
pub attributes: Vec<IacAttribute>,
#[serde(default)]
pub read_mapping: std::collections::BTreeMap<String, String>,
}
impl std::fmt::Display for IacDataSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"data_source<{}> ({} attrs)",
self.name,
self.attributes.len()
)
}
}
impl HasAttributes for IacDataSource {
fn attributes(&self) -> &[IacAttribute] {
&self.attributes
}
}
impl IacDataSource {
#[must_use]
pub fn computed_attribute_names(&self) -> Vec<&str> {
self.attributes
.iter()
.filter(|a| a.computed)
.map(|a| a.canonical_name.as_str())
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IacProvider {
pub name: String,
pub description: String,
pub version: String,
pub auth: AuthInfo,
pub skip_fields: Vec<String>,
pub platform_config: BTreeMap<String, toml::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuthInfo {
pub token_field: String,
pub env_var: String,
pub gateway_url_field: String,
pub gateway_env_var: String,
}
impl From<&crate::spec::AuthConfig> for AuthInfo {
fn from(auth: &crate::spec::AuthConfig) -> Self {
Self {
token_field: auth.token_field.clone(),
env_var: auth.env_var.clone(),
gateway_url_field: auth.gateway_url_field.clone(),
gateway_env_var: auth.gateway_env_var.clone(),
}
}
}
impl AuthInfo {
#[must_use]
pub fn has_token(&self) -> bool {
!self.token_field.is_empty()
}
#[must_use]
pub fn has_gateway(&self) -> bool {
!self.gateway_url_field.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TestAttributeBuilder;
#[test]
fn attribute_is_input_required() {
let attr = TestAttributeBuilder::new("name", IacType::String)
.required()
.build();
assert!(attr.is_input());
}
#[test]
fn attribute_is_input_optional() {
let attr = TestAttributeBuilder::new("tag", IacType::String)
.optional()
.build();
assert!(attr.is_input());
}
#[test]
fn attribute_is_input_optional_computed() {
let attr = TestAttributeBuilder::new("default_val", IacType::String)
.optional()
.computed()
.build();
assert!(attr.is_input());
}
#[test]
fn attribute_is_not_input_purely_computed() {
let attr = TestAttributeBuilder::new("id", IacType::String)
.computed()
.build();
assert!(!attr.is_input());
}
#[test]
fn attribute_is_output_computed() {
let attr = TestAttributeBuilder::new("id", IacType::String)
.computed()
.build();
assert!(attr.is_output());
}
#[test]
fn attribute_is_output_required() {
let attr = TestAttributeBuilder::new("name", IacType::String)
.required()
.build();
assert!(attr.is_output());
}
#[test]
fn attribute_is_not_output_plain_optional() {
let attr = TestAttributeBuilder::new("tag", IacType::String).build();
assert!(!attr.is_output());
}
#[test]
fn iac_type_display() {
assert_eq!(IacType::String.to_string(), "string");
assert_eq!(IacType::Integer.to_string(), "integer");
assert_eq!(IacType::Float.to_string(), "float");
assert_eq!(IacType::Numeric.to_string(), "numeric");
assert_eq!(IacType::Boolean.to_string(), "boolean");
assert_eq!(
IacType::List(Box::new(IacType::String)).to_string(),
"list<string>"
);
assert_eq!(
IacType::Set(Box::new(IacType::Integer)).to_string(),
"set<integer>"
);
assert_eq!(
IacType::Map(Box::new(IacType::Boolean)).to_string(),
"map<string, boolean>"
);
assert_eq!(
IacType::Object {
name: "User".to_string(),
fields: vec![]
}
.to_string(),
"object<User>"
);
assert_eq!(
IacType::Enum {
values: vec!["a".into()],
underlying: Box::new(IacType::String)
}
.to_string(),
"enum<string>"
);
assert_eq!(IacType::Any.to_string(), "any");
}
#[test]
fn iac_type_equality() {
assert_eq!(IacType::String, IacType::String);
assert_ne!(IacType::String, IacType::Integer);
assert_eq!(
IacType::List(Box::new(IacType::String)),
IacType::List(Box::new(IacType::String))
);
assert_ne!(
IacType::List(Box::new(IacType::String)),
IacType::List(Box::new(IacType::Integer))
);
}
#[test]
fn iac_type_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(IacType::String);
set.insert(IacType::Integer);
set.insert(IacType::String);
assert_eq!(set.len(), 2);
}
#[test]
fn iac_attribute_display() {
let attr = IacAttribute {
api_name: "name".to_string(),
canonical_name: "name".to_string(),
description: String::new(),
iac_type: IacType::String,
required: true,
optional: false,
computed: false,
sensitive: false,
json_encoded: false,
immutable: false,
default_value: None,
enum_values: None,
read_path: None,
update_only: false,
};
assert_eq!(attr.to_string(), "name: string (required)");
let optional = IacAttribute {
required: false,
optional: true,
..attr
};
assert_eq!(optional.to_string(), "name: string (optional)");
}
#[test]
fn auth_info_helpers() {
let empty = AuthInfo::default();
assert!(!empty.has_token());
assert!(!empty.has_gateway());
let configured = AuthInfo {
token_field: "token".to_string(),
env_var: "TOKEN".to_string(),
gateway_url_field: "gateway".to_string(),
gateway_env_var: "GW".to_string(),
};
assert!(configured.has_token());
assert!(configured.has_gateway());
}
#[test]
fn iac_resource_serialize_roundtrip() {
let resource = IacResource {
name: "test_resource".to_string(),
description: "A test".to_string(),
category: "test".to_string(),
crud: CrudInfo {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: None,
update_schema: None,
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
},
attributes: vec![IacAttribute {
api_name: "name".to_string(),
canonical_name: "name".to_string(),
description: "The name".to_string(),
iac_type: IacType::String,
required: true,
optional: false,
computed: false,
sensitive: false,
json_encoded: false,
immutable: true,
default_value: None,
enum_values: None,
read_path: None,
update_only: false,
}],
identity: IdentityInfo {
id_field: "name".to_string(),
import_field: "name".to_string(),
force_replace_fields: vec!["name".to_string()],
},
read_mapping: BTreeMap::new(),
};
let json = serde_json::to_string(&resource).expect("serialize");
let deserialized: IacResource = serde_json::from_str(&json).expect("deserialize");
assert_eq!(deserialized.name, resource.name);
assert_eq!(deserialized.attributes.len(), 1);
assert_eq!(deserialized.attributes[0].iac_type, IacType::String);
}
fn resource_with_mixed_attrs() -> IacResource {
IacResource {
name: "mixed".to_string(),
description: String::new(),
category: "test".to_string(),
crud: CrudInfo {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: None,
update_schema: None,
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
},
attributes: vec![
TestAttributeBuilder::new("name", IacType::String)
.required()
.immutable()
.build(),
TestAttributeBuilder::new("secret", IacType::String)
.required()
.sensitive()
.build(),
TestAttributeBuilder::new("computed_id", IacType::String)
.computed()
.build(),
TestAttributeBuilder::new("tags", IacType::List(Box::new(IacType::String))).build(),
],
identity: IdentityInfo {
id_field: "name".to_string(),
import_field: "name".to_string(),
force_replace_fields: vec!["name".to_string()],
},
read_mapping: BTreeMap::new(),
}
}
#[test]
fn input_attributes_excludes_purely_computed() {
let r = resource_with_mixed_attrs();
let inputs = r.input_attributes();
assert_eq!(inputs.len(), 3);
assert!(inputs.iter().all(|a| a.canonical_name != "computed_id"));
}
#[test]
fn output_attributes_includes_computed_and_required() {
let r = resource_with_mixed_attrs();
let outputs = r.output_attributes();
assert_eq!(outputs.len(), 3);
assert!(outputs.iter().all(|a| a.canonical_name != "tags"));
}
#[test]
fn required_attribute_names_returns_correct_set() {
let r = resource_with_mixed_attrs();
let names = r.required_attribute_names();
assert_eq!(names, vec!["name", "secret"]);
}
#[test]
fn sensitive_attribute_names_returns_correct_set() {
let r = resource_with_mixed_attrs();
let names = r.sensitive_attribute_names();
assert_eq!(names, vec!["secret"]);
}
#[test]
fn immutable_attribute_names_returns_correct_set() {
let r = resource_with_mixed_attrs();
let names = r.immutable_attribute_names();
assert_eq!(names, vec!["name"]);
}
#[test]
fn data_source_input_and_output_attributes() {
let ds = crate::testing::test_data_source("cfg");
let inputs = ds.input_attributes();
assert_eq!(inputs.len(), 1);
assert_eq!(inputs[0].canonical_name, "name");
let outputs = ds.output_attributes();
assert_eq!(outputs.len(), 2);
let computed = ds.computed_attribute_names();
assert_eq!(computed, vec!["value"]);
}
#[test]
fn data_source_required_and_sensitive() {
let ds = IacDataSource {
name: "test".to_string(),
description: String::new(),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
attributes: vec![
TestAttributeBuilder::new("id", IacType::String)
.required()
.build(),
TestAttributeBuilder::new("password", IacType::String)
.sensitive()
.computed()
.build(),
],
read_mapping: BTreeMap::new(),
};
assert_eq!(ds.required_attribute_names(), vec!["id"]);
assert_eq!(ds.sensitive_attribute_names(), vec!["password"]);
}
#[test]
fn resource_input_attributes_mix() {
let r = IacResource {
name: "mix".to_string(),
description: String::new(),
category: "test".to_string(),
crud: CrudInfo {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: None,
update_schema: None,
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
},
attributes: vec![
TestAttributeBuilder::new("req_only", IacType::String)
.required()
.build(),
TestAttributeBuilder::new("comp_only", IacType::String)
.computed()
.build(),
TestAttributeBuilder::new("comp_and_req", IacType::String)
.computed()
.required()
.build(),
TestAttributeBuilder::new("optional", IacType::String).build(),
],
identity: IdentityInfo {
id_field: "req_only".to_string(),
import_field: "req_only".to_string(),
force_replace_fields: vec![],
},
read_mapping: BTreeMap::new(),
};
let inputs = r.input_attributes();
let input_names: Vec<&str> = inputs.iter().map(|a| a.canonical_name.as_str()).collect();
assert_eq!(input_names, vec!["req_only", "comp_and_req", "optional"]);
let outputs = r.output_attributes();
let output_names: Vec<&str> = outputs.iter().map(|a| a.canonical_name.as_str()).collect();
assert_eq!(output_names, vec!["req_only", "comp_only", "comp_and_req"]);
}
#[test]
fn data_source_input_and_output_full() {
let ds = IacDataSource {
name: "full_ds".to_string(),
description: String::new(),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
attributes: vec![
TestAttributeBuilder::new("input_key", IacType::String)
.required()
.build(),
TestAttributeBuilder::new("output_val", IacType::String)
.computed()
.build(),
TestAttributeBuilder::new("both", IacType::String)
.required()
.computed()
.build(),
TestAttributeBuilder::new("neither", IacType::String).build(),
],
read_mapping: BTreeMap::new(),
};
let inputs = ds.input_attributes();
let input_names: Vec<&str> = inputs.iter().map(|a| a.canonical_name.as_str()).collect();
assert_eq!(input_names, vec!["input_key", "both", "neither"]);
let outputs = ds.output_attributes();
let output_names: Vec<&str> = outputs.iter().map(|a| a.canonical_name.as_str()).collect();
assert_eq!(output_names, vec!["input_key", "output_val", "both"]);
let computed = ds.computed_attribute_names();
assert_eq!(computed, vec!["output_val", "both"]);
}
#[test]
fn auth_info_has_token_only() {
let auth = AuthInfo {
token_field: "token".to_string(),
env_var: "TOKEN".to_string(),
gateway_url_field: String::new(),
gateway_env_var: String::new(),
};
assert!(auth.has_token());
assert!(!auth.has_gateway());
}
#[test]
fn auth_info_has_gateway_only() {
let auth = AuthInfo {
token_field: String::new(),
env_var: String::new(),
gateway_url_field: "gateway".to_string(),
gateway_env_var: "GW".to_string(),
};
assert!(!auth.has_token());
assert!(auth.has_gateway());
}
#[test]
fn resource_no_attributes() {
let r = IacResource {
name: "empty".to_string(),
description: String::new(),
category: "test".to_string(),
crud: CrudInfo {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: None,
update_schema: None,
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
},
attributes: vec![],
identity: IdentityInfo {
id_field: "id".to_string(),
import_field: "id".to_string(),
force_replace_fields: vec![],
},
read_mapping: BTreeMap::new(),
};
assert!(r.input_attributes().is_empty());
assert!(r.output_attributes().is_empty());
assert!(r.required_attribute_names().is_empty());
assert!(r.sensitive_attribute_names().is_empty());
assert!(r.immutable_attribute_names().is_empty());
}
#[test]
fn data_source_no_attributes() {
let ds = IacDataSource {
name: "empty".to_string(),
description: String::new(),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
attributes: vec![],
read_mapping: BTreeMap::new(),
};
assert!(ds.input_attributes().is_empty());
assert!(ds.output_attributes().is_empty());
assert!(ds.required_attribute_names().is_empty());
assert!(ds.sensitive_attribute_names().is_empty());
assert!(ds.computed_attribute_names().is_empty());
}
#[test]
fn iac_type_display_nested() {
assert_eq!(
IacType::List(Box::new(IacType::List(Box::new(IacType::String)))).to_string(),
"list<list<string>>"
);
assert_eq!(
IacType::Map(Box::new(IacType::List(Box::new(IacType::Integer)))).to_string(),
"map<string, list<integer>>"
);
assert_eq!(
IacType::Set(Box::new(IacType::Set(Box::new(IacType::Boolean)))).to_string(),
"set<set<boolean>>"
);
assert_eq!(
IacType::Set(Box::new(IacType::Map(Box::new(IacType::Float)))).to_string(),
"set<map<string, float>>"
);
}
#[test]
fn iac_attribute_display_optional() {
let attr = IacAttribute {
api_name: "tags".to_string(),
canonical_name: "tags".to_string(),
description: String::new(),
iac_type: IacType::List(Box::new(IacType::String)),
required: false,
optional: true,
computed: false,
sensitive: false,
json_encoded: false,
immutable: false,
default_value: None,
enum_values: None,
read_path: None,
update_only: false,
};
assert_eq!(attr.to_string(), "tags: list<string> (optional)");
}
#[test]
fn iac_type_set_serde_roundtrip() {
let set_type = IacType::Set(Box::new(IacType::String));
let json = serde_json::to_string(&set_type).expect("serialize");
let rt: IacType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(set_type, rt);
}
#[test]
fn iac_type_set_display() {
assert_eq!(
IacType::Set(Box::new(IacType::String)).to_string(),
"set<string>"
);
assert_eq!(
IacType::Set(Box::new(IacType::Integer)).to_string(),
"set<integer>"
);
}
#[test]
fn iac_type_set_equality() {
let a = IacType::Set(Box::new(IacType::String));
let b = IacType::Set(Box::new(IacType::String));
let c = IacType::Set(Box::new(IacType::Integer));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn iac_type_set_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(IacType::Set(Box::new(IacType::String)));
set.insert(IacType::Set(Box::new(IacType::String)));
set.insert(IacType::Set(Box::new(IacType::Integer)));
assert_eq!(set.len(), 2);
}
#[test]
fn iac_type_serde_roundtrip_all_variants() {
let types = vec![
IacType::String,
IacType::Integer,
IacType::Float,
IacType::Numeric,
IacType::Boolean,
IacType::Any,
IacType::List(Box::new(IacType::Integer)),
IacType::Set(Box::new(IacType::Boolean)),
IacType::Map(Box::new(IacType::Float)),
IacType::Object {
name: "Nested".to_string(),
fields: vec![],
},
IacType::Enum {
values: vec!["x".into(), "y".into()],
underlying: Box::new(IacType::String),
},
];
for t in &types {
let json = serde_json::to_string(t).expect("serialize");
let roundtripped: IacType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(*t, roundtripped, "roundtrip failed for {t}");
}
}
#[test]
fn iac_type_serde_roundtrip_deeply_nested() {
let deep = IacType::List(Box::new(IacType::Map(Box::new(IacType::Set(Box::new(
IacType::Object {
name: "Inner".to_string(),
fields: vec![
crate::testing::TestAttributeBuilder::new("f", IacType::String)
.required()
.build(),
],
},
))))));
let json = serde_json::to_string(&deep).expect("serialize");
let roundtripped: IacType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(deep, roundtripped);
}
#[test]
fn iac_attribute_serde_with_all_fields_set() {
let attr = IacAttribute {
api_name: "my-key".to_string(),
canonical_name: "my_key".to_string(),
description: "A key".to_string(),
iac_type: IacType::String,
required: true,
optional: false,
computed: true,
sensitive: true,
json_encoded: true,
immutable: true,
default_value: Some(serde_json::json!("default_val")),
enum_values: Some(vec!["a".into(), "b".into()]),
read_path: Some("response_key".to_string()),
update_only: true,
};
let json = serde_json::to_string(&attr).expect("serialize");
let roundtripped: IacAttribute = serde_json::from_str(&json).expect("deserialize");
assert_eq!(attr, roundtripped);
}
#[test]
fn iac_attribute_json_encoded_default_false() {
let json_str = r#"{
"api_name":"f","canonical_name":"f","description":"","iac_type":"String",
"required":false,"optional":false,"computed":false,"sensitive":false,
"immutable":false,"default_value":null,"enum_values":null,
"read_path":null,"update_only":false
}"#;
let attr: IacAttribute = serde_json::from_str(json_str).expect("deserialize");
assert!(!attr.json_encoded, "json_encoded should default to false");
}
#[test]
fn iac_attribute_optional_default_false() {
let json_str = r#"{
"api_name":"f","canonical_name":"f","description":"","iac_type":"String",
"required":false,"computed":false,"sensitive":false,
"immutable":false,"default_value":null,"enum_values":null,
"read_path":null,"update_only":false
}"#;
let attr: IacAttribute = serde_json::from_str(json_str).expect("deserialize");
assert!(!attr.optional, "optional should default to false");
}
#[test]
fn resource_optional_computed_is_input() {
let r = IacResource {
name: "opt_comp".to_string(),
description: String::new(),
category: "test".to_string(),
crud: CrudInfo {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: None,
update_schema: None,
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
},
attributes: vec![
TestAttributeBuilder::new("opt_comp_field", IacType::String)
.optional()
.computed()
.build(),
],
identity: IdentityInfo {
id_field: "id".to_string(),
import_field: "id".to_string(),
force_replace_fields: vec![],
},
read_mapping: BTreeMap::new(),
};
let inputs = r.input_attributes();
assert_eq!(inputs.len(), 1, "optional+computed should be an input");
assert_eq!(inputs[0].canonical_name, "opt_comp_field");
}
#[test]
fn data_source_optional_computed_is_input() {
let ds = IacDataSource {
name: "opt_comp_ds".to_string(),
description: String::new(),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: None,
attributes: vec![
TestAttributeBuilder::new("opt_comp_field", IacType::String)
.optional()
.computed()
.build(),
],
read_mapping: BTreeMap::new(),
};
let inputs = ds.input_attributes();
assert_eq!(
inputs.len(),
1,
"optional+computed should be an input for data source"
);
}
#[test]
fn iac_type_clone_equality() {
let original = IacType::Object {
name: "Config".to_string(),
fields: vec![crate::testing::TestAttributeBuilder::new("k", IacType::String).build()],
};
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn crud_info_serde_roundtrip_with_optionals() {
let crud = CrudInfo {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: Some("/update".to_string()),
update_schema: Some("Update".to_string()),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: Some("ReadResp".to_string()),
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
};
let json = serde_json::to_string(&crud).expect("serialize");
let rt: CrudInfo = serde_json::from_str(&json).expect("deserialize");
assert_eq!(rt.update_endpoint, Some("/update".to_string()));
assert_eq!(rt.update_schema, Some("Update".to_string()));
assert_eq!(rt.read_response_schema, Some("ReadResp".to_string()));
}
#[test]
fn identity_info_serde_roundtrip() {
let id = IdentityInfo {
id_field: "name".to_string(),
import_field: "path".to_string(),
force_replace_fields: vec!["name".to_string(), "region".to_string()],
};
let json = serde_json::to_string(&id).expect("serialize");
let rt: IdentityInfo = serde_json::from_str(&json).expect("deserialize");
assert_eq!(rt.id_field, "name");
assert_eq!(rt.import_field, "path");
assert_eq!(rt.force_replace_fields.len(), 2);
}
#[test]
fn iac_provider_empty_auth_serializes() {
let provider = IacProvider {
name: "minimal".to_string(),
description: String::new(),
version: "0.1.0".to_string(),
auth: AuthInfo::default(),
skip_fields: vec![],
platform_config: BTreeMap::new(),
};
let json = serde_json::to_string(&provider).expect("serialize");
let rt: IacProvider = serde_json::from_str(&json).expect("deserialize");
assert!(!rt.auth.has_token());
assert!(!rt.auth.has_gateway());
assert!(rt.skip_fields.is_empty());
assert!(rt.platform_config.is_empty());
}
#[test]
fn iac_provider_serialize_roundtrip() {
let provider = IacProvider {
name: "test".to_string(),
description: "Test provider".to_string(),
version: "1.0.0".to_string(),
auth: AuthInfo {
token_field: "token".to_string(),
env_var: "TOKEN".to_string(),
gateway_url_field: "gw".to_string(),
gateway_env_var: "GW".to_string(),
},
skip_fields: vec!["token".to_string()],
platform_config: {
let mut m = BTreeMap::new();
m.insert(
"terraform".to_string(),
toml::Value::String("sdk".to_string()),
);
m
},
};
let json = serde_json::to_string(&provider).expect("serialize");
let deserialized: IacProvider = serde_json::from_str(&json).expect("deserialize");
assert_eq!(deserialized.name, "test");
assert_eq!(deserialized.auth.token_field, "token");
assert_eq!(deserialized.skip_fields, vec!["token"]);
}
#[test]
fn iac_data_source_serialize_roundtrip() {
let ds = IacDataSource {
name: "ds".to_string(),
description: "A data source".to_string(),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: Some("ReadOutput".to_string()),
attributes: vec![
TestAttributeBuilder::new("key", IacType::String)
.required()
.build(),
],
read_mapping: BTreeMap::new(),
};
let json = serde_json::to_string(&ds).expect("serialize");
let deserialized: IacDataSource = serde_json::from_str(&json).expect("deserialize");
assert_eq!(deserialized.name, "ds");
assert_eq!(
deserialized.read_response_schema,
Some("ReadOutput".to_string())
);
assert_eq!(deserialized.attributes.len(), 1);
}
#[test]
fn crud_info_from_crud_mapping() {
let mapping = crate::spec::CrudMapping {
create_endpoint: "/create".to_string(),
create_schema: "Create".to_string(),
update_endpoint: Some("/update".to_string()),
update_schema: Some("Update".to_string()),
read_endpoint: "/read".to_string(),
read_schema: "Read".to_string(),
read_response_schema: Some("ReadResp".to_string()),
delete_endpoint: "/delete".to_string(),
delete_schema: "Delete".to_string(),
};
let info = CrudInfo::from(&mapping);
assert_eq!(info.create_endpoint, "/create");
assert_eq!(info.update_endpoint, Some("/update".to_string()));
assert_eq!(info.read_response_schema, Some("ReadResp".to_string()));
}
#[test]
fn auth_info_from_auth_config() {
let config = crate::spec::AuthConfig {
token_field: "token".to_string(),
env_var: "TOKEN".to_string(),
gateway_url_field: "gw".to_string(),
gateway_env_var: "GW".to_string(),
};
let info = AuthInfo::from(&config);
assert_eq!(info.token_field, "token");
assert_eq!(info.env_var, "TOKEN");
assert!(info.has_token());
assert!(info.has_gateway());
}
#[test]
fn auth_info_from_default_auth_config() {
let config = crate::spec::AuthConfig::default();
let info = AuthInfo::from(&config);
assert!(!info.has_token());
assert!(!info.has_gateway());
}
#[test]
fn iac_resource_display() {
let r = crate::testing::test_resource("secret");
assert_eq!(r.to_string(), "resource<secret> (3 attrs)");
}
#[test]
fn iac_data_source_display() {
let ds = crate::testing::test_data_source("config");
assert_eq!(ds.to_string(), "data_source<config> (2 attrs)");
}
#[test]
fn identity_info_from_identity_config() {
let config = crate::spec::IdentityConfig {
id_field: "name".to_string(),
import_field: Some("path".to_string()),
force_new_fields: vec!["name".to_string()],
};
let info = IdentityInfo::from(&config);
assert_eq!(info.id_field, "name");
assert_eq!(info.import_field, "path");
assert_eq!(info.force_replace_fields, vec!["name"]);
}
#[test]
fn identity_info_from_config_defaults_import_field() {
let config = crate::spec::IdentityConfig {
id_field: "my_id".to_string(),
import_field: None,
force_new_fields: vec![],
};
let info = IdentityInfo::from(&config);
assert_eq!(info.import_field, "my_id");
}
#[test]
fn iac_attribute_default() {
let attr = IacAttribute::default();
assert!(attr.api_name.is_empty());
assert!(attr.canonical_name.is_empty());
assert!(attr.description.is_empty());
assert_eq!(attr.iac_type, IacType::String);
assert!(!attr.required);
assert!(!attr.optional);
assert!(!attr.computed);
assert!(!attr.sensitive);
assert!(!attr.json_encoded);
assert!(!attr.immutable);
assert!(attr.default_value.is_none());
assert!(attr.enum_values.is_none());
assert!(attr.read_path.is_none());
assert!(!attr.update_only);
}
#[test]
fn iac_type_is_scalar() {
assert!(IacType::String.is_scalar());
assert!(IacType::Integer.is_scalar());
assert!(IacType::Float.is_scalar());
assert!(IacType::Numeric.is_scalar());
assert!(IacType::Boolean.is_scalar());
assert!(!IacType::List(Box::new(IacType::String)).is_scalar());
assert!(!IacType::Set(Box::new(IacType::String)).is_scalar());
assert!(!IacType::Map(Box::new(IacType::String)).is_scalar());
assert!(
!IacType::Object {
name: "X".into(),
fields: vec![]
}
.is_scalar()
);
assert!(!IacType::Any.is_scalar());
}
#[test]
fn iac_type_is_composite() {
assert!(IacType::List(Box::new(IacType::String)).is_composite());
assert!(IacType::Set(Box::new(IacType::Integer)).is_composite());
assert!(IacType::Map(Box::new(IacType::Boolean)).is_composite());
assert!(
IacType::Object {
name: "X".into(),
fields: vec![]
}
.is_composite()
);
assert!(!IacType::String.is_composite());
assert!(!IacType::Integer.is_composite());
assert!(!IacType::Any.is_composite());
}
#[test]
fn iac_type_enum_is_neither_scalar_nor_composite() {
let enum_type = IacType::Enum {
values: vec!["a".into()],
underlying: Box::new(IacType::String),
};
assert!(!enum_type.is_scalar());
assert!(!enum_type.is_composite());
}
#[test]
fn iac_type_inner_type_list() {
let t = IacType::List(Box::new(IacType::String));
assert_eq!(t.inner_type(), Some(&IacType::String));
}
#[test]
fn iac_type_inner_type_set() {
let t = IacType::Set(Box::new(IacType::Integer));
assert_eq!(t.inner_type(), Some(&IacType::Integer));
}
#[test]
fn iac_type_inner_type_map() {
let t = IacType::Map(Box::new(IacType::Boolean));
assert_eq!(t.inner_type(), Some(&IacType::Boolean));
}
#[test]
fn iac_type_inner_type_enum() {
let t = IacType::Enum {
values: vec!["a".into()],
underlying: Box::new(IacType::String),
};
assert_eq!(t.inner_type(), Some(&IacType::String));
}
#[test]
fn iac_type_inner_type_scalar_none() {
assert!(IacType::String.inner_type().is_none());
assert!(IacType::Integer.inner_type().is_none());
assert!(IacType::Boolean.inner_type().is_none());
assert!(IacType::Any.inner_type().is_none());
}
#[test]
fn iac_type_any_is_neither_scalar_nor_composite() {
assert!(!IacType::Any.is_scalar());
assert!(!IacType::Any.is_composite());
}
}