cirrus_metadata/handlers/
crud.rs1use crate::MetadataClient;
47use crate::envelope::xml_escape;
48use crate::error::{MetadataError, MetadataResult};
49use crate::result::{DeleteResult, SaveResult, UpsertResult};
50use crate::transport::SoapOperation;
51use serde::Deserialize;
52use serde::de::DeserializeOwned;
53use std::marker::PhantomData;
54
55pub const MAX_CRUD_COMPONENTS_PER_CALL: usize = 10;
61
62pub const MAX_CRUD_COMPONENTS_PER_CALL_LARGE: usize = 200;
66
67fn per_call_cap(type_name: &str) -> usize {
69 match type_name {
70 "CustomMetadata" | "CustomApplication" => MAX_CRUD_COMPONENTS_PER_CALL_LARGE,
71 _ => MAX_CRUD_COMPONENTS_PER_CALL,
72 }
73}
74
75fn render_metadata_components<S: AsRef<str>>(type_name: &str, components: &[S], out: &mut String) {
83 for component in components {
84 out.push_str(r#"<met:metadata xsi:type="met:"#);
85 out.push_str(&xml_escape(type_name));
86 out.push_str(r#"" xmlns="http://soap.sforce.com/2006/04/metadata">"#);
91 out.push_str(component.as_ref());
92 out.push_str("</met:metadata>");
93 }
94}
95
96fn render_type_and_full_names<S: AsRef<str>>(type_name: &str, full_names: &[S], out: &mut String) {
99 out.push_str("<met:type>");
100 out.push_str(&xml_escape(type_name));
101 out.push_str("</met:type>");
102 for name in full_names {
103 out.push_str("<met:fullNames>");
104 out.push_str(&xml_escape(name.as_ref()));
105 out.push_str("</met:fullNames>");
106 }
107}
108
109fn check_component_cap(count: usize, type_name: &str, op_label: &str) -> MetadataResult<()> {
110 if count == 0 {
111 return Err(MetadataError::InvalidArgument(format!(
112 "{op_label} requires at least one component; got 0"
113 )));
114 }
115 let cap = per_call_cap(type_name);
116 if count > cap {
117 return Err(MetadataError::InvalidArgument(format!(
118 "{op_label} accepts at most {cap} {type_name} components per call; got {count}"
119 )));
120 }
121 Ok(())
122}
123
124struct CreateMetadataOp<'a, S: AsRef<str>> {
129 type_name: &'a str,
130 components: &'a [S],
131}
132
133#[derive(Deserialize)]
134struct SaveResultsWire {
135 #[serde(default, rename = "result")]
136 results: Vec<SaveResult>,
137}
138
139impl<S: AsRef<str>> SoapOperation for CreateMetadataOp<'_, S> {
140 const NAME: &'static str = "createMetadata";
141 type Response = SaveResultsWire;
142
143 fn render_body(&self) -> MetadataResult<String> {
144 let mut out = String::with_capacity(self.components.len() * 256);
145 render_metadata_components(self.type_name, self.components, &mut out);
146 Ok(out)
147 }
148}
149
150struct UpdateMetadataOp<'a, S: AsRef<str>> {
151 type_name: &'a str,
152 components: &'a [S],
153}
154
155impl<S: AsRef<str>> SoapOperation for UpdateMetadataOp<'_, S> {
156 const NAME: &'static str = "updateMetadata";
157 type Response = SaveResultsWire;
158
159 fn render_body(&self) -> MetadataResult<String> {
160 let mut out = String::with_capacity(self.components.len() * 256);
161 render_metadata_components(self.type_name, self.components, &mut out);
162 Ok(out)
163 }
164}
165
166struct UpsertMetadataOp<'a, S: AsRef<str>> {
167 type_name: &'a str,
168 components: &'a [S],
169}
170
171#[derive(Deserialize)]
172struct UpsertResultsWire {
173 #[serde(default, rename = "result")]
174 results: Vec<UpsertResult>,
175}
176
177impl<S: AsRef<str>> SoapOperation for UpsertMetadataOp<'_, S> {
178 const NAME: &'static str = "upsertMetadata";
179 type Response = UpsertResultsWire;
180
181 fn render_body(&self) -> MetadataResult<String> {
182 let mut out = String::with_capacity(self.components.len() * 256);
183 render_metadata_components(self.type_name, self.components, &mut out);
184 Ok(out)
185 }
186}
187
188struct DeleteMetadataOp<'a, S: AsRef<str>> {
189 type_name: &'a str,
190 full_names: &'a [S],
191}
192
193#[derive(Deserialize)]
194struct DeleteResultsWire {
195 #[serde(default, rename = "result")]
196 results: Vec<DeleteResult>,
197}
198
199impl<S: AsRef<str>> SoapOperation for DeleteMetadataOp<'_, S> {
200 const NAME: &'static str = "deleteMetadata";
201 type Response = DeleteResultsWire;
202
203 fn render_body(&self) -> MetadataResult<String> {
204 let mut out = String::with_capacity(64 + self.full_names.len() * 64);
205 render_type_and_full_names(self.type_name, self.full_names, &mut out);
206 Ok(out)
207 }
208}
209
210struct ReadMetadataOp<'a, T, S: AsRef<str>> {
211 type_name: &'a str,
212 full_names: &'a [S],
213 _marker: PhantomData<fn() -> T>,
214}
215
216#[derive(Deserialize)]
217#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
218struct ReadMetadataResponseWire<T> {
219 result: ReadResultWire<T>,
220}
221
222#[derive(Deserialize)]
223#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
228struct ReadResultWire<T> {
229 #[serde(default = "Vec::new")]
230 records: Vec<T>,
231}
232
233impl<T, S> SoapOperation for ReadMetadataOp<'_, T, S>
234where
235 T: DeserializeOwned,
236 S: AsRef<str>,
237{
238 const NAME: &'static str = "readMetadata";
239 const IDEMPOTENT: bool = true;
241 type Response = ReadMetadataResponseWire<T>;
242
243 fn render_body(&self) -> MetadataResult<String> {
244 let mut out = String::with_capacity(64 + self.full_names.len() * 64);
245 render_type_and_full_names(self.type_name, self.full_names, &mut out);
246 Ok(out)
247 }
248}
249
250struct RenameMetadataOp<'a> {
251 type_name: &'a str,
252 old_full_name: &'a str,
253 new_full_name: &'a str,
254}
255
256#[derive(Deserialize)]
257struct RenameMetadataResponseWire {
258 result: SaveResult,
259}
260
261impl SoapOperation for RenameMetadataOp<'_> {
262 const NAME: &'static str = "renameMetadata";
263 type Response = RenameMetadataResponseWire;
264
265 fn render_body(&self) -> MetadataResult<String> {
266 Ok(format!(
267 "<met:type>{}</met:type>\
268 <met:oldFullName>{}</met:oldFullName>\
269 <met:newFullName>{}</met:newFullName>",
270 xml_escape(self.type_name),
271 xml_escape(self.old_full_name),
272 xml_escape(self.new_full_name),
273 ))
274 }
275}
276
277impl MetadataClient {
282 pub async fn create_metadata<S: AsRef<str>>(
312 &self,
313 type_name: &str,
314 components: &[S],
315 ) -> MetadataResult<Vec<SaveResult>> {
316 check_component_cap(components.len(), type_name, "create_metadata")?;
317 let op = CreateMetadataOp {
318 type_name,
319 components,
320 };
321 let resp = self.call(&op).await?;
322 Ok(resp.results)
323 }
324
325 pub async fn update_metadata<S: AsRef<str>>(
331 &self,
332 type_name: &str,
333 components: &[S],
334 ) -> MetadataResult<Vec<SaveResult>> {
335 check_component_cap(components.len(), type_name, "update_metadata")?;
336 let op = UpdateMetadataOp {
337 type_name,
338 components,
339 };
340 let resp = self.call(&op).await?;
341 Ok(resp.results)
342 }
343
344 pub async fn upsert_metadata<S: AsRef<str>>(
351 &self,
352 type_name: &str,
353 components: &[S],
354 ) -> MetadataResult<Vec<UpsertResult>> {
355 check_component_cap(components.len(), type_name, "upsert_metadata")?;
356 let op = UpsertMetadataOp {
357 type_name,
358 components,
359 };
360 let resp = self.call(&op).await?;
361 Ok(resp.results)
362 }
363
364 pub async fn delete_metadata<S: AsRef<str>>(
369 &self,
370 type_name: &str,
371 full_names: &[S],
372 ) -> MetadataResult<Vec<DeleteResult>> {
373 check_component_cap(full_names.len(), type_name, "delete_metadata")?;
374 let op = DeleteMetadataOp {
375 type_name,
376 full_names,
377 };
378 let resp = self.call(&op).await?;
379 Ok(resp.results)
380 }
381
382 pub async fn read_metadata<T, S>(
410 &self,
411 type_name: &str,
412 full_names: &[S],
413 ) -> MetadataResult<Vec<T>>
414 where
415 T: DeserializeOwned,
416 S: AsRef<str>,
417 {
418 check_component_cap(full_names.len(), type_name, "read_metadata")?;
419 let op = ReadMetadataOp::<T, S> {
420 type_name,
421 full_names,
422 _marker: PhantomData,
423 };
424 let resp = self.call(&op).await?;
425 Ok(resp.result.records)
426 }
427
428 pub async fn rename_metadata(
433 &self,
434 type_name: &str,
435 old_full_name: &str,
436 new_full_name: &str,
437 ) -> MetadataResult<SaveResult> {
438 let op = RenameMetadataOp {
439 type_name,
440 old_full_name,
441 new_full_name,
442 };
443 let resp = self.call(&op).await?;
444 Ok(resp.result)
445 }
446}
447
448#[cfg(test)]
449#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn create_op_wraps_components_with_xsi_type_and_default_ns() {
455 let op = CreateMetadataOp {
456 type_name: "ApexClass",
457 components: &["<fullName>Foo</fullName>"],
458 };
459 let body = op.render_body().unwrap();
460 assert!(body.contains(r#"<met:metadata xsi:type="met:ApexClass""#));
461 assert!(body.contains(r#"xmlns="http://soap.sforce.com/2006/04/metadata""#));
462 assert!(body.contains("<fullName>Foo</fullName>"));
463 assert!(body.contains("</met:metadata>"));
464 }
465
466 #[test]
467 fn create_op_emits_one_wrapper_per_component() {
468 let op = CreateMetadataOp {
469 type_name: "ApexClass",
470 components: &["<fullName>A</fullName>", "<fullName>B</fullName>"],
471 };
472 let body = op.render_body().unwrap();
473 assert_eq!(
474 body.matches(r#"<met:metadata xsi:type="met:ApexClass""#)
475 .count(),
476 2
477 );
478 assert_eq!(body.matches("</met:metadata>").count(), 2);
479 }
480
481 #[test]
482 fn read_op_emits_type_and_full_names() {
483 #[derive(Deserialize)]
486 struct Empty {}
487 let op = ReadMetadataOp::<Empty, _> {
488 type_name: "ApexClass",
489 full_names: &["Foo", "Bar"],
490 _marker: PhantomData,
491 };
492 let body = op.render_body().unwrap();
493 assert_eq!(
494 body,
495 "<met:type>ApexClass</met:type>\
496 <met:fullNames>Foo</met:fullNames>\
497 <met:fullNames>Bar</met:fullNames>"
498 );
499 }
500
501 #[test]
502 fn delete_op_shares_body_shape_with_read() {
503 let op = DeleteMetadataOp {
504 type_name: "ApexTrigger",
505 full_names: &["AccountTrigger"],
506 };
507 let body = op.render_body().unwrap();
508 assert_eq!(
509 body,
510 "<met:type>ApexTrigger</met:type>\
511 <met:fullNames>AccountTrigger</met:fullNames>"
512 );
513 }
514
515 #[test]
516 fn rename_op_emits_type_and_both_full_names() {
517 let op = RenameMetadataOp {
518 type_name: "ApexClass",
519 old_full_name: "OldName",
520 new_full_name: "NewName",
521 };
522 let body = op.render_body().unwrap();
523 assert_eq!(
524 body,
525 "<met:type>ApexClass</met:type>\
526 <met:oldFullName>OldName</met:oldFullName>\
527 <met:newFullName>NewName</met:newFullName>"
528 );
529 }
530
531 #[test]
532 fn render_escapes_special_chars_in_type_and_names() {
533 let op = DeleteMetadataOp {
534 type_name: "Weird<>",
535 full_names: &["a&b"],
536 };
537 let body = op.render_body().unwrap();
538 assert!(body.contains("<met:type>Weird<></met:type>"));
539 assert!(body.contains("<met:fullNames>a&b</met:fullNames>"));
540 }
541
542 #[test]
543 fn check_component_cap_rejects_empty_input() {
544 let err = check_component_cap(0, "ApexClass", "create_metadata").unwrap_err();
545 assert!(err.to_string().contains("at least one"));
546 }
547
548 #[test]
549 fn check_component_cap_rejects_more_than_ten() {
550 let err = check_component_cap(11, "ApexClass", "delete_metadata").unwrap_err();
551 let msg = err.to_string();
552 assert!(msg.contains("10"));
553 assert!(msg.contains("11"));
554 }
555
556 #[test]
557 fn check_component_cap_accepts_one_to_ten() {
558 for n in 1..=10 {
559 assert!(
560 check_component_cap(n, "ApexClass", "x").is_ok(),
561 "should accept {n}"
562 );
563 }
564 }
565
566 #[test]
570 fn check_component_cap_allows_200_for_documented_large_types() {
571 for ty in ["CustomMetadata", "CustomApplication"] {
572 assert!(check_component_cap(11, ty, "x").is_ok());
573 assert!(check_component_cap(200, ty, "x").is_ok());
574 let err = check_component_cap(201, ty, "x").unwrap_err();
575 let msg = err.to_string();
576 assert!(
577 msg.contains("200"),
578 "message should cite the 200 cap: {msg}"
579 );
580 }
581 }
582}