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