1use ciborium::Value as CborValue;
10use serde_json::Value as JsonValue;
11use vantage_expressions::Expression;
12
13#[derive(Clone)]
14pub enum AwsCondition {
15 Eq { field: String, value: CborValue },
17 In {
20 field: String,
21 values: Vec<CborValue>,
22 },
23 Deferred {
27 field: String,
28 source: Expression<CborValue>,
29 },
30}
31
32impl std::fmt::Debug for AwsCondition {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::Eq { field, value } => f
39 .debug_struct("Eq")
40 .field("field", field)
41 .field("value", value)
42 .finish(),
43 Self::In { field, values } => f
44 .debug_struct("In")
45 .field("field", field)
46 .field("values", values)
47 .finish(),
48 Self::Deferred { field, source } => f
49 .debug_struct("Deferred")
50 .field("field", field)
51 .field("source.template", &source.template)
52 .field("source.params", &source.parameters.len())
53 .finish(),
54 }
55 }
56}
57
58impl AwsCondition {
59 pub fn eq(field: impl Into<String>, value: impl Into<CborValue>) -> Self {
60 Self::Eq {
61 field: field.into(),
62 value: value.into(),
63 }
64 }
65
66 pub fn in_<I, V>(field: impl Into<String>, values: I) -> Self
67 where
68 I: IntoIterator<Item = V>,
69 V: Into<CborValue>,
70 {
71 Self::In {
72 field: field.into(),
73 values: values.into_iter().map(Into::into).collect(),
74 }
75 }
76
77 pub fn field(&self) -> &str {
78 match self {
79 Self::Eq { field, .. } | Self::In { field, .. } | Self::Deferred { field, .. } => field,
80 }
81 }
82}
83
84pub fn eq(field: impl Into<String>, value: impl Into<CborValue>) -> AwsCondition {
91 AwsCondition::eq(field, value)
92}
93
94pub fn in_<I, V>(field: impl Into<String>, values: I) -> AwsCondition
98where
99 I: IntoIterator<Item = V>,
100 V: Into<CborValue>,
101{
102 AwsCondition::in_(field, values)
103}
104
105pub(crate) fn build_body(
111 conditions: &[AwsCondition],
112) -> vantage_core::Result<serde_json::Map<String, JsonValue>> {
113 let mut body = serde_json::Map::new();
114 for cond in conditions {
115 match cond {
116 AwsCondition::Eq { field, value } => {
117 body.insert(field.clone(), cbor_to_json(value));
118 }
119 AwsCondition::In { field, values } => match values.as_slice() {
120 [single] => {
121 body.insert(field.clone(), cbor_to_json(single));
122 }
123 [] => {
124 return Err(vantage_core::error!(
125 "AwsCondition::In with zero values is not representable",
126 field = field.as_str()
127 ));
128 }
129 _ => {
130 return Err(vantage_core::error!(
131 "AwsCondition::In with more than one value is not supported \
132 by AWS — relations must traverse from a single parent",
133 field = field.as_str(),
134 count = values.len()
135 ));
136 }
137 },
138 AwsCondition::Deferred { field, .. } => {
139 return Err(vantage_core::error!(
140 "Internal: Deferred condition reached build_body unresolved \
141 — AwsJson1::resolve_conditions should have materialised it",
142 field = field.as_str()
143 ));
144 }
145 }
146 }
147 Ok(body)
148}
149
150fn cbor_to_json(v: &CborValue) -> JsonValue {
155 v.deserialized::<JsonValue>().unwrap_or(JsonValue::Null)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use serde_json::json;
162
163 #[test]
164 fn eq_folds_into_body() {
165 let conds = [eq("logGroupNamePrefix", "/aws/lambda/")];
166 let body = build_body(&conds).unwrap();
167 assert_eq!(body["logGroupNamePrefix"], json!("/aws/lambda/"));
168 }
169
170 #[test]
171 fn single_element_in_collapses_to_eq() {
172 let conds = [in_(
173 "logGroupName",
174 vec![CborValue::from("/aws/lambda/foo")],
175 )];
176 let body = build_body(&conds).unwrap();
177 assert_eq!(body["logGroupName"], json!("/aws/lambda/foo"));
178 }
179
180 #[test]
181 fn multi_element_in_errors() {
182 let conds = [in_(
183 "logGroupName",
184 vec![CborValue::from("a"), CborValue::from("b")],
185 )];
186 let err = build_body(&conds).unwrap_err();
187 assert!(format!("{err}").contains("more than one value"));
188 }
189
190 #[test]
191 fn empty_in_errors() {
192 let conds = [AwsCondition::In {
193 field: "x".into(),
194 values: vec![],
195 }];
196 assert!(build_body(&conds).is_err());
197 }
198
199 #[test]
200 fn deferred_in_build_body_is_internal_error() {
201 let conds = [AwsCondition::Deferred {
205 field: "x".into(),
206 source: Expression::new("noop", vec![]),
207 }];
208 let err = build_body(&conds).unwrap_err();
209 assert!(format!("{err}").contains("Deferred"));
210 }
211
212 #[test]
213 fn multiple_eqs_compose() {
214 let conds = [
215 eq("logGroupName", "/aws/lambda/foo"),
216 eq("startTime", 1_700_000_000_000i64),
217 ];
218 let body = build_body(&conds).unwrap();
219 assert_eq!(body["logGroupName"], json!("/aws/lambda/foo"));
220 assert_eq!(body["startTime"], json!(1_700_000_000_000i64));
221 }
222}