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