cirrus_metadata/handlers/
crud.rs1use crate::MetadataClient;
45use crate::envelope::xml_escape;
46use crate::error::{MetadataError, MetadataResult};
47use crate::result::{DeleteResult, SaveResult, UpsertResult};
48use crate::transport::SoapOperation;
49use serde::Deserialize;
50use serde::de::DeserializeOwned;
51use std::marker::PhantomData;
52
53pub const MAX_CRUD_COMPONENTS_PER_CALL: usize = 10;
57
58fn render_metadata_components<S: AsRef<str>>(type_name: &str, components: &[S], out: &mut String) {
66 for component in components {
67 out.push_str(r#"<met:metadata xsi:type="met:"#);
68 out.push_str(&xml_escape(type_name));
69 out.push_str(r#"" xmlns="http://soap.sforce.com/2006/04/metadata">"#);
74 out.push_str(component.as_ref());
75 out.push_str("</met:metadata>");
76 }
77}
78
79fn render_type_and_full_names<S: AsRef<str>>(type_name: &str, full_names: &[S], out: &mut String) {
82 out.push_str("<met:type>");
83 out.push_str(&xml_escape(type_name));
84 out.push_str("</met:type>");
85 for name in full_names {
86 out.push_str("<met:fullNames>");
87 out.push_str(&xml_escape(name.as_ref()));
88 out.push_str("</met:fullNames>");
89 }
90}
91
92fn check_component_cap(count: usize, op_label: &str) -> MetadataResult<()> {
93 if count == 0 {
94 return Err(MetadataError::InvalidArgument(format!(
95 "{op_label} requires at least one component; got 0"
96 )));
97 }
98 if count > MAX_CRUD_COMPONENTS_PER_CALL {
99 return Err(MetadataError::InvalidArgument(format!(
100 "{op_label} accepts at most {MAX_CRUD_COMPONENTS_PER_CALL} components per call; \
101 got {count}"
102 )));
103 }
104 Ok(())
105}
106
107struct CreateMetadataOp<'a, S: AsRef<str>> {
112 type_name: &'a str,
113 components: &'a [S],
114}
115
116#[derive(Deserialize)]
117struct SaveResultsWire {
118 #[serde(default, rename = "result")]
119 results: Vec<SaveResult>,
120}
121
122impl<S: AsRef<str>> SoapOperation for CreateMetadataOp<'_, S> {
123 const NAME: &'static str = "createMetadata";
124 type Response = SaveResultsWire;
125
126 fn render_body(&self) -> MetadataResult<String> {
127 let mut out = String::with_capacity(self.components.len() * 256);
128 render_metadata_components(self.type_name, self.components, &mut out);
129 Ok(out)
130 }
131}
132
133struct UpdateMetadataOp<'a, S: AsRef<str>> {
134 type_name: &'a str,
135 components: &'a [S],
136}
137
138impl<S: AsRef<str>> SoapOperation for UpdateMetadataOp<'_, S> {
139 const NAME: &'static str = "updateMetadata";
140 type Response = SaveResultsWire;
141
142 fn render_body(&self) -> MetadataResult<String> {
143 let mut out = String::with_capacity(self.components.len() * 256);
144 render_metadata_components(self.type_name, self.components, &mut out);
145 Ok(out)
146 }
147}
148
149struct UpsertMetadataOp<'a, S: AsRef<str>> {
150 type_name: &'a str,
151 components: &'a [S],
152}
153
154#[derive(Deserialize)]
155struct UpsertResultsWire {
156 #[serde(default, rename = "result")]
157 results: Vec<UpsertResult>,
158}
159
160impl<S: AsRef<str>> SoapOperation for UpsertMetadataOp<'_, S> {
161 const NAME: &'static str = "upsertMetadata";
162 type Response = UpsertResultsWire;
163
164 fn render_body(&self) -> MetadataResult<String> {
165 let mut out = String::with_capacity(self.components.len() * 256);
166 render_metadata_components(self.type_name, self.components, &mut out);
167 Ok(out)
168 }
169}
170
171struct DeleteMetadataOp<'a, S: AsRef<str>> {
172 type_name: &'a str,
173 full_names: &'a [S],
174}
175
176#[derive(Deserialize)]
177struct DeleteResultsWire {
178 #[serde(default, rename = "result")]
179 results: Vec<DeleteResult>,
180}
181
182impl<S: AsRef<str>> SoapOperation for DeleteMetadataOp<'_, S> {
183 const NAME: &'static str = "deleteMetadata";
184 type Response = DeleteResultsWire;
185
186 fn render_body(&self) -> MetadataResult<String> {
187 let mut out = String::with_capacity(64 + self.full_names.len() * 64);
188 render_type_and_full_names(self.type_name, self.full_names, &mut out);
189 Ok(out)
190 }
191}
192
193struct ReadMetadataOp<'a, T, S: AsRef<str>> {
194 type_name: &'a str,
195 full_names: &'a [S],
196 _marker: PhantomData<fn() -> T>,
197}
198
199#[derive(Deserialize)]
200#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
201struct ReadMetadataResponseWire<T> {
202 result: ReadResultWire<T>,
203}
204
205#[derive(Deserialize)]
206#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
211struct ReadResultWire<T> {
212 #[serde(default = "Vec::new")]
213 records: Vec<T>,
214}
215
216impl<T, S> SoapOperation for ReadMetadataOp<'_, T, S>
217where
218 T: DeserializeOwned,
219 S: AsRef<str>,
220{
221 const NAME: &'static str = "readMetadata";
222 type Response = ReadMetadataResponseWire<T>;
223
224 fn render_body(&self) -> MetadataResult<String> {
225 let mut out = String::with_capacity(64 + self.full_names.len() * 64);
226 render_type_and_full_names(self.type_name, self.full_names, &mut out);
227 Ok(out)
228 }
229}
230
231struct RenameMetadataOp<'a> {
232 type_name: &'a str,
233 old_full_name: &'a str,
234 new_full_name: &'a str,
235}
236
237#[derive(Deserialize)]
238struct RenameMetadataResponseWire {
239 result: SaveResult,
240}
241
242impl SoapOperation for RenameMetadataOp<'_> {
243 const NAME: &'static str = "renameMetadata";
244 type Response = RenameMetadataResponseWire;
245
246 fn render_body(&self) -> MetadataResult<String> {
247 Ok(format!(
248 "<met:type>{}</met:type>\
249 <met:oldFullName>{}</met:oldFullName>\
250 <met:newFullName>{}</met:newFullName>",
251 xml_escape(self.type_name),
252 xml_escape(self.old_full_name),
253 xml_escape(self.new_full_name),
254 ))
255 }
256}
257
258impl MetadataClient {
263 pub async fn create_metadata<S: AsRef<str>>(
293 &self,
294 type_name: &str,
295 components: &[S],
296 ) -> MetadataResult<Vec<SaveResult>> {
297 check_component_cap(components.len(), "create_metadata")?;
298 let op = CreateMetadataOp {
299 type_name,
300 components,
301 };
302 let resp = self.call(&op).await?;
303 Ok(resp.results)
304 }
305
306 pub async fn update_metadata<S: AsRef<str>>(
312 &self,
313 type_name: &str,
314 components: &[S],
315 ) -> MetadataResult<Vec<SaveResult>> {
316 check_component_cap(components.len(), "update_metadata")?;
317 let op = UpdateMetadataOp {
318 type_name,
319 components,
320 };
321 let resp = self.call(&op).await?;
322 Ok(resp.results)
323 }
324
325 pub async fn upsert_metadata<S: AsRef<str>>(
332 &self,
333 type_name: &str,
334 components: &[S],
335 ) -> MetadataResult<Vec<UpsertResult>> {
336 check_component_cap(components.len(), "upsert_metadata")?;
337 let op = UpsertMetadataOp {
338 type_name,
339 components,
340 };
341 let resp = self.call(&op).await?;
342 Ok(resp.results)
343 }
344
345 pub async fn delete_metadata<S: AsRef<str>>(
350 &self,
351 type_name: &str,
352 full_names: &[S],
353 ) -> MetadataResult<Vec<DeleteResult>> {
354 check_component_cap(full_names.len(), "delete_metadata")?;
355 let op = DeleteMetadataOp {
356 type_name,
357 full_names,
358 };
359 let resp = self.call(&op).await?;
360 Ok(resp.results)
361 }
362
363 pub async fn read_metadata<T, S>(
391 &self,
392 type_name: &str,
393 full_names: &[S],
394 ) -> MetadataResult<Vec<T>>
395 where
396 T: DeserializeOwned,
397 S: AsRef<str>,
398 {
399 check_component_cap(full_names.len(), "read_metadata")?;
400 let op = ReadMetadataOp::<T, S> {
401 type_name,
402 full_names,
403 _marker: PhantomData,
404 };
405 let resp = self.call(&op).await?;
406 Ok(resp.result.records)
407 }
408
409 pub async fn rename_metadata(
414 &self,
415 type_name: &str,
416 old_full_name: &str,
417 new_full_name: &str,
418 ) -> MetadataResult<SaveResult> {
419 let op = RenameMetadataOp {
420 type_name,
421 old_full_name,
422 new_full_name,
423 };
424 let resp = self.call(&op).await?;
425 Ok(resp.result)
426 }
427}
428
429#[cfg(test)]
430#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn create_op_wraps_components_with_xsi_type_and_default_ns() {
436 let op = CreateMetadataOp {
437 type_name: "ApexClass",
438 components: &["<fullName>Foo</fullName>"],
439 };
440 let body = op.render_body().unwrap();
441 assert!(body.contains(r#"<met:metadata xsi:type="met:ApexClass""#));
442 assert!(body.contains(r#"xmlns="http://soap.sforce.com/2006/04/metadata""#));
443 assert!(body.contains("<fullName>Foo</fullName>"));
444 assert!(body.contains("</met:metadata>"));
445 }
446
447 #[test]
448 fn create_op_emits_one_wrapper_per_component() {
449 let op = CreateMetadataOp {
450 type_name: "ApexClass",
451 components: &["<fullName>A</fullName>", "<fullName>B</fullName>"],
452 };
453 let body = op.render_body().unwrap();
454 assert_eq!(
455 body.matches(r#"<met:metadata xsi:type="met:ApexClass""#)
456 .count(),
457 2
458 );
459 assert_eq!(body.matches("</met:metadata>").count(), 2);
460 }
461
462 #[test]
463 fn read_op_emits_type_and_full_names() {
464 #[derive(Deserialize)]
467 struct Empty {}
468 let op = ReadMetadataOp::<Empty, _> {
469 type_name: "ApexClass",
470 full_names: &["Foo", "Bar"],
471 _marker: PhantomData,
472 };
473 let body = op.render_body().unwrap();
474 assert_eq!(
475 body,
476 "<met:type>ApexClass</met:type>\
477 <met:fullNames>Foo</met:fullNames>\
478 <met:fullNames>Bar</met:fullNames>"
479 );
480 }
481
482 #[test]
483 fn delete_op_shares_body_shape_with_read() {
484 let op = DeleteMetadataOp {
485 type_name: "ApexTrigger",
486 full_names: &["AccountTrigger"],
487 };
488 let body = op.render_body().unwrap();
489 assert_eq!(
490 body,
491 "<met:type>ApexTrigger</met:type>\
492 <met:fullNames>AccountTrigger</met:fullNames>"
493 );
494 }
495
496 #[test]
497 fn rename_op_emits_type_and_both_full_names() {
498 let op = RenameMetadataOp {
499 type_name: "ApexClass",
500 old_full_name: "OldName",
501 new_full_name: "NewName",
502 };
503 let body = op.render_body().unwrap();
504 assert_eq!(
505 body,
506 "<met:type>ApexClass</met:type>\
507 <met:oldFullName>OldName</met:oldFullName>\
508 <met:newFullName>NewName</met:newFullName>"
509 );
510 }
511
512 #[test]
513 fn render_escapes_special_chars_in_type_and_names() {
514 let op = DeleteMetadataOp {
515 type_name: "Weird<>",
516 full_names: &["a&b"],
517 };
518 let body = op.render_body().unwrap();
519 assert!(body.contains("<met:type>Weird<></met:type>"));
520 assert!(body.contains("<met:fullNames>a&b</met:fullNames>"));
521 }
522
523 #[test]
524 fn check_component_cap_rejects_empty_input() {
525 let err = check_component_cap(0, "create_metadata").unwrap_err();
526 assert!(err.to_string().contains("at least one"));
527 }
528
529 #[test]
530 fn check_component_cap_rejects_more_than_ten() {
531 let err = check_component_cap(11, "delete_metadata").unwrap_err();
532 let msg = err.to_string();
533 assert!(msg.contains("10"));
534 assert!(msg.contains("11"));
535 }
536
537 #[test]
538 fn check_component_cap_accepts_one_to_ten() {
539 for n in 1..=10 {
540 assert!(check_component_cap(n, "x").is_ok(), "should accept {n}");
541 }
542 }
543}