Skip to main content

max / goingson

25.0 KB · 759 lines History Blame Raw
1 //! Input validation for domain objects.
2 //!
3 //! This module provides the [`Validate`] trait and implementations for
4 //! validating DTOs before persistence operations.
5
6 use crate::constants::{
7 MAX_CONTACT_DISPLAY_NAME_LENGTH, MAX_EVENT_TITLE_LENGTH, MAX_PROJECT_NAME_LENGTH,
8 MAX_SCHEDULED_DURATION_MINUTES, MAX_TASK_DESCRIPTION_LENGTH,
9 };
10 use crate::error::CoreError;
11 use crate::contact::{NewContact, UpdateContact};
12 use crate::models::{NewEvent, NewProject, NewTask, UpdateEvent, UpdateProject, UpdateTask};
13
14 /// Tag rules for GoingsOn: shallow hierarchy, no required semantic prefix.
15 const GO_TAG_CONFIG: tagtree::TagConfig = tagtree::TagConfig {
16 max_depth: 3,
17 max_length: 60,
18 semantic_depth: 0,
19 };
20
21 /// Validate a single tag against the GoingsOn config.
22 fn validate_tag(tag: &str) -> Result<(), CoreError> {
23 tagtree::validate_with(tag, &GO_TAG_CONFIG)
24 .map_err(|e| CoreError::validation("tags", e.to_string()))
25 }
26
27 /// Validates that a required string field is non-empty and within a max length.
28 fn validate_required_string(field: &'static str, value: &str, max_len: usize) -> Result<(), CoreError> {
29 if value.trim().is_empty() {
30 return Err(CoreError::validation(field, "cannot be empty"));
31 }
32 // Fast path: byte length can't exceed char count, so if bytes fit, chars do too
33 if value.len() > max_len && value.chars().count() > max_len {
34 return Err(CoreError::validation(
35 field,
36 format!("must be {} characters or less", max_len),
37 ));
38 }
39 Ok(())
40 }
41
42 /// Validates an optional duration is positive and within a max (in minutes).
43 fn validate_optional_duration(field: &'static str, value: Option<i32>, max: i32) -> Result<(), CoreError> {
44 if let Some(duration) = value {
45 if duration <= 0 {
46 return Err(CoreError::validation(field, "must be positive"));
47 }
48 if duration > max {
49 return Err(CoreError::validation(field, format!("cannot exceed {} minutes", max)));
50 }
51 }
52 Ok(())
53 }
54
55 /// Validates a slice of tags against the GoingsOn tag config.
56 fn validate_tags(tags: &[String]) -> Result<(), CoreError> {
57 for tag in tags {
58 validate_tag(tag)?;
59 }
60 Ok(())
61 }
62
63 /// A trait for types that can validate their own data before persistence.
64 ///
65 /// Call `.validate()` on DTOs (`NewTask`, `NewProject`, `NewEvent`, `UpdateTask`,
66 /// `UpdateProject`, `UpdateEvent`) before passing them to a repository. This centralizes business rules
67 /// (length limits, required fields, range checks) in the core crate so
68 /// they're enforced regardless of whether the caller is a Tauri command,
69 /// a plugin import, or a test.
70 ///
71 /// # Example
72 ///
73 /// ```rust,ignore
74 /// use goingson_core::{NewTask, Validate};
75 ///
76 /// let task = NewTask { description: "".to_string(), /* ... */ };
77 /// if let Err(e) = task.validate() {
78 /// println!("Invalid task: {}", e);
79 /// }
80 /// ```
81 pub trait Validate {
82 /// Validates the object, returning an error if invalid.
83 fn validate(&self) -> Result<(), CoreError>;
84 }
85
86 // Project validation: non-empty name within MAX_PROJECT_NAME_LENGTH.
87 impl Validate for NewProject {
88 fn validate(&self) -> Result<(), CoreError> {
89 validate_required_string("name", &self.name, MAX_PROJECT_NAME_LENGTH)
90 }
91 }
92
93 // Same rules as NewProject — updates must also pass validation.
94 impl Validate for UpdateProject {
95 fn validate(&self) -> Result<(), CoreError> {
96 validate_required_string("name", &self.name, MAX_PROJECT_NAME_LENGTH)
97 }
98 }
99
100 // Task validation: non-empty description, valid duration (positive, ≤24h),
101 // tags validated via tagtree (lowercase, dot-separated, max 3 levels, 60 chars).
102 impl Validate for NewTask {
103 fn validate(&self) -> Result<(), CoreError> {
104 validate_required_string("description", &self.description, MAX_TASK_DESCRIPTION_LENGTH)?;
105 validate_optional_duration("scheduled_duration", self.scheduled_duration, MAX_SCHEDULED_DURATION_MINUTES)?;
106 validate_optional_duration("estimated_minutes", self.estimated_minutes, MAX_SCHEDULED_DURATION_MINUTES)?;
107 validate_tags(&self.tags)
108 }
109 }
110
111 // Same rules as NewTask — updates must also pass validation.
112 impl Validate for UpdateTask {
113 fn validate(&self) -> Result<(), CoreError> {
114 validate_required_string("description", &self.description, MAX_TASK_DESCRIPTION_LENGTH)?;
115 validate_optional_duration("scheduled_duration", self.scheduled_duration, MAX_SCHEDULED_DURATION_MINUTES)?;
116 validate_optional_duration("estimated_minutes", self.estimated_minutes, MAX_SCHEDULED_DURATION_MINUTES)?;
117 validate_tags(&self.tags)
118 }
119 }
120
121 // Event validation: non-empty title, end_time must be after start_time.
122 impl Validate for NewEvent {
123 fn validate(&self) -> Result<(), CoreError> {
124 validate_required_string("title", &self.title, MAX_EVENT_TITLE_LENGTH)?;
125 if let Some(end) = self.end_time
126 && end <= self.start_time {
127 return Err(CoreError::validation("end_time", "must be after start_time"));
128 }
129 Ok(())
130 }
131 }
132
133 // Same rules as NewEvent — updates must also pass validation.
134 impl Validate for UpdateEvent {
135 fn validate(&self) -> Result<(), CoreError> {
136 validate_required_string("title", &self.title, MAX_EVENT_TITLE_LENGTH)?;
137 if let Some(end) = self.end_time
138 && end <= self.start_time {
139 return Err(CoreError::validation("end_time", "must be after start_time"));
140 }
141 Ok(())
142 }
143 }
144
145 // Contact validation: non-empty display_name, valid tags.
146 fn validate_contact_fields(display_name: &str, tags: &[String]) -> Result<(), CoreError> {
147 validate_required_string("display_name", display_name, MAX_CONTACT_DISPLAY_NAME_LENGTH)?;
148 validate_tags(tags)
149 }
150
151 impl Validate for NewContact {
152 fn validate(&self) -> Result<(), CoreError> {
153 validate_contact_fields(&self.display_name, &self.tags)
154 }
155 }
156
157 impl Validate for UpdateContact {
158 fn validate(&self) -> Result<(), CoreError> {
159 validate_contact_fields(&self.display_name, &self.tags)
160 }
161 }
162
163 #[cfg(test)]
164 mod tests {
165 use super::*;
166 use crate::models::{BlockType, Priority, ProjectStatus, ProjectType, Recurrence, UpdateEvent, UpdateProject};
167 use chrono::{Duration, Utc};
168
169 #[test]
170 fn test_new_project_valid() {
171 let project = NewProject {
172 name: "Test Project".to_string(),
173 description: "A test".to_string(),
174 project_type: ProjectType::SideProject,
175 status: ProjectStatus::Active,
176 };
177 assert!(project.validate().is_ok());
178 }
179
180 #[test]
181 fn test_new_project_empty_name() {
182 let project = NewProject {
183 name: " ".to_string(),
184 description: "A test".to_string(),
185 project_type: ProjectType::SideProject,
186 status: ProjectStatus::Active,
187 };
188 let err = project.validate().unwrap_err();
189 assert!(matches!(err, CoreError::Validation { field: "name", .. }));
190 }
191
192 #[test]
193 fn test_new_task_valid() {
194 let task = NewTask {
195 project_id: None,
196 description: "Do something".to_string(),
197 priority: Priority::Medium,
198 due: None,
199 tags: vec!["work".to_string()],
200 recurrence: Recurrence::None,
201 recurrence_rule: None,
202 urgency: 5.0,
203 source_email_id: None,
204 scheduled_start: None,
205 scheduled_duration: Some(60),
206 contact_id: None,
207 milestone_id: None,
208 estimated_minutes: None,
209 recurrence_parent_id: None,
210 };
211 assert!(task.validate().is_ok());
212 }
213
214 #[test]
215 fn test_new_task_empty_description() {
216 let task = NewTask {
217 project_id: None,
218 description: "".to_string(),
219 priority: Priority::Medium,
220 due: None,
221 tags: vec![],
222 recurrence: Recurrence::None,
223 recurrence_rule: None,
224 urgency: 5.0,
225 source_email_id: None,
226 scheduled_start: None,
227 scheduled_duration: None,
228 contact_id: None,
229 milestone_id: None,
230 estimated_minutes: None,
231 recurrence_parent_id: None,
232 };
233 let err = task.validate().unwrap_err();
234 assert!(matches!(err, CoreError::Validation { field: "description", .. }));
235 }
236
237 #[test]
238 fn test_new_task_negative_duration() {
239 let task = NewTask {
240 project_id: None,
241 description: "Do something".to_string(),
242 priority: Priority::Medium,
243 due: None,
244 tags: vec![],
245 recurrence: Recurrence::None,
246 recurrence_rule: None,
247 urgency: 5.0,
248 source_email_id: None,
249 scheduled_start: None,
250 scheduled_duration: Some(-30),
251 contact_id: None,
252 milestone_id: None,
253 estimated_minutes: None,
254 recurrence_parent_id: None,
255 };
256 let err = task.validate().unwrap_err();
257 assert!(matches!(err, CoreError::Validation { field: "scheduled_duration", .. }));
258 }
259
260 #[test]
261 fn test_new_event_valid() {
262 let now = Utc::now();
263 let event = NewEvent {
264 user_id: None,
265 project_id: None,
266 title: "Meeting".to_string(),
267 description: "Team standup".to_string(),
268 start_time: now,
269 end_time: Some(now + Duration::hours(1)),
270 location: None,
271 linked_task_id: None,
272 recurrence: Recurrence::None,
273 recurrence_rule: None,
274 contact_id: None,
275 block_type: None,
276 reminder_offsets_seconds: Vec::new(),
277 };
278 assert!(event.validate().is_ok());
279 }
280
281 #[test]
282 fn test_new_event_end_before_start() {
283 let now = Utc::now();
284 let event = NewEvent {
285 user_id: None,
286 project_id: None,
287 title: "Meeting".to_string(),
288 description: "".to_string(),
289 start_time: now,
290 end_time: Some(now - Duration::hours(1)),
291 location: None,
292 linked_task_id: None,
293 recurrence: Recurrence::None,
294 recurrence_rule: None,
295 contact_id: None,
296 block_type: None,
297 reminder_offsets_seconds: Vec::new(),
298 };
299 let err = event.validate().unwrap_err();
300 assert!(matches!(err, CoreError::Validation { field: "end_time", .. }));
301 }
302
303 #[test]
304 fn test_new_event_empty_title() {
305 let now = Utc::now();
306 let event = NewEvent {
307 user_id: None,
308 project_id: None,
309 title: "".to_string(),
310 description: "".to_string(),
311 start_time: now,
312 end_time: None,
313 location: None,
314 linked_task_id: None,
315 recurrence: Recurrence::None,
316 recurrence_rule: None,
317 contact_id: None,
318 block_type: None,
319 reminder_offsets_seconds: Vec::new(),
320 };
321 let err = event.validate().unwrap_err();
322 assert!(matches!(err, CoreError::Validation { field: "title", .. }));
323 }
324
325 #[test]
326 fn test_new_task_empty_tag() {
327 let task = NewTask {
328 project_id: None,
329 description: "Task with empty tag".to_string(),
330 priority: Priority::Medium,
331 due: None,
332 tags: vec!["valid".to_string(), "".to_string()],
333 recurrence: Recurrence::None,
334 recurrence_rule: None,
335 urgency: 5.0,
336 source_email_id: None,
337 scheduled_start: None,
338 scheduled_duration: None,
339 contact_id: None,
340 milestone_id: None,
341 estimated_minutes: None,
342 recurrence_parent_id: None,
343 };
344 let err = task.validate().unwrap_err();
345 assert!(matches!(err, CoreError::Validation { field: "tags", .. }));
346 }
347
348 #[test]
349 fn test_new_task_tag_too_long() {
350 let task = NewTask {
351 project_id: None,
352 description: "Task with long tag".to_string(),
353 priority: Priority::Medium,
354 due: None,
355 tags: vec!["a".repeat(61)], // exceeds tagtree max_length of 60
356 recurrence: Recurrence::None,
357 recurrence_rule: None,
358 urgency: 5.0,
359 source_email_id: None,
360 scheduled_start: None,
361 scheduled_duration: None,
362 contact_id: None,
363 milestone_id: None,
364 estimated_minutes: None,
365 recurrence_parent_id: None,
366 };
367 let err = task.validate().unwrap_err();
368 assert!(matches!(err, CoreError::Validation { field: "tags", .. }));
369 }
370
371 #[test]
372 fn test_new_task_duration_too_long() {
373 let task = NewTask {
374 project_id: None,
375 description: "Task with excessive duration".to_string(),
376 priority: Priority::Medium,
377 due: None,
378 tags: vec![],
379 recurrence: Recurrence::None,
380 recurrence_rule: None,
381 urgency: 5.0,
382 source_email_id: None,
383 scheduled_start: None,
384 scheduled_duration: Some(MAX_SCHEDULED_DURATION_MINUTES + 1),
385 contact_id: None,
386 milestone_id: None,
387 estimated_minutes: None,
388 recurrence_parent_id: None,
389 };
390 let err = task.validate().unwrap_err();
391 assert!(matches!(err, CoreError::Validation { field: "scheduled_duration", .. }));
392 }
393
394 #[test]
395 fn test_new_project_name_too_long() {
396 let project = NewProject {
397 name: "a".repeat(MAX_PROJECT_NAME_LENGTH + 1),
398 description: "".to_string(),
399 project_type: ProjectType::SideProject,
400 status: ProjectStatus::Active,
401 };
402 let err = project.validate().unwrap_err();
403 assert!(matches!(err, CoreError::Validation { field: "name", .. }));
404 }
405
406 #[test]
407 fn test_new_task_description_too_long() {
408 let task = NewTask {
409 project_id: None,
410 description: "a".repeat(MAX_TASK_DESCRIPTION_LENGTH + 1),
411 priority: Priority::Medium,
412 due: None,
413 tags: vec![],
414 recurrence: Recurrence::None,
415 recurrence_rule: None,
416 urgency: 5.0,
417 source_email_id: None,
418 scheduled_start: None,
419 scheduled_duration: None,
420 contact_id: None,
421 milestone_id: None,
422 estimated_minutes: None,
423 recurrence_parent_id: None,
424 };
425 let err = task.validate().unwrap_err();
426 assert!(matches!(err, CoreError::Validation { field: "description", .. }));
427 }
428
429 #[test]
430 fn test_new_event_title_too_long() {
431 let now = Utc::now();
432 let event = NewEvent {
433 user_id: None,
434 project_id: None,
435 title: "a".repeat(MAX_EVENT_TITLE_LENGTH + 1),
436 description: "".to_string(),
437 start_time: now,
438 end_time: None,
439 location: None,
440 linked_task_id: None,
441 recurrence: Recurrence::None,
442 recurrence_rule: None,
443 contact_id: None,
444 block_type: None,
445 reminder_offsets_seconds: Vec::new(),
446 };
447 let err = event.validate().unwrap_err();
448 assert!(matches!(err, CoreError::Validation { field: "title", .. }));
449 }
450
451 #[test]
452 fn test_new_event_end_equals_start() {
453 let now = Utc::now();
454 let event = NewEvent {
455 user_id: None,
456 project_id: None,
457 title: "Meeting".to_string(),
458 description: "".to_string(),
459 start_time: now,
460 end_time: Some(now), // Same as start
461 location: None,
462 linked_task_id: None,
463 recurrence: Recurrence::None,
464 recurrence_rule: None,
465 contact_id: None,
466 block_type: None,
467 reminder_offsets_seconds: Vec::new(),
468 };
469 let err = event.validate().unwrap_err();
470 assert!(matches!(err, CoreError::Validation { field: "end_time", .. }));
471 }
472
473 #[test]
474 fn test_update_task_valid() {
475 let task = UpdateTask {
476 project_id: None,
477 contact_id: None,
478 milestone_id: None,
479 description: "Updated task".to_string(),
480 status: crate::models::TaskStatus::Started,
481 priority: Priority::High,
482 due: Some(Utc::now() + Duration::days(1)),
483 tags: vec!["updated".to_string()],
484 recurrence: Recurrence::Weekly,
485 recurrence_rule: None,
486 urgency: 7.0,
487 scheduled_start: None,
488 scheduled_duration: None,
489 estimated_minutes: None,
490 };
491 assert!(task.validate().is_ok());
492 }
493
494 #[test]
495 fn test_whitespace_only_fields_rejected() {
496 // Project with whitespace-only name
497 let project = NewProject {
498 name: " \t\n".to_string(),
499 description: "".to_string(),
500 project_type: ProjectType::SideProject,
501 status: ProjectStatus::Active,
502 };
503 assert!(project.validate().is_err());
504
505 // Task with whitespace-only description
506 let task = NewTask {
507 project_id: None,
508 description: "\n\t ".to_string(),
509 priority: Priority::Medium,
510 due: None,
511 tags: vec![],
512 recurrence: Recurrence::None,
513 recurrence_rule: None,
514 urgency: 5.0,
515 source_email_id: None,
516 scheduled_start: None,
517 scheduled_duration: None,
518 contact_id: None,
519 milestone_id: None,
520 estimated_minutes: None,
521 recurrence_parent_id: None,
522 };
523 assert!(task.validate().is_err());
524
525 // Event with whitespace-only title
526 let event = NewEvent {
527 user_id: None,
528 project_id: None,
529 title: " \t".to_string(),
530 description: "".to_string(),
531 start_time: Utc::now(),
532 end_time: None,
533 location: None,
534 linked_task_id: None,
535 recurrence: Recurrence::None,
536 recurrence_rule: None,
537 contact_id: None,
538 block_type: None,
539 reminder_offsets_seconds: Vec::new(),
540 };
541 assert!(event.validate().is_err());
542 }
543
544 // ---- UpdateProject validation tests ----
545
546 #[test]
547 fn test_update_project_valid() {
548 let project = UpdateProject {
549 name: "Renamed Project".to_string(),
550 description: "Updated description".to_string(),
551 project_type: ProjectType::Job,
552 status: ProjectStatus::Active,
553 };
554 assert!(project.validate().is_ok());
555 }
556
557 #[test]
558 fn test_update_project_empty_name() {
559 let project = UpdateProject {
560 name: " ".to_string(),
561 description: "".to_string(),
562 project_type: ProjectType::SideProject,
563 status: ProjectStatus::Active,
564 };
565 let err = project.validate().unwrap_err();
566 assert!(matches!(err, CoreError::Validation { field: "name", .. }));
567 }
568
569 #[test]
570 fn test_update_project_name_too_long() {
571 let project = UpdateProject {
572 name: "a".repeat(MAX_PROJECT_NAME_LENGTH + 1),
573 description: "".to_string(),
574 project_type: ProjectType::SideProject,
575 status: ProjectStatus::Active,
576 };
577 let err = project.validate().unwrap_err();
578 assert!(matches!(err, CoreError::Validation { field: "name", .. }));
579 }
580
581 // ---- UpdateEvent validation tests ----
582
583 #[test]
584 fn test_update_event_valid() {
585 let now = Utc::now();
586 let event = UpdateEvent {
587 project_id: None,
588 contact_id: None,
589 title: "Updated Meeting".to_string(),
590 description: "New notes".to_string(),
591 start_time: now,
592 end_time: Some(now + Duration::hours(2)),
593 location: Some("Room B".to_string()),
594 linked_task_id: None,
595 recurrence: Recurrence::None,
596 recurrence_rule: None,
597 block_type: Some(BlockType::Focus),
598 reminder_offsets_seconds: Vec::new(),
599 };
600 assert!(event.validate().is_ok());
601 }
602
603 #[test]
604 fn test_update_event_empty_title() {
605 let now = Utc::now();
606 let event = UpdateEvent {
607 project_id: None,
608 contact_id: None,
609 title: "".to_string(),
610 description: "".to_string(),
611 start_time: now,
612 end_time: None,
613 location: None,
614 linked_task_id: None,
615 recurrence: Recurrence::None,
616 recurrence_rule: None,
617 block_type: None,
618 reminder_offsets_seconds: Vec::new(),
619 };
620 let err = event.validate().unwrap_err();
621 assert!(matches!(err, CoreError::Validation { field: "title", .. }));
622 }
623
624 #[test]
625 fn test_update_event_title_too_long() {
626 let now = Utc::now();
627 let event = UpdateEvent {
628 project_id: None,
629 contact_id: None,
630 title: "a".repeat(MAX_EVENT_TITLE_LENGTH + 1),
631 description: "".to_string(),
632 start_time: now,
633 end_time: None,
634 location: None,
635 linked_task_id: None,
636 recurrence: Recurrence::None,
637 recurrence_rule: None,
638 block_type: None,
639 reminder_offsets_seconds: Vec::new(),
640 };
641 let err = event.validate().unwrap_err();
642 assert!(matches!(err, CoreError::Validation { field: "title", .. }));
643 }
644
645 #[test]
646 fn test_update_event_end_before_start() {
647 let now = Utc::now();
648 let event = UpdateEvent {
649 project_id: None,
650 contact_id: None,
651 title: "Meeting".to_string(),
652 description: "".to_string(),
653 start_time: now,
654 end_time: Some(now - Duration::hours(1)),
655 location: None,
656 linked_task_id: None,
657 recurrence: Recurrence::None,
658 recurrence_rule: None,
659 block_type: None,
660 reminder_offsets_seconds: Vec::new(),
661 };
662 let err = event.validate().unwrap_err();
663 assert!(matches!(err, CoreError::Validation { field: "end_time", .. }));
664 }
665
666 #[test]
667 fn test_update_event_end_equals_start() {
668 let now = Utc::now();
669 let event = UpdateEvent {
670 project_id: None,
671 contact_id: None,
672 title: "Meeting".to_string(),
673 description: "".to_string(),
674 start_time: now,
675 end_time: Some(now),
676 location: None,
677 linked_task_id: None,
678 recurrence: Recurrence::None,
679 recurrence_rule: None,
680 block_type: None,
681 reminder_offsets_seconds: Vec::new(),
682 };
683 let err = event.validate().unwrap_err();
684 assert!(matches!(err, CoreError::Validation { field: "end_time", .. }));
685 }
686
687 // ---- Contact validation tests ----
688
689 fn make_new_contact(name: &str) -> NewContact {
690 NewContact {
691 display_name: name.to_string(),
692 nickname: None,
693 company: None,
694 title: None,
695 notes: String::new(),
696 tags: vec![],
697 birthday: None,
698 timezone: None,
699 is_implicit: false,
700 }
701 }
702
703 #[test]
704 fn test_new_contact_valid() {
705 assert!(make_new_contact("Alice").validate().is_ok());
706 }
707
708 #[test]
709 fn test_new_contact_empty_name() {
710 let err = make_new_contact(" ").validate().unwrap_err();
711 assert!(matches!(err, CoreError::Validation { field: "display_name", .. }));
712 }
713
714 #[test]
715 fn test_new_contact_name_too_long() {
716 let err = make_new_contact(&"a".repeat(256)).validate().unwrap_err();
717 assert!(matches!(err, CoreError::Validation { field: "display_name", .. }));
718 }
719
720 #[test]
721 fn test_new_contact_empty_tag() {
722 let mut c = make_new_contact("Bob");
723 c.tags = vec!["valid".to_string(), "".to_string()];
724 let err = c.validate().unwrap_err();
725 assert!(matches!(err, CoreError::Validation { field: "tags", .. }));
726 }
727
728 #[test]
729 fn test_update_contact_valid() {
730 let update = UpdateContact {
731 display_name: "Updated".to_string(),
732 nickname: None,
733 company: None,
734 title: None,
735 notes: String::new(),
736 tags: vec!["ok".to_string()],
737 birthday: None,
738 timezone: None,
739 };
740 assert!(update.validate().is_ok());
741 }
742
743 #[test]
744 fn test_update_contact_empty_name() {
745 let update = UpdateContact {
746 display_name: "".to_string(),
747 nickname: None,
748 company: None,
749 title: None,
750 notes: String::new(),
751 tags: vec![],
752 birthday: None,
753 timezone: None,
754 };
755 let err = update.validate().unwrap_err();
756 assert!(matches!(err, CoreError::Validation { field: "display_name", .. }));
757 }
758 }
759