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