1use anyhow::Result;
11use url::Url;
12
13use crate::datadog::client::DatadogClient;
14use crate::datadog::types::{Monitor, MonitorSearchResult};
15
16pub const HARD_CAP: usize = 10_000;
19
20pub const LIST_PAGE_SIZE: usize = 100;
26const SEARCH_PAGE_SIZE: usize = 30;
27
28#[derive(Debug, Default, Clone)]
35pub struct MonitorListFilter {
36 pub name: Option<String>,
38 pub tags: Option<String>,
40 pub monitor_tags: Option<String>,
42}
43
44#[derive(Debug)]
46pub struct MonitorsApi<'a> {
47 client: &'a DatadogClient,
48}
49
50impl<'a> MonitorsApi<'a> {
51 #[must_use]
53 pub fn new(client: &'a DatadogClient) -> Self {
54 Self { client }
55 }
56
57 pub async fn list(&self, filter: &MonitorListFilter, limit: usize) -> Result<Vec<Monitor>> {
64 let cap = effective_cap(limit);
65 let mut out: Vec<Monitor> = Vec::new();
66 let mut page: u32 = 0;
67 loop {
68 let remaining = cap - out.len();
69 let page_size = remaining.min(LIST_PAGE_SIZE);
70 let url = build_list_url(self.client.base_url(), filter, page, page_size)?;
71 let batch: Vec<Monitor> = self
72 .client
73 .get_parsed(url.as_str(), "Failed to parse /api/v1/monitor response")
74 .await?;
75 let exhausted = batch.len() < page_size;
76 out.extend(batch);
77 if out.len() >= cap || exhausted {
78 break;
79 }
80 page += 1;
81 }
82 out.truncate(cap);
83 Ok(out)
84 }
85
86 pub async fn get(&self, id: i64) -> Result<Monitor> {
88 let url = build_get_url(self.client.base_url(), id)?;
89 self.client
90 .get_parsed(
91 url.as_str(),
92 "Failed to parse /api/v1/monitor/<id> response",
93 )
94 .await
95 }
96
97 pub async fn search(&self, query: &str, limit: usize) -> Result<MonitorSearchResult> {
105 let cap = effective_cap(limit);
106 let mut acc: Option<MonitorSearchResult> = None;
107 let mut page: u32 = 0;
108 loop {
109 let collected = acc.as_ref().map_or(0, |r| r.monitors.len());
110 let remaining = cap - collected;
111 let per_page = remaining.min(SEARCH_PAGE_SIZE);
112 let url = build_search_url(self.client.base_url(), query, page, per_page)?;
113 let batch: MonitorSearchResult = self
114 .client
115 .get_parsed(
116 url.as_str(),
117 "Failed to parse /api/v1/monitor/search response",
118 )
119 .await?;
120 let batch_len = batch.monitors.len();
121 let page_count = batch.metadata.as_ref().and_then(|m| m.page_count);
122 let exhausted_by_size = batch_len < per_page;
123 let exhausted_by_metadata = page_count.is_some_and(|pc| i64::from(page) + 1 >= pc);
124
125 match acc.as_mut() {
126 Some(existing) => existing.monitors.extend(batch.monitors),
127 None => acc = Some(batch),
128 }
129
130 let collected = acc.as_ref().map_or(0, |r| r.monitors.len());
131 if collected >= cap || exhausted_by_size || exhausted_by_metadata {
132 break;
133 }
134 page += 1;
135 }
136 let mut result = acc.unwrap_or_default();
137 result.monitors.truncate(cap);
138 Ok(result)
139 }
140}
141
142fn effective_cap(limit: usize) -> usize {
145 if limit == 0 {
146 HARD_CAP
147 } else {
148 limit.min(HARD_CAP)
149 }
150}
151
152fn build_list_url(
154 base_url: &str,
155 filter: &MonitorListFilter,
156 page: u32,
157 page_size: usize,
158) -> Result<Url> {
159 let mut url = DatadogClient::api_url(base_url, "/api/v1/monitor")?;
160 {
161 let mut q = url.query_pairs_mut();
162 if let Some(name) = filter.name.as_deref() {
163 q.append_pair("name", name);
164 }
165 if let Some(tags) = filter.tags.as_deref() {
166 q.append_pair("tags", tags);
167 }
168 if let Some(monitor_tags) = filter.monitor_tags.as_deref() {
169 q.append_pair("monitor_tags", monitor_tags);
170 }
171 q.append_pair("page", &page.to_string());
172 q.append_pair("page_size", &page_size.to_string());
173 }
174 Ok(url)
175}
176
177fn build_get_url(base_url: &str, id: i64) -> Result<Url> {
179 DatadogClient::api_url(base_url, &format!("/api/v1/monitor/{id}"))
180}
181
182fn build_search_url(base_url: &str, query: &str, page: u32, per_page: usize) -> Result<Url> {
184 let mut url = DatadogClient::api_url(base_url, "/api/v1/monitor/search")?;
185 url.query_pairs_mut()
186 .append_pair("query", query)
187 .append_pair("page", &page.to_string())
188 .append_pair("per_page", &per_page.to_string());
189 Ok(url)
190}
191
192#[cfg(test)]
193#[allow(clippy::unwrap_used, clippy::expect_used)]
194mod tests {
195 use super::*;
196
197 #[test]
200 fn effective_cap_zero_means_hard_cap() {
201 assert_eq!(effective_cap(0), HARD_CAP);
202 }
203
204 #[test]
205 fn effective_cap_clamps_to_hard_cap() {
206 assert_eq!(effective_cap(HARD_CAP + 5), HARD_CAP);
207 }
208
209 #[test]
210 fn effective_cap_passes_through_small_limits() {
211 assert_eq!(effective_cap(42), 42);
212 }
213
214 #[test]
217 fn build_list_url_appends_only_provided_filters() {
218 let filter = MonitorListFilter {
219 name: Some("cpu".into()),
220 tags: None,
221 monitor_tags: None,
222 };
223 let url = build_list_url("https://api.datadoghq.com", &filter, 0, 100).unwrap();
224 let qs = url.query().unwrap();
225 assert!(qs.contains("name=cpu"));
226 assert!(qs.contains("page=0"));
227 assert!(qs.contains("page_size=100"));
228 assert!(!qs.contains("tags="));
229 assert!(!qs.contains("monitor_tags="));
230 }
231
232 #[test]
233 fn build_list_url_encodes_tags_and_monitor_tags() {
234 let filter = MonitorListFilter {
235 name: None,
236 tags: Some("team:sre,env:prod".into()),
237 monitor_tags: Some("severity:high".into()),
238 };
239 let url = build_list_url("https://api.datadoghq.com", &filter, 2, 50).unwrap();
240 let qs = url.query().unwrap();
241 assert!(qs.contains("tags=team%3Asre%2Cenv%3Aprod"));
243 assert!(qs.contains("monitor_tags=severity%3Ahigh"));
244 assert!(qs.contains("page=2"));
245 assert!(qs.contains("page_size=50"));
246 }
247
248 #[test]
249 fn build_list_url_rejects_invalid_base() {
250 let err = build_list_url("not a url", &MonitorListFilter::default(), 0, 100).unwrap_err();
251 assert!(err.to_string().contains("Invalid Datadog base URL"));
252 }
253
254 #[test]
255 fn build_get_url_includes_id_path_segment() {
256 let url = build_get_url("https://api.datadoghq.com", 12345).unwrap();
257 assert_eq!(url.path(), "/api/v1/monitor/12345");
258 }
259
260 #[test]
261 fn build_get_url_rejects_invalid_base() {
262 let err = build_get_url("not a url", 1).unwrap_err();
263 assert!(err.to_string().contains("Invalid Datadog base URL"));
264 }
265
266 #[test]
267 fn build_search_url_encodes_query() {
268 let url = build_search_url(
269 "https://api.datadoghq.com",
270 "status:alert AND env:prod",
271 0,
272 30,
273 )
274 .unwrap();
275 let qs = url.query().unwrap();
276 assert!(qs.contains("query=status%3Aalert+AND+env%3Aprod"));
277 assert!(qs.contains("page=0"));
278 assert!(qs.contains("per_page=30"));
279 }
280
281 #[test]
282 fn build_search_url_rejects_invalid_base() {
283 let err = build_search_url("not a url", "q", 0, 30).unwrap_err();
284 assert!(err.to_string().contains("Invalid Datadog base URL"));
285 }
286
287 fn monitor_json(id: i64, name: &str) -> serde_json::Value {
290 serde_json::json!({
291 "id": id,
292 "name": name,
293 "type": "metric alert",
294 "query": "avg(last_5m):avg:system.cpu.user{*} > 90",
295 "tags": ["team:sre"],
296 "overall_state": "OK"
297 })
298 }
299
300 fn search_page(items: usize, page: i64, page_count: i64, total: i64) -> serde_json::Value {
301 let monitors: Vec<serde_json::Value> = (0..items)
302 .map(|i| {
303 serde_json::json!({
304 "id": (page * 100) + i as i64,
305 "name": format!("Monitor {i}"),
306 "status": "ALERT",
307 "tags": ["env:prod"]
308 })
309 })
310 .collect();
311 serde_json::json!({
312 "monitors": monitors,
313 "counts": {},
314 "metadata": {
315 "page": page,
316 "per_page": items as i64,
317 "page_count": page_count,
318 "total_count": total
319 }
320 })
321 }
322
323 #[tokio::test]
326 async fn list_single_page_returns_parsed_monitors() {
327 let server = wiremock::MockServer::start().await;
328 wiremock::Mock::given(wiremock::matchers::method("GET"))
329 .and(wiremock::matchers::path("/api/v1/monitor"))
330 .and(wiremock::matchers::query_param("name", "cpu"))
331 .and(wiremock::matchers::query_param("page", "0"))
332 .and(wiremock::matchers::query_param("page_size", "5"))
333 .and(wiremock::matchers::header("DD-API-KEY", "api"))
334 .and(wiremock::matchers::header("DD-APPLICATION-KEY", "app"))
335 .respond_with(
336 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!([
337 monitor_json(1, "Disk full"),
338 monitor_json(2, "CPU high")
339 ])),
340 )
341 .expect(1)
342 .mount(&server)
343 .await;
344
345 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
346 let filter = MonitorListFilter {
347 name: Some("cpu".into()),
348 tags: None,
349 monitor_tags: None,
350 };
351 let monitors = MonitorsApi::new(&client).list(&filter, 5).await.unwrap();
352 assert_eq!(monitors.len(), 2);
353 assert_eq!(monitors[0].id, 1);
354 assert_eq!(monitors[1].name, "CPU high");
355 }
356
357 #[tokio::test]
358 async fn list_auto_paginates_until_short_page() {
359 let server = wiremock::MockServer::start().await;
361 for page in 0..2 {
362 let body: Vec<serde_json::Value> = (0..LIST_PAGE_SIZE as i64)
363 .map(|i| monitor_json(page * 100 + i, "m"))
364 .collect();
365 wiremock::Mock::given(wiremock::matchers::method("GET"))
366 .and(wiremock::matchers::path("/api/v1/monitor"))
367 .and(wiremock::matchers::query_param("page", page.to_string()))
368 .and(wiremock::matchers::query_param(
369 "page_size",
370 LIST_PAGE_SIZE.to_string(),
371 ))
372 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(body))
373 .expect(1)
374 .mount(&server)
375 .await;
376 }
377 let last_page: Vec<serde_json::Value> =
378 (0..37_i64).map(|i| monitor_json(200 + i, "m")).collect();
379 wiremock::Mock::given(wiremock::matchers::method("GET"))
380 .and(wiremock::matchers::path("/api/v1/monitor"))
381 .and(wiremock::matchers::query_param("page", "2"))
382 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(last_page))
383 .expect(1)
384 .mount(&server)
385 .await;
386
387 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
388 let monitors = MonitorsApi::new(&client)
389 .list(&MonitorListFilter::default(), 0)
390 .await
391 .unwrap();
392 assert_eq!(monitors.len(), LIST_PAGE_SIZE * 2 + 37);
393 assert_eq!(monitors[0].id, 0);
395 assert_eq!(monitors.last().unwrap().id, 236);
396 }
397
398 #[tokio::test]
399 async fn list_caps_explicit_limit_at_hard_cap() {
400 let server = wiremock::MockServer::start().await;
404 let body: Vec<serde_json::Value> = (0..LIST_PAGE_SIZE as i64)
405 .map(|i| monitor_json(i, "m"))
406 .collect();
407 wiremock::Mock::given(wiremock::matchers::method("GET"))
410 .and(wiremock::matchers::path("/api/v1/monitor"))
411 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(body))
412 .mount(&server)
413 .await;
414
415 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
416 let monitors = MonitorsApi::new(&client)
417 .list(&MonitorListFilter::default(), HARD_CAP + 50)
418 .await
419 .unwrap();
420 assert_eq!(monitors.len(), HARD_CAP);
421 }
422
423 #[tokio::test]
424 async fn list_stops_when_explicit_limit_reached_within_first_page() {
425 let server = wiremock::MockServer::start().await;
426 let body: Vec<serde_json::Value> = (0..3_i64).map(|i| monitor_json(i, "m")).collect();
429 wiremock::Mock::given(wiremock::matchers::method("GET"))
430 .and(wiremock::matchers::path("/api/v1/monitor"))
431 .and(wiremock::matchers::query_param("page", "0"))
432 .and(wiremock::matchers::query_param("page_size", "3"))
433 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(body))
434 .expect(1)
435 .mount(&server)
436 .await;
437
438 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
439 let monitors = MonitorsApi::new(&client)
440 .list(&MonitorListFilter::default(), 3)
441 .await
442 .unwrap();
443 assert_eq!(monitors.len(), 3);
444 }
445
446 #[tokio::test]
447 async fn list_propagates_api_errors() {
448 let server = wiremock::MockServer::start().await;
449 wiremock::Mock::given(wiremock::matchers::method("GET"))
450 .and(wiremock::matchers::path("/api/v1/monitor"))
451 .respond_with(
452 wiremock::ResponseTemplate::new(403).set_body_string(r#"{"errors":["nope"]}"#),
453 )
454 .mount(&server)
455 .await;
456
457 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
458 let err = MonitorsApi::new(&client)
459 .list(&MonitorListFilter::default(), 5)
460 .await
461 .unwrap_err();
462 let msg = err.to_string();
463 assert!(msg.contains("403"));
464 assert!(msg.contains("nope"));
465 }
466
467 #[tokio::test]
468 async fn list_propagates_invalid_base_url_error() {
469 let client = DatadogClient::new("not a url", "api", "app").unwrap();
470 let err = MonitorsApi::new(&client)
471 .list(&MonitorListFilter::default(), 5)
472 .await
473 .unwrap_err();
474 assert!(err.to_string().contains("Invalid Datadog base URL"));
475 }
476
477 #[tokio::test]
478 async fn list_propagates_network_errors() {
479 let client = DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap();
482 let err = MonitorsApi::new(&client)
483 .list(&MonitorListFilter::default(), 5)
484 .await
485 .unwrap_err();
486 assert!(err.to_string().contains("Failed to send"));
487 }
488
489 #[tokio::test]
490 async fn list_errors_on_malformed_response() {
491 let server = wiremock::MockServer::start().await;
492 wiremock::Mock::given(wiremock::matchers::method("GET"))
493 .and(wiremock::matchers::path("/api/v1/monitor"))
494 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
495 .mount(&server)
496 .await;
497
498 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
499 let err = MonitorsApi::new(&client)
500 .list(&MonitorListFilter::default(), 5)
501 .await
502 .unwrap_err();
503 assert!(err.to_string().contains("Failed to parse"));
504 }
505
506 #[tokio::test]
509 async fn get_returns_parsed_monitor() {
510 let server = wiremock::MockServer::start().await;
511 wiremock::Mock::given(wiremock::matchers::method("GET"))
512 .and(wiremock::matchers::path("/api/v1/monitor/12345"))
513 .and(wiremock::matchers::header("DD-API-KEY", "api"))
514 .respond_with(
515 wiremock::ResponseTemplate::new(200).set_body_json(monitor_json(12345, "CPU high")),
516 )
517 .expect(1)
518 .mount(&server)
519 .await;
520
521 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
522 let m = MonitorsApi::new(&client).get(12345).await.unwrap();
523 assert_eq!(m.id, 12345);
524 assert_eq!(m.name, "CPU high");
525 }
526
527 #[tokio::test]
528 async fn get_propagates_404() {
529 let server = wiremock::MockServer::start().await;
530 wiremock::Mock::given(wiremock::matchers::method("GET"))
531 .and(wiremock::matchers::path("/api/v1/monitor/9"))
532 .respond_with(
533 wiremock::ResponseTemplate::new(404).set_body_string(r#"{"errors":["Not found"]}"#),
534 )
535 .mount(&server)
536 .await;
537
538 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
539 let err = MonitorsApi::new(&client).get(9).await.unwrap_err();
540 assert!(err.to_string().contains("404"));
541 assert!(err.to_string().contains("Not found"));
542 }
543
544 #[tokio::test]
545 async fn get_propagates_invalid_base_url_error() {
546 let client = DatadogClient::new("not a url", "api", "app").unwrap();
547 let err = MonitorsApi::new(&client).get(1).await.unwrap_err();
548 assert!(err.to_string().contains("Invalid Datadog base URL"));
549 }
550
551 #[tokio::test]
552 async fn get_propagates_network_errors() {
553 let client = DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap();
554 let err = MonitorsApi::new(&client).get(1).await.unwrap_err();
555 assert!(err.to_string().contains("Failed to send"));
556 }
557
558 #[tokio::test]
559 async fn get_errors_on_malformed_response() {
560 let server = wiremock::MockServer::start().await;
561 wiremock::Mock::given(wiremock::matchers::method("GET"))
562 .and(wiremock::matchers::path("/api/v1/monitor/1"))
563 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
564 .mount(&server)
565 .await;
566
567 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
568 let err = MonitorsApi::new(&client).get(1).await.unwrap_err();
569 assert!(err.to_string().contains("Failed to parse"));
570 }
571
572 #[tokio::test]
575 async fn search_single_page_returns_envelope() {
576 let server = wiremock::MockServer::start().await;
577 wiremock::Mock::given(wiremock::matchers::method("GET"))
578 .and(wiremock::matchers::path("/api/v1/monitor/search"))
579 .and(wiremock::matchers::query_param("query", "status:alert"))
580 .and(wiremock::matchers::query_param("page", "0"))
581 .and(wiremock::matchers::query_param("per_page", "30"))
582 .respond_with(
583 wiremock::ResponseTemplate::new(200).set_body_json(search_page(2, 0, 1, 2)),
584 )
585 .expect(1)
586 .mount(&server)
587 .await;
588
589 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
590 let result = MonitorsApi::new(&client)
591 .search("status:alert", 30)
592 .await
593 .unwrap();
594 assert_eq!(result.monitors.len(), 2);
595 assert_eq!(result.metadata.unwrap().total_count, Some(2));
596 }
597
598 #[tokio::test]
599 async fn search_auto_paginates_with_unbounded_limit() {
600 let server = wiremock::MockServer::start().await;
603 for page in 0..2_i64 {
604 wiremock::Mock::given(wiremock::matchers::method("GET"))
605 .and(wiremock::matchers::path("/api/v1/monitor/search"))
606 .and(wiremock::matchers::query_param("page", page.to_string()))
607 .respond_with(
608 wiremock::ResponseTemplate::new(200).set_body_json(search_page(
609 SEARCH_PAGE_SIZE,
610 page,
611 2,
612 60,
613 )),
614 )
615 .expect(1)
616 .mount(&server)
617 .await;
618 }
619
620 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
621 let result = MonitorsApi::new(&client).search("q", 0).await.unwrap();
622 assert_eq!(result.monitors.len(), SEARCH_PAGE_SIZE * 2);
623 }
624
625 #[tokio::test]
626 async fn search_stops_on_short_page_when_metadata_missing() {
627 let server = wiremock::MockServer::start().await;
629 wiremock::Mock::given(wiremock::matchers::method("GET"))
630 .and(wiremock::matchers::path("/api/v1/monitor/search"))
631 .and(wiremock::matchers::query_param("page", "0"))
632 .respond_with(
633 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
634 "monitors": [
635 { "id": 1_i64, "name": "Only" }
636 ]
637 })),
638 )
639 .expect(1)
640 .mount(&server)
641 .await;
642
643 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
644 let result = MonitorsApi::new(&client).search("q", 0).await.unwrap();
645 assert_eq!(result.monitors.len(), 1);
646 assert!(result.metadata.is_none());
647 }
648
649 #[tokio::test]
650 async fn search_caps_at_explicit_limit_within_full_page() {
651 let server = wiremock::MockServer::start().await;
652 wiremock::Mock::given(wiremock::matchers::method("GET"))
654 .and(wiremock::matchers::path("/api/v1/monitor/search"))
655 .and(wiremock::matchers::query_param("page", "0"))
656 .and(wiremock::matchers::query_param("per_page", "5"))
657 .respond_with(
658 wiremock::ResponseTemplate::new(200).set_body_json(search_page(5, 0, 10, 100)),
659 )
660 .expect(1)
661 .mount(&server)
662 .await;
663
664 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
665 let result = MonitorsApi::new(&client).search("q", 5).await.unwrap();
666 assert_eq!(result.monitors.len(), 5);
667 }
668
669 #[tokio::test]
670 async fn search_propagates_api_errors() {
671 let server = wiremock::MockServer::start().await;
672 wiremock::Mock::given(wiremock::matchers::method("GET"))
673 .and(wiremock::matchers::path("/api/v1/monitor/search"))
674 .respond_with(
675 wiremock::ResponseTemplate::new(400).set_body_string(r#"{"errors":["bad query"]}"#),
676 )
677 .mount(&server)
678 .await;
679
680 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
681 let err = MonitorsApi::new(&client)
682 .search("???", 5)
683 .await
684 .unwrap_err();
685 let msg = err.to_string();
686 assert!(msg.contains("400"));
687 assert!(msg.contains("bad query"));
688 }
689
690 #[tokio::test]
691 async fn search_propagates_invalid_base_url_error() {
692 let client = DatadogClient::new("not a url", "api", "app").unwrap();
693 let err = MonitorsApi::new(&client).search("q", 5).await.unwrap_err();
694 assert!(err.to_string().contains("Invalid Datadog base URL"));
695 }
696
697 #[tokio::test]
698 async fn search_propagates_network_errors() {
699 let client = DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap();
700 let err = MonitorsApi::new(&client).search("q", 5).await.unwrap_err();
701 assert!(err.to_string().contains("Failed to send"));
702 }
703
704 #[tokio::test]
705 async fn search_errors_on_malformed_response() {
706 let server = wiremock::MockServer::start().await;
707 wiremock::Mock::given(wiremock::matchers::method("GET"))
708 .and(wiremock::matchers::path("/api/v1/monitor/search"))
709 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
710 .mount(&server)
711 .await;
712
713 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
714 let err = MonitorsApi::new(&client).search("q", 5).await.unwrap_err();
715 assert!(err.to_string().contains("Failed to parse"));
716 }
717}