cirrus_metadata/handlers/
utility.rs1use crate::MetadataClient;
15use crate::envelope::xml_escape;
16use crate::error::{MetadataError, MetadataResult};
17use crate::result::{
18 DescribeMetadataResult, DescribeValueTypeResult, FileProperties, ListMetadataQuery,
19};
20use crate::transport::SoapOperation;
21use serde::Deserialize;
22
23pub const MAX_LIST_METADATA_QUERIES_PER_CALL: usize = 3;
26
27struct ListMetadataOp<'a> {
32 queries: Vec<ListMetadataQuery>,
33 as_of_version: &'a str,
34}
35
36#[derive(Deserialize)]
41struct ListMetadataResponseWire {
42 #[serde(default, rename = "result")]
43 results: Vec<FileProperties>,
44}
45
46impl SoapOperation for ListMetadataOp<'_> {
47 const NAME: &'static str = "listMetadata";
48 const IDEMPOTENT: bool = true;
50 type Response = ListMetadataResponseWire;
51
52 fn render_body(&self) -> MetadataResult<String> {
53 let mut out = String::with_capacity(64 + self.queries.len() * 64);
54 for q in &self.queries {
55 out.push_str("<met:queries>");
56 if let Some(folder) = &q.folder {
57 out.push_str("<met:folder>");
58 out.push_str(&xml_escape(folder));
59 out.push_str("</met:folder>");
60 }
61 out.push_str("<met:type>");
62 out.push_str(&xml_escape(&q.type_name));
63 out.push_str("</met:type>");
64 out.push_str("</met:queries>");
65 }
66 out.push_str("<met:asOfVersion>");
70 out.push_str(&xml_escape(self.as_of_version));
71 out.push_str("</met:asOfVersion>");
72 Ok(out)
73 }
74}
75
76struct DescribeMetadataOp<'a> {
77 as_of_version: &'a str,
78}
79
80#[derive(Deserialize)]
81struct DescribeMetadataResponseWire {
82 result: DescribeMetadataResult,
83}
84
85impl SoapOperation for DescribeMetadataOp<'_> {
86 const NAME: &'static str = "describeMetadata";
87 const IDEMPOTENT: bool = true;
89 type Response = DescribeMetadataResponseWire;
90
91 fn render_body(&self) -> MetadataResult<String> {
92 Ok(format!(
93 "<met:asOfVersion>{}</met:asOfVersion>",
94 xml_escape(self.as_of_version),
95 ))
96 }
97}
98
99struct DescribeValueTypeOp {
100 type_name: String,
101}
102
103#[derive(Deserialize)]
104struct DescribeValueTypeResponseWire {
105 result: DescribeValueTypeResult,
106}
107
108impl SoapOperation for DescribeValueTypeOp {
109 const NAME: &'static str = "describeValueType";
110 const IDEMPOTENT: bool = true;
112 type Response = DescribeValueTypeResponseWire;
113
114 fn render_body(&self) -> MetadataResult<String> {
115 Ok(format!(
116 "<met:type>{}</met:type>",
117 xml_escape(&self.type_name),
118 ))
119 }
120}
121
122impl MetadataClient {
127 pub async fn list_metadata(
143 &self,
144 queries: Vec<ListMetadataQuery>,
145 as_of_version: &str,
146 ) -> MetadataResult<Vec<FileProperties>> {
147 if queries.is_empty() {
148 return Err(MetadataError::InvalidArgument(
149 "listMetadata requires at least one query; got 0".to_string(),
150 ));
151 }
152 if queries.len() > MAX_LIST_METADATA_QUERIES_PER_CALL {
153 return Err(MetadataError::InvalidArgument(format!(
154 "listMetadata accepts at most {MAX_LIST_METADATA_QUERIES_PER_CALL} queries per \
155 call; got {}",
156 queries.len()
157 )));
158 }
159 let op = ListMetadataOp {
160 queries,
161 as_of_version,
162 };
163 let resp = self.call(&op).await?;
164 Ok(resp.results)
165 }
166
167 pub async fn describe_metadata(
182 &self,
183 as_of_version: &str,
184 ) -> MetadataResult<DescribeMetadataResult> {
185 let op = DescribeMetadataOp { as_of_version };
186 let resp = self.call(&op).await?;
187 Ok(resp.result)
188 }
189
190 pub async fn describe_value_type(
203 &self,
204 qualified_type_name: &str,
205 ) -> MetadataResult<DescribeValueTypeResult> {
206 let op = DescribeValueTypeOp {
207 type_name: qualified_type_name.to_string(),
208 };
209 let resp = self.call(&op).await?;
210 Ok(resp.result)
211 }
212}
213
214#[cfg(test)]
215#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn list_metadata_op_emits_queries_and_version() {
221 let op = ListMetadataOp {
222 queries: vec![
223 ListMetadataQuery {
224 type_name: "ApexClass".into(),
225 folder: None,
226 },
227 ListMetadataQuery {
228 type_name: "Dashboard".into(),
229 folder: Some("SharedDashboards".into()),
230 },
231 ],
232 as_of_version: "66.0",
233 };
234 let body = op.render_body().unwrap();
235 assert!(body.contains("<met:queries><met:type>ApexClass</met:type></met:queries>"));
236 assert!(body.contains(
237 "<met:queries><met:folder>SharedDashboards</met:folder><met:type>Dashboard</met:type></met:queries>"
238 ));
239 assert!(body.contains("<met:asOfVersion>66.0</met:asOfVersion>"));
240 }
241
242 #[test]
243 fn list_metadata_op_escapes_folder_and_type() {
244 let op = ListMetadataOp {
245 queries: vec![ListMetadataQuery {
246 type_name: "Type<>".into(),
247 folder: Some("Folder&Co".into()),
248 }],
249 as_of_version: "66.0",
250 };
251 let body = op.render_body().unwrap();
252 assert!(body.contains("<met:folder>Folder&Co</met:folder>"));
253 assert!(body.contains("<met:type>Type<></met:type>"));
254 }
255
256 #[test]
257 fn describe_metadata_op_emits_version() {
258 let op = DescribeMetadataOp {
259 as_of_version: "58.0",
260 };
261 let body = op.render_body().unwrap();
262 assert_eq!(body, "<met:asOfVersion>58.0</met:asOfVersion>");
263 }
264
265 #[test]
266 fn describe_value_type_emits_qualified_type() {
267 let op = DescribeValueTypeOp {
268 type_name: "{http://soap.sforce.com/2006/04/metadata}ApexClass".into(),
269 };
270 let body = op.render_body().unwrap();
271 assert_eq!(
274 body,
275 "<met:type>{http://soap.sforce.com/2006/04/metadata}ApexClass</met:type>"
276 );
277 }
278}