1use std::collections::BTreeMap;
16
17use magma_cty::CtyType;
18use magma_protocol::tfplugin6::schema::{self, Attribute, Block, NestedBlock, Object};
19
20#[derive(Debug, thiserror::Error)]
21pub enum SchemaError {
22 #[error("attribute {0:?} has neither a type nor a nested_type")]
23 AttributeNoType(String),
24 #[error("cty type decode for {0:?}: {1}")]
25 Cty(String, magma_cty::CtyError),
26 #[error("invalid nesting mode {0} for {1:?}")]
27 BadNesting(i32, String),
28 #[error("nested block {0:?} has no inner block")]
29 EmptyNestedBlock(String),
30}
31
32pub fn block_implied_type(block: &Block) -> Result<CtyType, SchemaError> {
35 let mut attrs: BTreeMap<String, CtyType> = BTreeMap::new();
36 for attr in &block.attributes {
37 attrs.insert(attr.name.clone(), attribute_type(attr)?);
38 }
39 for nb in &block.block_types {
40 attrs.insert(nb.type_name.clone(), nested_block_type(nb)?);
41 }
42 Ok(CtyType::Object(attrs))
43}
44
45fn attribute_type(attr: &Attribute) -> Result<CtyType, SchemaError> {
48 if let Some(obj) = &attr.nested_type {
49 return object_implied_type(obj, &attr.name);
50 }
51 if attr.r#type.is_empty() {
52 return Err(SchemaError::AttributeNoType(attr.name.clone()));
53 }
54 let json: serde_json::Value = serde_json::from_slice(&attr.r#type).map_err(|e| {
55 SchemaError::Cty(attr.name.clone(), magma_cty::CtyError::Type(e.to_string()))
56 })?;
57 CtyType::from_json(&json).map_err(|e| SchemaError::Cty(attr.name.clone(), e))
58}
59
60fn object_implied_type(obj: &Object, label: &str) -> Result<CtyType, SchemaError> {
62 let mut attrs = BTreeMap::new();
63 for attr in &obj.attributes {
64 attrs.insert(attr.name.clone(), attribute_type(attr)?);
65 }
66 let inner = CtyType::Object(attrs);
67 let nesting = schema::object::NestingMode::try_from(obj.nesting)
68 .map_err(|_| SchemaError::BadNesting(obj.nesting, label.to_string()))?;
69 Ok(match nesting {
70 schema::object::NestingMode::Single | schema::object::NestingMode::Invalid => inner,
71 schema::object::NestingMode::List => CtyType::list(inner),
72 schema::object::NestingMode::Set => CtyType::set(inner),
73 schema::object::NestingMode::Map => CtyType::map(inner),
74 })
75}
76
77fn nested_block_type(nb: &NestedBlock) -> Result<CtyType, SchemaError> {
80 let block = nb
81 .block
82 .as_ref()
83 .ok_or_else(|| SchemaError::EmptyNestedBlock(nb.type_name.clone()))?;
84 let inner = block_implied_type(block)?;
85 let nesting = schema::nested_block::NestingMode::try_from(nb.nesting)
86 .map_err(|_| SchemaError::BadNesting(nb.nesting, nb.type_name.clone()))?;
87 Ok(match nesting {
88 schema::nested_block::NestingMode::Single
89 | schema::nested_block::NestingMode::Group
90 | schema::nested_block::NestingMode::Invalid => inner,
91 schema::nested_block::NestingMode::List => CtyType::list(inner),
92 schema::nested_block::NestingMode::Set => CtyType::set(inner),
93 schema::nested_block::NestingMode::Map => CtyType::map(inner),
94 })
95}
96
97use magma_protocol::tfplugin5;
107
108pub fn block5_implied_type(block: &tfplugin5::schema::Block) -> Result<CtyType, SchemaError> {
111 block_implied_type(&block5_to_v6(block))
112}
113
114fn block5_to_v6(b: &tfplugin5::schema::Block) -> Block {
115 Block {
116 version: b.version,
117 attributes: b.attributes.iter().map(attr5_to_v6).collect(),
118 block_types: b.block_types.iter().map(nb5_to_v6).collect(),
119 ..Default::default()
120 }
121}
122
123fn attr5_to_v6(a: &tfplugin5::schema::Attribute) -> Attribute {
124 Attribute {
125 name: a.name.clone(),
126 r#type: a.r#type.clone(),
127 nested_type: None, ..Default::default()
129 }
130}
131
132fn nb5_to_v6(nb: &tfplugin5::schema::NestedBlock) -> NestedBlock {
133 NestedBlock {
134 type_name: nb.type_name.clone(),
135 block: nb.block.as_ref().map(block5_to_v6),
136 nesting: nb.nesting, ..Default::default()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn ty_bytes(v: serde_json::Value) -> Vec<u8> {
147 serde_json::to_vec(&v).unwrap()
148 }
149
150 fn attr(name: &str, ty: serde_json::Value) -> Attribute {
151 Attribute {
152 name: name.into(),
153 r#type: ty_bytes(ty),
154 ..Default::default()
155 }
156 }
157
158 fn block(attributes: Vec<Attribute>, block_types: Vec<NestedBlock>) -> Block {
159 Block {
160 attributes,
161 block_types,
162 ..Default::default()
163 }
164 }
165
166 #[test]
167 fn simple_scalar_attributes() {
168 let b = block(
169 vec![
170 attr("name", serde_json::json!("string")),
171 attr("private", serde_json::json!("bool")),
172 attr("retries", serde_json::json!("number")),
173 ],
174 vec![],
175 );
176 let ty = block_implied_type(&b).unwrap();
177 let expected = CtyType::object([
178 ("name".into(), CtyType::String),
179 ("private".into(), CtyType::Bool),
180 ("retries".into(), CtyType::Number),
181 ]);
182 assert_eq!(ty, expected);
183 }
184
185 #[test]
186 fn collection_attribute_types() {
187 let b = block(
188 vec![
189 attr("topics", serde_json::json!(["list", "string"])),
190 attr("labels", serde_json::json!(["map", "string"])),
191 ],
192 vec![],
193 );
194 let ty = block_implied_type(&b).unwrap();
195 let expected = CtyType::object([
196 ("topics".into(), CtyType::list(CtyType::String)),
197 ("labels".into(), CtyType::map(CtyType::String)),
198 ]);
199 assert_eq!(ty, expected);
200 }
201
202 #[test]
203 fn nested_block_list_becomes_list_of_object() {
204 let inner = block(vec![attr("branch", serde_json::json!("string"))], vec![]);
206 let nb = NestedBlock {
207 type_name: "pages".into(),
208 block: Some(inner),
209 nesting: schema::nested_block::NestingMode::List as i32,
210 ..Default::default()
211 };
212 let b = block(vec![attr("name", serde_json::json!("string"))], vec![nb]);
213 let ty = block_implied_type(&b).unwrap();
214 let expected = CtyType::object([
215 ("name".into(), CtyType::String),
216 (
217 "pages".into(),
218 CtyType::list(CtyType::object([("branch".into(), CtyType::String)])),
219 ),
220 ]);
221 assert_eq!(ty, expected);
222 }
223
224 #[test]
225 fn nested_block_single_is_bare_object() {
226 let inner = block(vec![attr("id".into(), serde_json::json!("string"))], vec![]);
227 let nb = NestedBlock {
228 type_name: "template".into(),
229 block: Some(inner),
230 nesting: schema::nested_block::NestingMode::Single as i32,
231 ..Default::default()
232 };
233 let b = block(vec![], vec![nb]);
234 let ty = block_implied_type(&b).unwrap();
235 let expected = CtyType::object([(
236 "template".into(),
237 CtyType::object([("id".into(), CtyType::String)]),
238 )]);
239 assert_eq!(ty, expected);
240 }
241
242 #[test]
243 fn nested_type_object_map() {
244 let obj = Object {
246 attributes: vec![attr("v", serde_json::json!("string"))],
247 nesting: schema::object::NestingMode::Map as i32,
248 ..Default::default()
249 };
250 let a = Attribute {
251 name: "entries".into(),
252 nested_type: Some(obj),
253 ..Default::default()
254 };
255 let b = block(vec![a], vec![]);
256 let ty = block_implied_type(&b).unwrap();
257 let expected = CtyType::object([(
258 "entries".into(),
259 CtyType::map(CtyType::object([("v".into(), CtyType::String)])),
260 )]);
261 assert_eq!(ty, expected);
262 }
263
264 #[test]
265 fn attribute_without_type_is_an_error() {
266 let a = Attribute {
267 name: "broken".into(),
268 ..Default::default()
269 };
270 let b = block(vec![a], vec![]);
271 assert!(matches!(
272 block_implied_type(&b),
273 Err(SchemaError::AttributeNoType(_))
274 ));
275 }
276}