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 type Response = ListMetadataResponseWire;
49
50 fn render_body(&self) -> MetadataResult<String> {
51 let mut out = String::with_capacity(64 + self.queries.len() * 64);
52 for q in &self.queries {
53 out.push_str("<met:queries>");
54 if let Some(folder) = &q.folder {
55 out.push_str("<met:folder>");
56 out.push_str(&xml_escape(folder));
57 out.push_str("</met:folder>");
58 }
59 out.push_str("<met:type>");
60 out.push_str(&xml_escape(&q.type_name));
61 out.push_str("</met:type>");
62 out.push_str("</met:queries>");
63 }
64 out.push_str("<met:asOfVersion>");
68 out.push_str(&xml_escape(self.as_of_version));
69 out.push_str("</met:asOfVersion>");
70 Ok(out)
71 }
72}
73
74struct DescribeMetadataOp<'a> {
75 as_of_version: &'a str,
76}
77
78#[derive(Deserialize)]
79struct DescribeMetadataResponseWire {
80 result: DescribeMetadataResult,
81}
82
83impl SoapOperation for DescribeMetadataOp<'_> {
84 const NAME: &'static str = "describeMetadata";
85 type Response = DescribeMetadataResponseWire;
86
87 fn render_body(&self) -> MetadataResult<String> {
88 Ok(format!(
89 "<met:asOfVersion>{}</met:asOfVersion>",
90 xml_escape(self.as_of_version),
91 ))
92 }
93}
94
95struct DescribeValueTypeOp {
96 type_name: String,
97}
98
99#[derive(Deserialize)]
100struct DescribeValueTypeResponseWire {
101 result: DescribeValueTypeResult,
102}
103
104impl SoapOperation for DescribeValueTypeOp {
105 const NAME: &'static str = "describeValueType";
106 type Response = DescribeValueTypeResponseWire;
107
108 fn render_body(&self) -> MetadataResult<String> {
109 Ok(format!(
110 "<met:type>{}</met:type>",
111 xml_escape(&self.type_name),
112 ))
113 }
114}
115
116impl MetadataClient {
121 pub async fn list_metadata(
137 &self,
138 queries: Vec<ListMetadataQuery>,
139 as_of_version: &str,
140 ) -> MetadataResult<Vec<FileProperties>> {
141 if queries.len() > MAX_LIST_METADATA_QUERIES_PER_CALL {
142 return Err(MetadataError::InvalidArgument(format!(
143 "listMetadata accepts at most {MAX_LIST_METADATA_QUERIES_PER_CALL} queries per \
144 call; got {}",
145 queries.len()
146 )));
147 }
148 let op = ListMetadataOp {
149 queries,
150 as_of_version,
151 };
152 let resp = self.call(&op).await?;
153 Ok(resp.results)
154 }
155
156 pub async fn describe_metadata(
171 &self,
172 as_of_version: &str,
173 ) -> MetadataResult<DescribeMetadataResult> {
174 let op = DescribeMetadataOp { as_of_version };
175 let resp = self.call(&op).await?;
176 Ok(resp.result)
177 }
178
179 pub async fn describe_value_type(
192 &self,
193 qualified_type_name: &str,
194 ) -> MetadataResult<DescribeValueTypeResult> {
195 let op = DescribeValueTypeOp {
196 type_name: qualified_type_name.to_string(),
197 };
198 let resp = self.call(&op).await?;
199 Ok(resp.result)
200 }
201}
202
203#[cfg(test)]
204#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn list_metadata_op_emits_queries_and_version() {
210 let op = ListMetadataOp {
211 queries: vec![
212 ListMetadataQuery {
213 type_name: "ApexClass".into(),
214 folder: None,
215 },
216 ListMetadataQuery {
217 type_name: "Dashboard".into(),
218 folder: Some("SharedDashboards".into()),
219 },
220 ],
221 as_of_version: "66.0",
222 };
223 let body = op.render_body().unwrap();
224 assert!(body.contains("<met:queries><met:type>ApexClass</met:type></met:queries>"));
225 assert!(body.contains(
226 "<met:queries><met:folder>SharedDashboards</met:folder><met:type>Dashboard</met:type></met:queries>"
227 ));
228 assert!(body.contains("<met:asOfVersion>66.0</met:asOfVersion>"));
229 }
230
231 #[test]
232 fn list_metadata_op_escapes_folder_and_type() {
233 let op = ListMetadataOp {
234 queries: vec![ListMetadataQuery {
235 type_name: "Type<>".into(),
236 folder: Some("Folder&Co".into()),
237 }],
238 as_of_version: "66.0",
239 };
240 let body = op.render_body().unwrap();
241 assert!(body.contains("<met:folder>Folder&Co</met:folder>"));
242 assert!(body.contains("<met:type>Type<></met:type>"));
243 }
244
245 #[test]
246 fn describe_metadata_op_emits_version() {
247 let op = DescribeMetadataOp {
248 as_of_version: "58.0",
249 };
250 let body = op.render_body().unwrap();
251 assert_eq!(body, "<met:asOfVersion>58.0</met:asOfVersion>");
252 }
253
254 #[test]
255 fn describe_value_type_emits_qualified_type() {
256 let op = DescribeValueTypeOp {
257 type_name: "{http://soap.sforce.com/2006/04/metadata}ApexClass".into(),
258 };
259 let body = op.render_body().unwrap();
260 assert_eq!(
263 body,
264 "<met:type>{http://soap.sforce.com/2006/04/metadata}ApexClass</met:type>"
265 );
266 }
267}