1#![allow(clippy::missing_errors_doc)]
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5fn deserialize_null_as_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
6where
7 T: Deserialize<'de>,
8 D: Deserializer<'de>,
9{
10 Ok(Option::deserialize(deserializer)?.unwrap_or_default())
11}
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct Query {
15 pub id: u64,
16 pub name: String,
17 pub description: Option<String>,
18 #[serde(rename = "query")]
19 pub sql: String,
20 pub data_source_id: u64,
21 #[serde(default)]
22 pub user: Option<QueryUser>,
23 pub schedule: Option<Schedule>,
24 pub options: QueryOptions,
25 #[serde(default)]
26 pub visualizations: Vec<Visualization>,
27 pub tags: Option<Vec<String>>,
28 pub is_archived: bool,
29 pub is_draft: bool,
30 pub updated_at: String,
31 pub created_at: String,
32}
33
34#[derive(Debug, Serialize, Clone)]
35pub struct CreateQuery {
36 pub name: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub description: Option<String>,
39 #[serde(rename = "query")]
40 pub sql: String,
41 pub data_source_id: u64,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub schedule: Option<Schedule>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub options: Option<QueryOptions>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub tags: Option<Vec<String>>,
48 pub is_archived: bool,
49 pub is_draft: bool,
50}
51
52#[derive(Debug, Serialize, Deserialize, Clone)]
53pub struct QueryUser {
54 pub id: u64,
55 pub name: String,
56 pub email: String,
57}
58
59#[derive(Debug, Serialize, Deserialize, Clone)]
60pub struct QueryOptions {
61 #[serde(default)]
62 pub parameters: Vec<Parameter>,
63}
64
65#[derive(Debug, Serialize, Deserialize, Clone)]
66pub struct Parameter {
67 pub name: String,
68 pub title: String,
69 #[serde(rename = "type")]
70 pub param_type: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub value: Option<serde_json::Value>,
73 #[serde(rename = "enumOptions", skip_serializing_if = "Option::is_none")]
74 pub enum_options: Option<String>,
75 #[serde(rename = "queryId", skip_serializing_if = "Option::is_none")]
76 pub query_id: Option<u64>,
77 #[serde(rename = "multiValuesOptions", skip_serializing_if = "Option::is_none")]
78 pub multi_values_options: Option<MultiValuesOptions>,
79}
80
81#[derive(Debug, Serialize, Deserialize, Clone)]
82pub struct MultiValuesOptions {
83 #[serde(rename = "prefix", skip_serializing_if = "Option::is_none")]
84 pub prefix: Option<String>,
85 #[serde(rename = "suffix", skip_serializing_if = "Option::is_none")]
86 pub suffix: Option<String>,
87 #[serde(rename = "separator", skip_serializing_if = "Option::is_none")]
88 pub separator: Option<String>,
89 #[serde(rename = "quoteCharacter", skip_serializing_if = "Option::is_none")]
90 pub quote_character: Option<String>,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone)]
94pub struct Schedule {
95 pub interval: Option<u64>,
96 pub time: Option<String>,
97 pub day_of_week: Option<String>,
98 pub until: Option<String>,
99}
100
101#[derive(Debug, Serialize, Deserialize, Clone)]
102pub struct Visualization {
103 pub id: u64,
104 pub name: String,
105 #[serde(rename = "type")]
106 pub viz_type: String,
107 pub options: serde_json::Value,
108 pub description: Option<String>,
109}
110
111#[derive(Debug, Serialize, Clone)]
112pub struct CreateVisualization {
113 pub query_id: u64,
114 pub name: String,
115 #[serde(rename = "type")]
116 pub viz_type: String,
117 pub options: serde_json::Value,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub description: Option<String>,
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123pub struct QueriesResponse {
124 pub results: Vec<Query>,
125 pub count: u64,
126 pub page: u64,
127 pub page_size: u64,
128}
129
130#[derive(Debug, Serialize, Deserialize)]
131pub struct QueryMetadata {
132 pub id: u64,
133 pub name: String,
134 pub description: Option<String>,
135 pub data_source_id: u64,
136 #[serde(default)]
137 pub user_id: Option<u64>,
138 pub schedule: Option<Schedule>,
139 pub options: QueryOptions,
140 pub visualizations: Vec<Visualization>,
141 pub tags: Option<Vec<String>>,
142}
143
144#[derive(Debug, Serialize, Deserialize, Clone)]
145pub struct User {
146 pub id: u64,
147 pub name: String,
148 pub email: String,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub profile_image_url: Option<String>,
151}
152
153#[derive(Debug, Serialize, Deserialize, Clone)]
154pub struct DataSource {
155 pub id: u64,
156 pub name: String,
157 #[serde(rename = "type")]
158 pub ds_type: String,
159 pub syntax: Option<String>,
160 pub description: Option<String>,
161 pub paused: u8,
162 pub pause_reason: Option<String>,
163 pub view_only: bool,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub queue_name: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub scheduled_queue_name: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub groups: Option<serde_json::Value>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub options: Option<serde_json::Value>,
172}
173
174#[derive(Debug, Serialize, Deserialize)]
175pub struct DataSourceSchema {
176 pub schema: Vec<SchemaTable>,
177}
178
179#[derive(Debug, Serialize, Deserialize)]
180pub struct SchemaTable {
181 pub name: String,
182 pub columns: Vec<SchemaColumn>,
183}
184
185#[derive(Debug, Serialize, Deserialize)]
186pub struct SchemaColumn {
187 pub name: String,
188 #[serde(rename = "type")]
189 pub column_type: String,
190}
191
192#[derive(Debug, Serialize, Deserialize)]
193pub struct RefreshRequest {
194 pub max_age: u64,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub parameters: Option<std::collections::HashMap<String, serde_json::Value>>,
197}
198
199#[derive(Debug, Serialize, Deserialize)]
200pub struct JobResponse {
201 pub job: Job,
202}
203
204#[derive(Debug, Serialize, Deserialize)]
205pub struct Job {
206 pub id: String,
207 pub status: u8,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub query_result_id: Option<u64>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub error: Option<String>,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
215pub struct QueryResultResponse {
216 pub query_result: QueryResult,
217}
218
219#[derive(Debug, Serialize, Deserialize)]
220pub struct QueryResult {
221 pub id: u64,
222 pub data: QueryResultData,
223 pub runtime: f64,
224 pub retrieved_at: String,
225}
226
227#[derive(Debug, Serialize, Deserialize)]
228pub struct QueryResultData {
229 pub columns: Vec<Column>,
230 pub rows: Vec<serde_json::Value>,
231}
232
233#[derive(Debug, Serialize, Deserialize)]
234pub struct Column {
235 pub name: String,
236 #[serde(rename = "type")]
237 pub type_name: String,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub friendly_name: Option<String>,
240}
241
242#[derive(Debug, Clone, Copy)]
243pub enum JobStatus {
244 Pending = 1,
245 Started = 2,
246 Success = 3,
247 Failure = 4,
248 Cancelled = 5,
249}
250
251impl JobStatus {
252 pub fn from_u8(status: u8) -> anyhow::Result<Self> {
253 match status {
254 1 => Ok(Self::Pending),
255 2 => Ok(Self::Started),
256 3 => Ok(Self::Success),
257 4 => Ok(Self::Failure),
258 5 => Ok(Self::Cancelled),
259 _ => Err(anyhow::anyhow!("Invalid job status: {status}")),
260 }
261 }
262}
263
264#[derive(Debug, Serialize, Deserialize)]
265pub struct Dashboard {
266 pub id: u64,
267 pub name: String,
268 pub slug: String,
269 pub user_id: u64,
270 pub is_archived: bool,
271 pub is_draft: bool,
272 #[serde(rename = "dashboard_filters_enabled")]
273 pub filters_enabled: bool,
274 pub tags: Vec<String>,
275 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
276 pub widgets: Vec<Widget>,
277}
278
279#[derive(Debug, Serialize)]
280pub struct CreateDashboard {
281 pub name: String,
282}
283
284#[derive(Debug, Serialize, Deserialize)]
285pub struct Widget {
286 pub id: u64,
287 pub dashboard_id: u64,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub visualization_id: Option<u64>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub visualization: Option<WidgetVisualization>,
292 #[serde(default)]
293 pub text: String,
294 pub options: WidgetOptions,
295}
296
297#[derive(Debug, Serialize, Deserialize)]
298pub struct WidgetVisualization {
299 pub id: u64,
300 pub name: String,
301 pub query: VisualizationQuery,
302}
303
304#[derive(Debug, Serialize, Deserialize)]
305pub struct VisualizationQuery {
306 pub id: u64,
307 pub name: String,
308}
309
310#[derive(Debug, Serialize, Deserialize, Clone)]
311pub struct WidgetOptions {
312 pub position: WidgetPosition,
313 #[serde(default, skip_serializing_if = "Option::is_none", rename = "parameterMappings")]
314 pub parameter_mappings: Option<serde_json::Value>,
315}
316
317#[derive(Debug, Serialize, Deserialize, Clone)]
318pub struct WidgetPosition {
319 pub col: u32,
320 pub row: u32,
321 #[serde(rename = "sizeX")]
322 pub size_x: u32,
323 #[serde(rename = "sizeY")]
324 pub size_y: u32,
325}
326
327#[derive(Debug, Serialize, Deserialize)]
328pub struct DashboardMetadata {
329 pub id: u64,
330 pub name: String,
331 pub slug: String,
332 pub user_id: u64,
333 pub is_draft: bool,
334 pub is_archived: bool,
335 #[serde(rename = "dashboard_filters_enabled")]
336 pub filters_enabled: bool,
337 pub tags: Vec<String>,
338 pub widgets: Vec<WidgetMetadata>,
339}
340
341#[derive(Debug, Serialize, Deserialize)]
342pub struct WidgetMetadata {
343 pub id: u64,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub visualization_id: Option<u64>,
346 #[serde(skip_serializing_if = "Option::is_none")]
347 pub query_id: Option<u64>,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub visualization_name: Option<String>,
350 #[serde(default, skip_serializing_if = "String::is_empty")]
351 pub text: String,
352 pub options: WidgetOptions,
353}
354
355#[derive(Debug, Deserialize)]
356pub struct DashboardsResponse {
357 pub results: Vec<DashboardSummary>,
358 pub count: u64,
359}
360
361#[derive(Debug, Deserialize)]
362pub struct DashboardSummary {
363 #[allow(dead_code)]
364 pub id: u64,
365 pub name: String,
366 #[allow(dead_code)]
367 pub slug: String,
368 pub is_draft: bool,
369 pub is_archived: bool,
370}
371
372#[derive(Debug, Serialize)]
373pub struct CreateWidget {
374 pub dashboard_id: u64,
375 pub visualization_id: Option<u64>,
376 pub text: String,
377 pub width: u32,
378 pub options: WidgetOptions,
379}
380
381#[must_use]
382pub fn build_dashboard_level_parameter_mappings(parameters: &[Parameter]) -> serde_json::Value {
383 let mut mappings = serde_json::Map::new();
384 for param in parameters {
385 mappings.insert(param.name.clone(), serde_json::json!({
386 "mapTo": param.name,
387 "name": param.name,
388 "title": "",
389 "type": "dashboard-level",
390 "value": null,
391 }));
392 }
393 serde_json::Value::Object(mappings)
394}
395
396#[cfg(test)]
397#[allow(clippy::missing_errors_doc)]
398#[allow(clippy::unnecessary_literal_unwrap)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_job_status_from_u8_valid() {
404 assert!(matches!(JobStatus::from_u8(1).unwrap(), JobStatus::Pending));
405 assert!(matches!(JobStatus::from_u8(2).unwrap(), JobStatus::Started));
406 assert!(matches!(JobStatus::from_u8(3).unwrap(), JobStatus::Success));
407 assert!(matches!(JobStatus::from_u8(4).unwrap(), JobStatus::Failure));
408 assert!(matches!(JobStatus::from_u8(5).unwrap(), JobStatus::Cancelled));
409 }
410
411 #[test]
412 fn test_job_status_from_u8_invalid() {
413 assert!(JobStatus::from_u8(0).is_err());
414 assert!(JobStatus::from_u8(6).is_err());
415 assert!(JobStatus::from_u8(255).is_err());
416
417 let err = JobStatus::from_u8(10).unwrap_err();
418 assert!(err.to_string().contains("Invalid job status"));
419 }
420
421 #[test]
422 fn test_query_serialization() {
423 let query = Query {
424 id: 1,
425 name: "Test Query".to_string(),
426 description: None,
427 sql: "SELECT * FROM table".to_string(),
428 data_source_id: 63,
429 user: None,
430 schedule: None,
431 options: QueryOptions { parameters: vec![] },
432 visualizations: vec![],
433 tags: None,
434 is_archived: false,
435 is_draft: false,
436 updated_at: "2026-01-21".to_string(),
437 created_at: "2026-01-21".to_string(),
438 };
439
440 let json = serde_json::to_string(&query).unwrap();
441 assert!(json.contains("\"query\":"));
442 assert!(json.contains("SELECT * FROM table"));
443 }
444
445 #[test]
446 fn test_query_metadata_deserialization() {
447 let yaml = r"
448id: 100064
449name: Test Query
450description: null
451data_source_id: 63
452user_id: 530
453schedule: null
454options:
455 parameters:
456 - name: project
457 title: project
458 type: enum
459 value:
460 - try
461 enumOptions: |
462 try
463 autoland
464visualizations: []
465tags:
466 - bug 1840828
467";
468
469 let metadata: QueryMetadata = serde_yaml::from_str(yaml).unwrap();
470 assert_eq!(metadata.id, 100_064);
471 assert_eq!(metadata.name, "Test Query");
472 assert_eq!(metadata.data_source_id, 63);
473 assert_eq!(metadata.options.parameters.len(), 1);
474 assert_eq!(metadata.options.parameters[0].name, "project");
475 }
476
477 #[test]
478 fn test_datasource_deserialization() {
479 let json = r#"{
480 "id": 63,
481 "name": "Test DB",
482 "type": "bigquery",
483 "description": null,
484 "syntax": "sql",
485 "paused": 0,
486 "pause_reason": null,
487 "view_only": false,
488 "queue_name": "queries",
489 "scheduled_queue_name": "scheduled_queries",
490 "groups": {},
491 "options": {}
492 }"#;
493
494 let ds: DataSource = serde_json::from_str(json).unwrap();
495 assert_eq!(ds.id, 63);
496 assert_eq!(ds.name, "Test DB");
497 assert_eq!(ds.ds_type, "bigquery");
498 assert_eq!(ds.syntax, Some("sql".to_string()));
499 assert_eq!(ds.description, None);
500 assert_eq!(ds.paused, 0);
501 assert!(!ds.view_only);
502 assert_eq!(ds.queue_name, Some("queries".to_string()));
503 }
504
505 #[test]
506 fn test_datasource_with_nulls() {
507 let json = r#"{
508 "id": 10,
509 "name": "Minimal DB",
510 "type": "pg",
511 "description": "Test description",
512 "syntax": null,
513 "paused": 1,
514 "pause_reason": "Maintenance",
515 "view_only": true,
516 "queue_name": null,
517 "scheduled_queue_name": null,
518 "groups": null,
519 "options": null
520 }"#;
521
522 let ds: DataSource = serde_json::from_str(json).unwrap();
523 assert_eq!(ds.id, 10);
524 assert_eq!(ds.name, "Minimal DB");
525 assert_eq!(ds.ds_type, "pg");
526 assert_eq!(ds.description, Some("Test description".to_string()));
527 assert_eq!(ds.syntax, None);
528 assert_eq!(ds.paused, 1);
529 assert_eq!(ds.pause_reason, Some("Maintenance".to_string()));
530 assert!(ds.view_only);
531 assert_eq!(ds.queue_name, None);
532 }
533
534 #[test]
535 fn test_datasource_schema_deserialization() {
536 let json = r#"{
537 "schema": [
538 {
539 "name": "table1",
540 "columns": [
541 {"name": "col1", "type": "STRING"},
542 {"name": "col2", "type": "INTEGER"}
543 ]
544 },
545 {
546 "name": "table2",
547 "columns": [{"name": "id", "type": "INTEGER"}]
548 }
549 ]
550 }"#;
551
552 let schema: DataSourceSchema = serde_json::from_str(json).unwrap();
553 assert_eq!(schema.schema.len(), 2);
554 assert_eq!(schema.schema[0].name, "table1");
555 assert_eq!(schema.schema[0].columns.len(), 2);
556 assert_eq!(schema.schema[0].columns[0].name, "col1");
557 assert_eq!(schema.schema[0].columns[0].column_type, "STRING");
558 assert_eq!(schema.schema[1].name, "table2");
559 assert_eq!(schema.schema[1].columns.len(), 1);
560 }
561
562 #[test]
563 fn test_schema_table_structure() {
564 let json = r#"{
565 "name": "users",
566 "columns": [
567 {"name": "id", "type": "INTEGER"},
568 {"name": "name", "type": "STRING"},
569 {"name": "email", "type": "STRING"}
570 ]
571 }"#;
572
573 let table: SchemaTable = serde_json::from_str(json).unwrap();
574 assert_eq!(table.name, "users");
575 assert_eq!(table.columns.len(), 3);
576 assert_eq!(table.columns[0].name, "id");
577 assert_eq!(table.columns[0].column_type, "INTEGER");
578 assert_eq!(table.columns[1].name, "name");
579 assert_eq!(table.columns[1].column_type, "STRING");
580 assert_eq!(table.columns[2].name, "email");
581 assert_eq!(table.columns[2].column_type, "STRING");
582 }
583
584 #[test]
585 fn test_datasource_serialization() {
586 let ds = DataSource {
587 id: 123,
588 name: "My DB".to_string(),
589 ds_type: "mysql".to_string(),
590 syntax: Some("sql".to_string()),
591 description: Some("Test".to_string()),
592 paused: 0,
593 pause_reason: None,
594 view_only: false,
595 queue_name: Some("queries".to_string()),
596 scheduled_queue_name: None,
597 groups: None,
598 options: None,
599 };
600
601 let json = serde_json::to_string(&ds).unwrap();
602 assert!(json.contains("\"id\":123"));
603 assert!(json.contains("\"name\":\"My DB\""));
604 assert!(json.contains("\"type\":\"mysql\""));
605 assert!(json.contains("\"syntax\":\"sql\""));
606 }
607
608 #[test]
609 fn test_dashboard_deserialization() {
610 let json = r#"{
611 "id": 2570,
612 "name": "Test Dashboard",
613 "slug": "test-dashboard",
614 "user_id": 530,
615 "is_archived": false,
616 "is_draft": false,
617 "dashboard_filters_enabled": true,
618 "tags": ["tag1", "tag2"],
619 "widgets": []
620 }"#;
621
622 let dashboard: Dashboard = serde_json::from_str(json).unwrap();
623 assert_eq!(dashboard.id, 2570);
624 assert_eq!(dashboard.name, "Test Dashboard");
625 assert_eq!(dashboard.slug, "test-dashboard");
626 assert_eq!(dashboard.user_id, 530);
627 assert!(!dashboard.is_archived);
628 assert!(!dashboard.is_draft);
629 assert!(dashboard.filters_enabled);
630 assert_eq!(dashboard.tags, vec!["tag1", "tag2"]);
631 assert_eq!(dashboard.widgets.len(), 0);
632 }
633
634 #[test]
635 fn test_dashboard_with_widgets() {
636 let json = r##"{
637 "id": 2570,
638 "name": "Test Dashboard",
639 "slug": "test-dashboard",
640 "user_id": 530,
641 "is_archived": false,
642 "is_draft": false,
643 "dashboard_filters_enabled": false,
644 "tags": [],
645 "widgets": [
646 {
647 "id": 75035,
648 "dashboard_id": 2570,
649 "text": "# Test Widget",
650 "options": {
651 "position": {
652 "col": 0,
653 "row": 0,
654 "sizeX": 6,
655 "sizeY": 2
656 }
657 }
658 },
659 {
660 "id": 75029,
661 "dashboard_id": 2570,
662 "visualization_id": 279588,
663 "visualization": {
664 "id": 279588,
665 "name": "Total MAU",
666 "query": {
667 "id": 114049,
668 "name": "MAU Query"
669 }
670 },
671 "text": "",
672 "options": {
673 "position": {
674 "col": 3,
675 "row": 2,
676 "sizeX": 3,
677 "sizeY": 8
678 },
679 "parameterMappings": {
680 "channel": {
681 "name": "channel",
682 "type": "dashboard-level"
683 }
684 }
685 }
686 }
687 ]
688 }"##;
689
690 let dashboard: Dashboard = serde_json::from_str(json).unwrap();
691 assert_eq!(dashboard.widgets.len(), 2);
692 assert_eq!(dashboard.widgets[0].id, 75035);
693 assert_eq!(dashboard.widgets[0].text, "# Test Widget");
694 assert!(dashboard.widgets[0].visualization_id.is_none());
695 assert_eq!(dashboard.widgets[1].id, 75029);
696 assert_eq!(dashboard.widgets[1].visualization_id, Some(279_588));
697 let viz = dashboard.widgets[1].visualization.as_ref().unwrap();
698 assert_eq!(viz.id, 279_588);
699 assert_eq!(viz.query.id, 114_049);
700 }
701
702 #[test]
703 fn test_widget_position_serde() {
704 let json = r#"{
705 "col": 3,
706 "row": 5,
707 "sizeX": 6,
708 "sizeY": 4
709 }"#;
710
711 let position: WidgetPosition = serde_json::from_str(json).unwrap();
712 assert_eq!(position.col, 3);
713 assert_eq!(position.row, 5);
714 assert_eq!(position.size_x, 6);
715 assert_eq!(position.size_y, 4);
716
717 let serialized = serde_json::to_string(&position).unwrap();
718 assert!(serialized.contains("\"sizeX\":6"));
719 assert!(serialized.contains("\"sizeY\":4"));
720 }
721
722 #[test]
723 fn test_dashboard_metadata_yaml() {
724 let yaml = r"
725id: 2570
726name: Test Dashboard
727slug: test-dashboard
728user_id: 530
729is_draft: false
730is_archived: false
731dashboard_filters_enabled: true
732tags:
733 - tag1
734 - tag2
735widgets:
736 - id: 75035
737 visualization_id: null
738 query_id: null
739 visualization_name: null
740 text: '# Test Widget'
741 options:
742 position:
743 col: 0
744 row: 0
745 sizeX: 6
746 sizeY: 2
747 parameter_mappings: null
748";
749
750 let metadata: DashboardMetadata = serde_yaml::from_str(yaml).unwrap();
751 assert_eq!(metadata.id, 2570);
752 assert_eq!(metadata.name, "Test Dashboard");
753 assert_eq!(metadata.slug, "test-dashboard");
754 assert_eq!(metadata.user_id, 530);
755 assert!(!metadata.is_draft);
756 assert!(!metadata.is_archived);
757 assert!(metadata.filters_enabled);
758 assert_eq!(metadata.tags, vec!["tag1", "tag2"]);
759 assert_eq!(metadata.widgets.len(), 1);
760 assert_eq!(metadata.widgets[0].id, 75035);
761 assert_eq!(metadata.widgets[0].text, "# Test Widget");
762 }
763
764 #[test]
765 fn test_widget_metadata_text_widget() {
766 let yaml = r"
767id: 75035
768visualization_id: null
769query_id: null
770visualization_name: null
771text: '## Section Header'
772options:
773 position:
774 col: 0
775 row: 0
776 sizeX: 6
777 sizeY: 2
778 parameter_mappings: null
779";
780
781 let widget: WidgetMetadata = serde_yaml::from_str(yaml).unwrap();
782 assert_eq!(widget.id, 75035);
783 assert!(widget.visualization_id.is_none());
784 assert!(widget.query_id.is_none());
785 assert!(widget.visualization_name.is_none());
786 assert_eq!(widget.text, "## Section Header");
787 assert_eq!(widget.options.position.col, 0);
788 assert_eq!(widget.options.position.size_x, 6);
789 }
790
791 #[test]
792 fn test_widget_metadata_viz_widget() {
793 let yaml = r"
794id: 75029
795visualization_id: 279588
796query_id: 114049
797visualization_name: Total MAU
798text: ''
799options:
800 position:
801 col: 3
802 row: 2
803 sizeX: 3
804 sizeY: 8
805 parameterMappings:
806 channel:
807 name: channel
808 type: dashboard-level
809";
810
811 let widget: WidgetMetadata = serde_yaml::from_str(yaml).unwrap();
812 assert_eq!(widget.id, 75029);
813 assert_eq!(widget.visualization_id, Some(279_588));
814 assert_eq!(widget.query_id, Some(114_049));
815 assert_eq!(widget.visualization_name, Some("Total MAU".to_string()));
816 assert_eq!(widget.text, "");
817 assert!(widget.options.parameter_mappings.is_some());
818 }
819
820 #[test]
821 fn test_create_widget_serialization() {
822 let widget = CreateWidget {
823 dashboard_id: 2570,
824 visualization_id: Some(279_588),
825 text: String::new(),
826 width: 1,
827 options: WidgetOptions {
828 position: WidgetPosition {
829 col: 0,
830 row: 0,
831 size_x: 3,
832 size_y: 2,
833 },
834 parameter_mappings: None,
835 },
836 };
837
838 let json = serde_json::to_string(&widget).unwrap();
839 assert!(json.contains("\"dashboard_id\":2570"));
840 assert!(json.contains("\"visualization_id\":279588"));
841 assert!(json.contains("\"sizeX\":3"));
842 assert!(json.contains("\"sizeY\":2"));
843 }
844
845 #[test]
846 fn test_create_text_widget_serialization() {
847 let widget = CreateWidget {
848 dashboard_id: 2570,
849 visualization_id: None,
850 text: "Some text".to_string(),
851 width: 1,
852 options: WidgetOptions {
853 position: WidgetPosition {
854 col: 0,
855 row: 0,
856 size_x: 3,
857 size_y: 2,
858 },
859 parameter_mappings: None,
860 },
861 };
862
863 let json = serde_json::to_string(&widget).unwrap();
864 assert!(json.contains("\"visualization_id\":null"));
865 }
866
867 #[test]
868 fn test_dashboards_response() {
869 let json = r#"{
870 "results": [
871 {
872 "id": 2570,
873 "name": "Dashboard 1",
874 "slug": "dashboard-1",
875 "is_draft": false,
876 "is_archived": false
877 },
878 {
879 "id": 2558,
880 "name": "Dashboard 2",
881 "slug": "dashboard-2",
882 "is_draft": true,
883 "is_archived": false
884 }
885 ],
886 "count": 2
887 }"#;
888
889 let response: DashboardsResponse = serde_json::from_str(json).unwrap();
890 assert_eq!(response.results.len(), 2);
891 assert_eq!(response.count, 2);
892 assert_eq!(response.results[0].id, 2570);
893 assert_eq!(response.results[0].name, "Dashboard 1");
894 assert_eq!(response.results[0].slug, "dashboard-1");
895 assert!(!response.results[0].is_draft);
896 assert!(!response.results[0].is_archived);
897 assert_eq!(response.results[1].id, 2558);
898 assert_eq!(response.results[1].slug, "dashboard-2");
899 assert!(response.results[1].is_draft);
900 }
901
902 #[test]
903 fn test_build_dashboard_level_parameter_mappings_empty() {
904 let result = build_dashboard_level_parameter_mappings(&[]);
905 assert_eq!(result, serde_json::json!({}));
906 }
907
908 #[test]
909 fn test_build_dashboard_level_parameter_mappings_with_params() {
910 let params = vec![
911 Parameter {
912 name: "channel".to_string(),
913 title: "Channel".to_string(),
914 param_type: "enum".to_string(),
915 value: None,
916 enum_options: None,
917 query_id: None,
918 multi_values_options: None,
919 },
920 Parameter {
921 name: "date".to_string(),
922 title: "Date".to_string(),
923 param_type: "date".to_string(),
924 value: None,
925 enum_options: None,
926 query_id: None,
927 multi_values_options: None,
928 },
929 ];
930
931 let result = build_dashboard_level_parameter_mappings(¶ms);
932
933 let expected = serde_json::json!({
934 "channel": {
935 "mapTo": "channel",
936 "name": "channel",
937 "title": "",
938 "type": "dashboard-level",
939 "value": null,
940 },
941 "date": {
942 "mapTo": "date",
943 "name": "date",
944 "title": "",
945 "type": "dashboard-level",
946 "value": null,
947 },
948 });
949
950 assert_eq!(result, expected);
951 }
952}