1use crate::ir::{
11 AuthInfo, CrudInfo, IacAction, IacAttribute, IacDataSource, IacProvider, IacResource, IacType,
12 IdentityInfo,
13};
14use crate::sexpr::{FromSExpr, SExpr, SExprError, ToSExpr, parse_struct, struct_expr, take_field};
15
16impl ToSExpr for IacType {
19 fn to_sexpr(&self) -> SExpr {
20 match self {
21 Self::String => SExpr::Symbol("string".into()),
22 Self::Integer => SExpr::Symbol("integer".into()),
23 Self::Float => SExpr::Symbol("float".into()),
24 Self::Numeric => SExpr::Symbol("numeric".into()),
25 Self::Boolean => SExpr::Symbol("boolean".into()),
26 Self::Any => SExpr::Symbol("any".into()),
27 Self::List(inner) => SExpr::List(vec![SExpr::Symbol("list".into()), inner.to_sexpr()]),
28 Self::Set(inner) => SExpr::List(vec![SExpr::Symbol("set".into()), inner.to_sexpr()]),
29 Self::Map(inner) => SExpr::List(vec![SExpr::Symbol("map".into()), inner.to_sexpr()]),
30 Self::Object { name, fields } => struct_expr(
31 "object",
32 vec![("name", name.to_sexpr()), ("fields", fields.to_sexpr())],
33 ),
34 Self::Enum { values, underlying } => struct_expr(
35 "enum",
36 vec![
37 ("values", values.to_sexpr()),
38 ("underlying", underlying.to_sexpr()),
39 ],
40 ),
41 other => panic!(
42 "unsupported IacType variant in ToSExpr: {other:?} — add an explicit mapping"
43 ),
44 }
45 }
46}
47
48impl FromSExpr for IacType {
49 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
50 if let SExpr::Symbol(tag) = s {
52 return match tag.as_str() {
53 "string" => Ok(Self::String),
54 "integer" => Ok(Self::Integer),
55 "float" => Ok(Self::Float),
56 "numeric" => Ok(Self::Numeric),
57 "boolean" => Ok(Self::Boolean),
58 "any" => Ok(Self::Any),
59 other => Err(SExprError::UnknownVariant(format!("IacType::{other}"))),
60 };
61 }
62
63 let items = s.as_list()?;
65 let (head, rest) = items
66 .split_first()
67 .ok_or_else(|| SExprError::Shape("empty IacType list".into()))?;
68 let tag = head.as_symbol()?;
69 match tag {
70 "list" | "set" | "map" => {
71 if rest.len() != 1 {
72 return Err(SExprError::Shape(format!(
73 "IacType::{tag} expects 1 arg, got {}",
74 rest.len()
75 )));
76 }
77 let inner = Box::new(IacType::from_sexpr(&rest[0])?);
78 Ok(match tag {
79 "list" => Self::List(inner),
80 "set" => Self::Set(inner),
81 "map" => Self::Map(inner),
82 _ => unreachable!(),
83 })
84 }
85 "object" => {
86 let fields = parse_struct(s, "object")?;
87 Ok(Self::Object {
88 name: String::from_sexpr(take_field(&fields, "name")?)?,
89 fields: Vec::<IacAttribute>::from_sexpr(take_field(&fields, "fields")?)?,
90 })
91 }
92 "enum" => {
93 let fields = parse_struct(s, "enum")?;
94 Ok(Self::Enum {
95 values: Vec::<String>::from_sexpr(take_field(&fields, "values")?)?,
96 underlying: Box::new(IacType::from_sexpr(take_field(&fields, "underlying")?)?),
97 })
98 }
99 other => Err(SExprError::UnknownVariant(format!("IacType::{other}"))),
100 }
101 }
102}
103
104fn json_to_sexpr(v: &serde_json::Value) -> SExpr {
111 let encoded = serde_json::to_string(v).expect("serde_json::Value is serializable");
112 SExpr::List(vec![
113 SExpr::Symbol("json-value".into()),
114 SExpr::String(encoded),
115 ])
116}
117
118fn json_from_sexpr(s: &SExpr) -> Result<serde_json::Value, SExprError> {
119 let items = s.as_list()?;
120 let (head, rest) = items
121 .split_first()
122 .ok_or_else(|| SExprError::Shape("expected (json-value ...) form".into()))?;
123 let tag = head.as_symbol()?;
124 if tag != "json-value" {
125 return Err(SExprError::Shape(format!(
126 "expected 'json-value' tag, got '{tag}'"
127 )));
128 }
129 if rest.len() != 1 {
130 return Err(SExprError::Shape(format!(
131 "json-value expects 1 arg, got {}",
132 rest.len()
133 )));
134 }
135 let encoded = rest[0].as_str()?;
136 serde_json::from_str(encoded).map_err(|e| SExprError::Parse(format!("json: {e}")))
137}
138
139impl ToSExpr for IacAttribute {
142 fn to_sexpr(&self) -> SExpr {
143 struct_expr(
144 "attribute",
145 vec![
146 ("api-name", self.api_name.to_sexpr()),
147 ("canonical-name", self.canonical_name.to_sexpr()),
148 ("description", self.description.to_sexpr()),
149 ("iac-type", self.iac_type.to_sexpr()),
150 ("required", self.required.to_sexpr()),
151 ("optional", self.optional.to_sexpr()),
152 ("computed", self.computed.to_sexpr()),
153 ("sensitive", self.sensitive.to_sexpr()),
154 ("json-encoded", self.json_encoded.to_sexpr()),
155 ("immutable", self.immutable.to_sexpr()),
156 (
157 "default-value",
158 self.default_value
159 .as_ref()
160 .map_or(SExpr::Nil, json_to_sexpr),
161 ),
162 ("enum-values", self.enum_values.to_sexpr()),
163 ("read-path", self.read_path.to_sexpr()),
164 ("update-only", self.update_only.to_sexpr()),
165 ],
166 )
167 }
168}
169
170impl FromSExpr for IacAttribute {
171 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
172 let fields = parse_struct(s, "attribute")?;
173 Ok(Self {
174 api_name: String::from_sexpr(take_field(&fields, "api-name")?)?,
175 canonical_name: String::from_sexpr(take_field(&fields, "canonical-name")?)?,
176 description: String::from_sexpr(take_field(&fields, "description")?)?,
177 iac_type: IacType::from_sexpr(take_field(&fields, "iac-type")?)?,
178 required: bool::from_sexpr(take_field(&fields, "required")?)?,
179 optional: bool::from_sexpr(take_field(&fields, "optional")?)?,
180 computed: bool::from_sexpr(take_field(&fields, "computed")?)?,
181 sensitive: bool::from_sexpr(take_field(&fields, "sensitive")?)?,
182 json_encoded: bool::from_sexpr(take_field(&fields, "json-encoded")?)?,
183 immutable: bool::from_sexpr(take_field(&fields, "immutable")?)?,
184 default_value: match take_field(&fields, "default-value")? {
185 SExpr::Nil => None,
186 other => Some(json_from_sexpr(other)?),
187 },
188 enum_values: Option::<Vec<String>>::from_sexpr(take_field(&fields, "enum-values")?)?,
189 read_path: Option::<String>::from_sexpr(take_field(&fields, "read-path")?)?,
190 update_only: bool::from_sexpr(take_field(&fields, "update-only")?)?,
191 })
192 }
193}
194
195impl ToSExpr for CrudInfo {
198 fn to_sexpr(&self) -> SExpr {
199 struct_expr(
200 "crud",
201 vec![
202 ("create-endpoint", self.create_endpoint.to_sexpr()),
203 ("create-schema", self.create_schema.to_sexpr()),
204 ("update-endpoint", self.update_endpoint.to_sexpr()),
205 ("update-schema", self.update_schema.to_sexpr()),
206 ("read-endpoint", self.read_endpoint.to_sexpr()),
207 ("read-schema", self.read_schema.to_sexpr()),
208 ("read-response-schema", self.read_response_schema.to_sexpr()),
209 ("delete-endpoint", self.delete_endpoint.to_sexpr()),
210 ("delete-schema", self.delete_schema.to_sexpr()),
211 ],
212 )
213 }
214}
215
216impl FromSExpr for CrudInfo {
217 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
218 let f = parse_struct(s, "crud")?;
219 Ok(Self {
220 create_endpoint: String::from_sexpr(take_field(&f, "create-endpoint")?)?,
221 create_schema: String::from_sexpr(take_field(&f, "create-schema")?)?,
222 update_endpoint: Option::<String>::from_sexpr(take_field(&f, "update-endpoint")?)?,
223 update_schema: Option::<String>::from_sexpr(take_field(&f, "update-schema")?)?,
224 read_endpoint: String::from_sexpr(take_field(&f, "read-endpoint")?)?,
225 read_schema: String::from_sexpr(take_field(&f, "read-schema")?)?,
226 read_response_schema: Option::<String>::from_sexpr(take_field(
227 &f,
228 "read-response-schema",
229 )?)?,
230 delete_endpoint: String::from_sexpr(take_field(&f, "delete-endpoint")?)?,
231 delete_schema: String::from_sexpr(take_field(&f, "delete-schema")?)?,
232 })
233 }
234}
235
236impl ToSExpr for IdentityInfo {
239 fn to_sexpr(&self) -> SExpr {
240 struct_expr(
241 "identity",
242 vec![
243 ("id-field", self.id_field.to_sexpr()),
244 ("import-field", self.import_field.to_sexpr()),
245 ("force-replace-fields", self.force_replace_fields.to_sexpr()),
246 ],
247 )
248 }
249}
250
251impl FromSExpr for IdentityInfo {
252 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
253 let f = parse_struct(s, "identity")?;
254 Ok(Self {
255 id_field: String::from_sexpr(take_field(&f, "id-field")?)?,
256 import_field: String::from_sexpr(take_field(&f, "import-field")?)?,
257 force_replace_fields: Vec::<String>::from_sexpr(take_field(
258 &f,
259 "force-replace-fields",
260 )?)?,
261 })
262 }
263}
264
265impl ToSExpr for AuthInfo {
268 fn to_sexpr(&self) -> SExpr {
269 struct_expr(
270 "auth",
271 vec![
272 ("token-field", self.token_field.to_sexpr()),
273 ("env-var", self.env_var.to_sexpr()),
274 ("gateway-url-field", self.gateway_url_field.to_sexpr()),
275 ("gateway-env-var", self.gateway_env_var.to_sexpr()),
276 ],
277 )
278 }
279}
280
281impl FromSExpr for AuthInfo {
282 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
283 let f = parse_struct(s, "auth")?;
284 Ok(Self {
285 token_field: String::from_sexpr(take_field(&f, "token-field")?)?,
286 env_var: String::from_sexpr(take_field(&f, "env-var")?)?,
287 gateway_url_field: String::from_sexpr(take_field(&f, "gateway-url-field")?)?,
288 gateway_env_var: String::from_sexpr(take_field(&f, "gateway-env-var")?)?,
289 })
290 }
291}
292
293fn string_map_to_sexpr(map: &std::collections::BTreeMap<String, String>) -> SExpr {
299 let mut items = Vec::with_capacity(map.len() + 1);
300 items.push(SExpr::Symbol("read-mapping".into()));
301 for (k, v) in map {
302 items.push(SExpr::List(vec![
303 SExpr::Symbol("pair".into()),
304 SExpr::String(k.clone()),
305 SExpr::String(v.clone()),
306 ]));
307 }
308 SExpr::List(items)
309}
310
311fn string_map_from_sexpr(
312 s: &SExpr,
313) -> Result<std::collections::BTreeMap<String, String>, SExprError> {
314 let items = s.as_list()?;
315 let (head, rest) = items
316 .split_first()
317 .ok_or_else(|| SExprError::Shape("expected (read-mapping ...) form".into()))?;
318 let tag = head.as_symbol()?;
319 if tag != "read-mapping" {
320 return Err(SExprError::Shape(format!(
321 "expected 'read-mapping' tag, got '{tag}'"
322 )));
323 }
324 let mut out = std::collections::BTreeMap::new();
325 for pair in rest {
326 let pair_items = pair.as_list()?;
327 let (phead, prest) = pair_items
328 .split_first()
329 .ok_or_else(|| SExprError::Shape("expected (pair k v) form".into()))?;
330 let ptag = phead.as_symbol()?;
331 if ptag != "pair" {
332 return Err(SExprError::Shape(format!(
333 "expected 'pair' tag, got '{ptag}'"
334 )));
335 }
336 if prest.len() != 2 {
337 return Err(SExprError::Shape(format!(
338 "pair expects 2 args, got {}",
339 prest.len()
340 )));
341 }
342 out.insert(
343 String::from_sexpr(&prest[0])?,
344 String::from_sexpr(&prest[1])?,
345 );
346 }
347 Ok(out)
348}
349
350impl ToSExpr for IacResource {
353 fn to_sexpr(&self) -> SExpr {
354 struct_expr(
355 "resource",
356 vec![
357 ("name", self.name.to_sexpr()),
358 ("description", self.description.to_sexpr()),
359 ("category", self.category.to_sexpr()),
360 ("crud", self.crud.to_sexpr()),
361 ("attributes", self.attributes.to_sexpr()),
362 ("identity", self.identity.to_sexpr()),
363 ("read-mapping", string_map_to_sexpr(&self.read_mapping)),
364 ],
365 )
366 }
367}
368
369impl FromSExpr for IacResource {
370 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
371 let f = parse_struct(s, "resource")?;
372 Ok(Self {
373 name: String::from_sexpr(take_field(&f, "name")?)?,
374 description: String::from_sexpr(take_field(&f, "description")?)?,
375 category: String::from_sexpr(take_field(&f, "category")?)?,
376 crud: CrudInfo::from_sexpr(take_field(&f, "crud")?)?,
377 attributes: Vec::<IacAttribute>::from_sexpr(take_field(&f, "attributes")?)?,
378 identity: IdentityInfo::from_sexpr(take_field(&f, "identity")?)?,
379 read_mapping: string_map_from_sexpr(take_field(&f, "read-mapping")?)?,
380 })
381 }
382}
383
384impl ToSExpr for IacDataSource {
387 fn to_sexpr(&self) -> SExpr {
388 struct_expr(
389 "data-source",
390 vec![
391 ("name", self.name.to_sexpr()),
392 ("description", self.description.to_sexpr()),
393 ("read-endpoint", self.read_endpoint.to_sexpr()),
394 ("read-schema", self.read_schema.to_sexpr()),
395 ("read-response-schema", self.read_response_schema.to_sexpr()),
396 ("attributes", self.attributes.to_sexpr()),
397 ("read-mapping", string_map_to_sexpr(&self.read_mapping)),
398 ],
399 )
400 }
401}
402
403impl FromSExpr for IacDataSource {
404 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
405 let f = parse_struct(s, "data-source")?;
406 Ok(Self {
407 name: String::from_sexpr(take_field(&f, "name")?)?,
408 description: String::from_sexpr(take_field(&f, "description")?)?,
409 read_endpoint: String::from_sexpr(take_field(&f, "read-endpoint")?)?,
410 read_schema: String::from_sexpr(take_field(&f, "read-schema")?)?,
411 read_response_schema: Option::<String>::from_sexpr(take_field(
412 &f,
413 "read-response-schema",
414 )?)?,
415 attributes: Vec::<IacAttribute>::from_sexpr(take_field(&f, "attributes")?)?,
416 read_mapping: string_map_from_sexpr(take_field(&f, "read-mapping")?)?,
417 })
418 }
419}
420
421impl ToSExpr for IacAction {
424 fn to_sexpr(&self) -> SExpr {
425 struct_expr(
426 "action",
427 vec![
428 ("name", self.name.to_sexpr()),
429 ("description", self.description.to_sexpr()),
430 ("category", self.category.to_sexpr()),
431 ("endpoint", self.endpoint.to_sexpr()),
432 ("schema", self.schema.to_sexpr()),
433 ("response-schema", self.response_schema.to_sexpr()),
434 ("mutating", self.mutating.to_sexpr()),
435 (
436 "sensitive-response-fields",
437 self.sensitive_response_fields.to_sexpr(),
438 ),
439 ("attributes", self.attributes.to_sexpr()),
440 ("sdk-method", self.sdk_method.to_sexpr()),
441 ],
442 )
443 }
444}
445
446impl FromSExpr for IacAction {
447 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
448 let f = parse_struct(s, "action")?;
449 Ok(Self {
450 name: String::from_sexpr(take_field(&f, "name")?)?,
451 description: String::from_sexpr(take_field(&f, "description")?)?,
452 category: String::from_sexpr(take_field(&f, "category")?)?,
453 endpoint: String::from_sexpr(take_field(&f, "endpoint")?)?,
454 schema: String::from_sexpr(take_field(&f, "schema")?)?,
455 response_schema: Option::<String>::from_sexpr(take_field(&f, "response-schema")?)?,
456 mutating: bool::from_sexpr(take_field(&f, "mutating")?)?,
457 sensitive_response_fields: Vec::<String>::from_sexpr(take_field(
458 &f,
459 "sensitive-response-fields",
460 )?)?,
461 attributes: Vec::<IacAttribute>::from_sexpr(take_field(&f, "attributes")?)?,
462 sdk_method: Option::<String>::from_sexpr(take_field(&f, "sdk-method")?)?,
463 })
464 }
465}
466
467fn toml_map_to_sexpr(map: &std::collections::BTreeMap<String, toml::Value>) -> SExpr {
475 let as_table: toml::Table = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
476 let encoded = toml::to_string(&as_table).expect("toml::Table is serializable");
477 SExpr::List(vec![
478 SExpr::Symbol("toml-map".into()),
479 SExpr::String(encoded),
480 ])
481}
482
483fn toml_map_from_sexpr(
484 s: &SExpr,
485) -> Result<std::collections::BTreeMap<String, toml::Value>, SExprError> {
486 let items = s.as_list()?;
487 let (head, rest) = items
488 .split_first()
489 .ok_or_else(|| SExprError::Shape("expected (toml-map ...) form".into()))?;
490 let tag = head.as_symbol()?;
491 if tag != "toml-map" {
492 return Err(SExprError::Shape(format!(
493 "expected 'toml-map' tag, got '{tag}'"
494 )));
495 }
496 if rest.len() != 1 {
497 return Err(SExprError::Shape(format!(
498 "toml-map expects 1 arg, got {}",
499 rest.len()
500 )));
501 }
502 let encoded = rest[0].as_str()?;
503 let table: toml::Table =
504 toml::from_str(encoded).map_err(|e| SExprError::Parse(format!("toml: {e}")))?;
505 Ok(table.into_iter().collect())
506}
507
508impl ToSExpr for IacProvider {
509 fn to_sexpr(&self) -> SExpr {
510 struct_expr(
511 "provider",
512 vec![
513 ("name", self.name.to_sexpr()),
514 ("description", self.description.to_sexpr()),
515 ("version", self.version.to_sexpr()),
516 ("auth", self.auth.to_sexpr()),
517 ("skip-fields", self.skip_fields.to_sexpr()),
518 ("platform-config", toml_map_to_sexpr(&self.platform_config)),
519 ],
520 )
521 }
522}
523
524impl FromSExpr for IacProvider {
525 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
526 let f = parse_struct(s, "provider")?;
527 Ok(Self {
528 name: String::from_sexpr(take_field(&f, "name")?)?,
529 description: String::from_sexpr(take_field(&f, "description")?)?,
530 version: String::from_sexpr(take_field(&f, "version")?)?,
531 auth: AuthInfo::from_sexpr(take_field(&f, "auth")?)?,
532 skip_fields: Vec::<String>::from_sexpr(take_field(&f, "skip-fields")?)?,
533 platform_config: toml_map_from_sexpr(take_field(&f, "platform-config")?)?,
534 })
535 }
536}
537
538#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::testing::{TestAttributeBuilder, test_provider, test_resource};
544
545 fn roundtrip_type(ty: &IacType) {
548 let s = ty.to_sexpr();
549 let parsed = IacType::from_sexpr(&s).expect("parse");
550 assert_eq!(&parsed, ty, "round-trip failed for {ty:?}");
551 let emitted = s.emit();
553 let reparsed_sexpr = SExpr::parse(&emitted).expect("parse emitted");
554 let reparsed = IacType::from_sexpr(&reparsed_sexpr).expect("from emitted");
555 assert_eq!(&reparsed, ty, "emit/parse round-trip failed for {ty:?}");
556 }
557
558 #[test]
559 fn round_trip_scalar_types() {
560 for ty in [
561 IacType::String,
562 IacType::Integer,
563 IacType::Float,
564 IacType::Numeric,
565 IacType::Boolean,
566 IacType::Any,
567 ] {
568 roundtrip_type(&ty);
569 }
570 }
571
572 #[test]
573 fn round_trip_list_of_string() {
574 roundtrip_type(&IacType::List(Box::new(IacType::String)));
575 }
576
577 #[test]
578 fn round_trip_set_of_integer() {
579 roundtrip_type(&IacType::Set(Box::new(IacType::Integer)));
580 }
581
582 #[test]
583 fn round_trip_map_of_bool() {
584 roundtrip_type(&IacType::Map(Box::new(IacType::Boolean)));
585 }
586
587 #[test]
588 fn round_trip_nested_list() {
589 roundtrip_type(&IacType::List(Box::new(IacType::List(Box::new(
590 IacType::List(Box::new(IacType::Integer)),
591 )))));
592 }
593
594 #[test]
595 fn round_trip_enum_with_values() {
596 roundtrip_type(&IacType::Enum {
597 values: vec!["tcp".into(), "udp".into(), "icmp".into()],
598 underlying: Box::new(IacType::String),
599 });
600 }
601
602 #[test]
603 fn round_trip_empty_enum() {
604 roundtrip_type(&IacType::Enum {
605 values: vec![],
606 underlying: Box::new(IacType::String),
607 });
608 }
609
610 #[test]
611 fn round_trip_object_with_fields() {
612 let attr = TestAttributeBuilder::new("inner", IacType::String)
613 .required()
614 .build();
615 roundtrip_type(&IacType::Object {
616 name: "Inner".into(),
617 fields: vec![attr],
618 });
619 }
620
621 #[test]
622 fn from_sexpr_rejects_unknown_variant() {
623 let err = IacType::from_sexpr(&SExpr::Symbol("nope".into())).unwrap_err();
624 assert!(matches!(err, SExprError::UnknownVariant(_)));
625 }
626
627 #[test]
628 fn from_sexpr_rejects_wrong_arity_list() {
629 let s = SExpr::List(vec![SExpr::Symbol("list".into())]);
631 let err = IacType::from_sexpr(&s).unwrap_err();
632 assert!(matches!(err, SExprError::Shape(_)));
633 }
634
635 fn sample_attr() -> IacAttribute {
638 TestAttributeBuilder::new("bound-secret", IacType::String)
639 .required()
640 .immutable()
641 .sensitive()
642 .description("a secret")
643 .build()
644 }
645
646 #[test]
647 fn round_trip_attribute() {
648 let a = sample_attr();
649 let s = a.to_sexpr();
650 let parsed = IacAttribute::from_sexpr(&s).expect("parse");
651 assert_eq!(parsed, a);
652 }
653
654 #[test]
655 fn round_trip_attribute_through_emit() {
656 let a = sample_attr();
657 let emitted = a.to_sexpr().emit();
658 let parsed = IacAttribute::from_sexpr(&SExpr::parse(&emitted).unwrap()).unwrap();
659 assert_eq!(parsed, a);
660 }
661
662 #[test]
663 fn round_trip_attribute_with_default_value() {
664 let attr = TestAttributeBuilder::new("note", IacType::String)
665 .default_value(serde_json::json!("hello"))
666 .build();
667 let parsed = IacAttribute::from_sexpr(&attr.to_sexpr()).expect("parse");
668 assert_eq!(parsed, attr);
669 }
670
671 #[test]
672 fn round_trip_attribute_with_enum_values_and_read_path() {
673 let attr = TestAttributeBuilder::new("protocol", IacType::String)
674 .enum_values(vec!["tcp".into(), "udp".into()])
675 .read_path("resp_path")
676 .build();
677 let parsed = IacAttribute::from_sexpr(&attr.to_sexpr()).expect("parse");
678 assert_eq!(parsed, attr);
679 }
680
681 #[test]
684 fn round_trip_resource() {
685 let r = test_resource("widget");
686 let parsed = IacResource::from_sexpr(&r.to_sexpr()).expect("parse");
687 assert_eq!(parsed.name, r.name);
688 assert_eq!(parsed.description, r.description);
689 assert_eq!(parsed.attributes, r.attributes);
690 assert_eq!(parsed.identity.id_field, r.identity.id_field);
691 assert_eq!(parsed.crud.create_endpoint, r.crud.create_endpoint);
692 }
693
694 #[test]
695 fn round_trip_resource_through_emit() {
696 let r = test_resource("widget");
697 let emitted = r.to_sexpr().emit();
698 let parsed = IacResource::from_sexpr(&SExpr::parse(&emitted).unwrap()).expect("parse");
699 assert_eq!(parsed.name, r.name);
700 assert_eq!(parsed.attributes, r.attributes);
701 }
702
703 fn sample_action() -> IacAction {
706 IacAction {
707 name: "akeyless_uid_generate_token".into(),
708 description: "Generate a UID token".into(),
709 category: "uid".into(),
710 endpoint: "/uid-generate-token".into(),
711 schema: "uidGenerateToken".into(),
712 response_schema: Some("uidGenerateTokenOutput".into()),
713 mutating: true,
714 sensitive_response_fields: vec!["token".into()],
715 attributes: vec![sample_attr()],
716 sdk_method: None,
717 }
718 }
719
720 #[test]
721 fn round_trip_action() {
722 let a = sample_action();
723 let parsed = IacAction::from_sexpr(&a.to_sexpr()).expect("parse");
724 assert_eq!(parsed.name, a.name);
725 assert_eq!(parsed.endpoint, a.endpoint);
726 assert_eq!(parsed.mutating, a.mutating);
727 assert_eq!(
728 parsed.sensitive_response_fields,
729 a.sensitive_response_fields
730 );
731 assert_eq!(parsed.sdk_method, a.sdk_method);
732 }
733
734 #[test]
735 fn round_trip_action_with_sdk_method_override() {
736 let mut a = sample_action();
737 a.schema = "BatchEncryptionRequestLine".into();
738 a.sdk_method = Some("encrypt_batch".into());
739 let parsed = IacAction::from_sexpr(&a.to_sexpr()).expect("parse");
740 assert_eq!(parsed.sdk_method.as_deref(), Some("encrypt_batch"));
741 }
742
743 #[test]
744 fn round_trip_action_through_emit() {
745 let a = sample_action();
746 let emitted = a.to_sexpr().emit();
747 let parsed = IacAction::from_sexpr(&SExpr::parse(&emitted).unwrap()).expect("parse");
748 assert_eq!(parsed.name, a.name);
749 assert_eq!(parsed.attributes.len(), a.attributes.len());
750 }
751
752 #[test]
755 fn round_trip_provider() {
756 let p = test_provider("acme");
757 let parsed = IacProvider::from_sexpr(&p.to_sexpr()).expect("parse");
758 assert_eq!(parsed.name, p.name);
759 assert_eq!(parsed.auth.token_field, p.auth.token_field);
760 assert_eq!(parsed.platform_config, p.platform_config);
761 }
762
763 #[test]
764 fn round_trip_provider_through_emit() {
765 let p = test_provider("acme");
766 let emitted = p.to_sexpr().emit();
767 let parsed = IacProvider::from_sexpr(&SExpr::parse(&emitted).unwrap()).expect("parse");
768 assert_eq!(parsed.name, p.name);
769 assert_eq!(parsed.auth.token_field, p.auth.token_field);
770 }
771
772 #[test]
775 fn emitted_form_is_deterministic() {
776 let r = test_resource("widget");
777 let a = r.to_sexpr().emit();
778 let b = r.to_sexpr().emit();
779 assert_eq!(a, b);
780 }
781
782 #[test]
783 fn parse_is_inverse_of_emit_on_all_types() {
784 let types = vec![
785 IacType::String,
786 IacType::Integer,
787 IacType::List(Box::new(IacType::Boolean)),
788 IacType::Enum {
789 values: vec!["a".into(), "b".into()],
790 underlying: Box::new(IacType::String),
791 },
792 IacType::Map(Box::new(IacType::List(Box::new(IacType::String)))),
793 ];
794 for ty in types {
795 let emitted = ty.to_sexpr().emit();
796 let parsed = IacType::from_sexpr(&SExpr::parse(&emitted).unwrap()).unwrap();
797 assert_eq!(parsed, ty);
798 }
799 }
800}