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