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, PartialEq)]
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)]
210pub struct QuerySnippet {
211 pub id: u64,
212 pub trigger: String,
213 pub description: Option<String>,
214 pub snippet: String,
215 #[serde(default)]
216 pub user: Option<QueryUser>,
217 pub updated_at: String,
218 pub created_at: String,
219}
220
221#[derive(Debug, Serialize, Clone)]
222pub struct CreateQuerySnippet {
223 pub trigger: String,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub description: Option<String>,
226 pub snippet: String,
227}
228
229#[derive(Debug, Serialize, Deserialize)]
230pub struct SnippetMetadata {
231 pub id: u64,
232 pub trigger: String,
233 pub description: Option<String>,
234}
235
236#[derive(Debug, Serialize, Deserialize, Clone)]
237#[allow(dead_code)]
238pub struct User {
239 pub id: u64,
240 pub name: String,
241 pub email: String,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub profile_image_url: Option<String>,
244}
245
246#[derive(Debug, Serialize, Deserialize, Clone)]
247pub struct DataSource {
248 pub id: u64,
249 pub name: String,
250 #[serde(rename = "type")]
251 pub ds_type: String,
252 pub syntax: Option<String>,
253 pub description: Option<String>,
254 pub paused: u8,
255 pub pause_reason: Option<String>,
256 pub view_only: bool,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub queue_name: Option<String>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub scheduled_queue_name: Option<String>,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub groups: Option<serde_json::Value>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub options: Option<serde_json::Value>,
265}
266
267#[derive(Debug, Serialize, Deserialize)]
268pub struct DataSourceSchema {
269 pub schema: Vec<SchemaTable>,
270}
271
272#[derive(Debug, Serialize, Deserialize)]
273pub struct SchemaTable {
274 pub name: String,
275 pub columns: Vec<SchemaColumn>,
276}
277
278#[derive(Debug, Serialize, Deserialize)]
279pub struct SchemaColumn {
280 pub name: String,
281 #[serde(rename = "type")]
282 pub column_type: String,
283}
284
285#[derive(Debug, Serialize, Deserialize)]
286pub struct RefreshRequest {
287 pub max_age: u64,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub parameters: Option<std::collections::HashMap<String, serde_json::Value>>,
290}
291
292#[derive(Debug, Serialize, Deserialize)]
293pub struct AdhocRefreshRequest {
294 #[serde(rename = "query")]
295 pub sql: String,
296 pub data_source_id: u64,
297 pub max_age: u64,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub parameters: Option<std::collections::HashMap<String, serde_json::Value>>,
300}
301
302#[derive(Debug, Serialize, Deserialize)]
303pub struct JobResponse {
304 pub job: Job,
305}
306
307#[derive(Debug, Serialize, Deserialize)]
308pub struct Job {
309 pub id: String,
310 pub status: u8,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub query_result_id: Option<u64>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub error: Option<String>,
315}
316
317#[derive(Debug, Serialize, Deserialize)]
318pub struct QueryResultResponse {
319 pub query_result: QueryResult,
320}
321
322#[derive(Debug, Serialize, Deserialize)]
323pub struct QueryResult {
324 pub id: u64,
325 pub data: QueryResultData,
326 pub runtime: f64,
327 pub retrieved_at: String,
328}
329
330#[derive(Debug, Serialize, Deserialize)]
331pub struct QueryResultData {
332 pub columns: Vec<Column>,
333 pub rows: Vec<serde_json::Value>,
334}
335
336#[derive(Debug, Serialize, Deserialize)]
337pub struct Column {
338 pub name: String,
339 #[serde(rename = "type")]
340 pub type_name: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub friendly_name: Option<String>,
343}
344
345#[derive(Debug, Clone, Copy)]
346pub enum JobStatus {
347 Pending = 1,
348 Started = 2,
349 Success = 3,
350 Failure = 4,
351 Cancelled = 5,
352}
353
354impl JobStatus {
355 pub fn from_u8(status: u8) -> anyhow::Result<Self> {
356 match status {
357 1 => Ok(Self::Pending),
358 2 => Ok(Self::Started),
359 3 => Ok(Self::Success),
360 4 => Ok(Self::Failure),
361 5 => Ok(Self::Cancelled),
362 _ => Err(anyhow::anyhow!("Invalid job status: {status}")),
363 }
364 }
365}
366
367#[derive(Debug, Serialize, Deserialize)]
368pub struct Dashboard {
369 pub id: u64,
370 pub name: String,
371 pub slug: String,
372 pub user_id: u64,
373 pub is_archived: bool,
374 pub is_draft: bool,
375 #[serde(rename = "dashboard_filters_enabled")]
376 pub filters_enabled: bool,
377 pub tags: Vec<String>,
378 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
379 pub widgets: Vec<Widget>,
380}
381
382#[derive(Debug, Serialize)]
383pub struct CreateDashboard {
384 pub name: String,
385}
386
387#[derive(Debug, Serialize, Deserialize)]
388pub struct Widget {
389 pub id: u64,
390 pub dashboard_id: u64,
391 pub width: u32,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub visualization_id: Option<u64>,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 pub visualization: Option<WidgetVisualization>,
396 #[serde(default, deserialize_with = "deserialize_null_as_empty_string")]
397 pub text: String,
398 pub options: WidgetOptions,
399}
400
401#[derive(Debug, Serialize, Deserialize)]
402pub struct WidgetVisualization {
403 pub id: u64,
404 pub name: String,
405 pub query: VisualizationQuery,
406}
407
408#[derive(Debug, Serialize, Deserialize)]
409pub struct VisualizationQuery {
410 pub id: u64,
411 pub name: String,
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone)]
415pub struct WidgetOptions {
416 pub position: WidgetPosition,
417 #[serde(
418 default,
419 skip_serializing_if = "Option::is_none",
420 rename = "parameterMappings"
421 )]
422 pub parameter_mappings: Option<serde_json::Value>,
423}
424
425#[derive(Debug, Serialize, Deserialize, Clone)]
426pub struct WidgetPosition {
427 pub col: u32,
428 pub row: u32,
429 #[serde(rename = "sizeX")]
430 pub size_x: u32,
431 #[serde(rename = "sizeY")]
432 pub size_y: u32,
433}
434
435#[derive(Debug, Serialize, Deserialize)]
436pub struct DashboardMetadata {
437 pub id: u64,
438 pub name: String,
439 pub slug: String,
440 pub user_id: u64,
441 pub is_draft: bool,
442 pub is_archived: bool,
443 #[serde(rename = "dashboard_filters_enabled")]
444 pub filters_enabled: bool,
445 pub tags: Vec<String>,
446 pub widgets: Vec<WidgetMetadata>,
447}
448
449#[derive(Debug, Serialize, Deserialize)]
450pub struct WidgetMetadata {
451 pub id: u64,
452 #[serde(default = "default_width")]
453 pub width: u32,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub visualization_id: Option<u64>,
456 #[serde(skip_serializing_if = "Option::is_none")]
457 pub query_id: Option<u64>,
458 #[serde(skip_serializing_if = "Option::is_none")]
459 pub visualization_name: Option<String>,
460 #[serde(default, skip_serializing_if = "String::is_empty")]
461 pub text: String,
462 pub options: WidgetOptions,
463}
464
465#[derive(Debug, Deserialize)]
466pub struct DashboardsResponse {
467 pub results: Vec<DashboardSummary>,
468 pub count: u64,
469}
470
471#[derive(Debug, Deserialize)]
472pub struct DashboardSummary {
473 #[allow(dead_code)]
474 pub id: u64,
475 pub name: String,
476 #[allow(dead_code)]
477 pub slug: String,
478 pub is_draft: bool,
479 pub is_archived: bool,
480}
481
482#[derive(Debug, Serialize)]
483pub struct CreateWidget {
484 pub dashboard_id: u64,
485 pub visualization_id: Option<u64>,
486 pub text: String,
487 pub width: u32,
488 pub options: WidgetOptions,
489}
490
491#[must_use]
492pub fn build_dashboard_level_parameter_mappings(parameters: &[Parameter]) -> serde_json::Value {
493 let mut mappings = serde_json::Map::new();
494 for param in parameters {
495 mappings.insert(
496 param.name.clone(),
497 serde_json::json!({
498 "mapTo": param.name,
499 "name": param.name,
500 "title": "",
501 "type": "dashboard-level",
502 "value": null,
503 }),
504 );
505 }
506 serde_json::Value::Object(mappings)
507}
508
509#[cfg(test)]
510#[allow(clippy::missing_errors_doc)]
511#[allow(clippy::unnecessary_literal_unwrap)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn test_job_status_from_u8_valid() {
517 assert!(matches!(JobStatus::from_u8(1).unwrap(), JobStatus::Pending));
518 assert!(matches!(JobStatus::from_u8(2).unwrap(), JobStatus::Started));
519 assert!(matches!(JobStatus::from_u8(3).unwrap(), JobStatus::Success));
520 assert!(matches!(JobStatus::from_u8(4).unwrap(), JobStatus::Failure));
521 assert!(matches!(
522 JobStatus::from_u8(5).unwrap(),
523 JobStatus::Cancelled
524 ));
525 }
526
527 #[test]
528 fn test_job_status_from_u8_invalid() {
529 assert!(JobStatus::from_u8(0).is_err());
530 assert!(JobStatus::from_u8(6).is_err());
531 assert!(JobStatus::from_u8(255).is_err());
532
533 let err = JobStatus::from_u8(10).unwrap_err();
534 assert!(err.to_string().contains("Invalid job status"));
535 }
536
537 #[test]
538 fn test_query_serialization() {
539 let query = Query {
540 id: 1,
541 name: "Test Query".to_string(),
542 description: None,
543 sql: "SELECT * FROM table".to_string(),
544 data_source_id: 63,
545 user: None,
546 schedule: None,
547 options: QueryOptions { parameters: vec![] },
548 visualizations: vec![],
549 tags: None,
550 is_archived: false,
551 is_draft: false,
552 updated_at: "2026-01-21".to_string(),
553 created_at: "2026-01-21".to_string(),
554 };
555
556 let json = serde_json::to_string(&query).unwrap();
557 assert!(json.contains("\"query\":"));
558 assert!(json.contains("SELECT * FROM table"));
559 }
560
561 #[test]
562 fn test_query_metadata_deserialization() {
563 let yaml = r"
564id: 100064
565name: Test Query
566description: null
567data_source_id: 63
568user_id: 530
569schedule: null
570options:
571 parameters:
572 - name: project
573 title: project
574 type: enum
575 value:
576 - try
577 enumOptions: |
578 try
579 autoland
580visualizations: []
581tags:
582 - bug 1840828
583";
584
585 let metadata: QueryMetadata = serde_yaml::from_str(yaml).unwrap();
586 assert_eq!(metadata.id, 100_064);
587 assert_eq!(metadata.name, "Test Query");
588 assert_eq!(metadata.data_source_id, 63);
589 assert_eq!(metadata.options.parameters.len(), 1);
590 assert_eq!(metadata.options.parameters[0].name, "project");
591 }
592
593 #[test]
594 fn test_query_snippet_deserialization() {
595 let json = r#"{
596 "id": 31,
597 "trigger": "reviewbot_e2e_action_ctcs",
598 "description": "Action-task gap compression CTEs",
599 "snippet": "action_tasks AS (\n SELECT 1\n)",
600 "user": {
601 "id": 530,
602 "name": "jlorenzo@mozilla.com",
603 "email": "jlorenzo@mozilla.com"
604 },
605 "updated_at": "2026-06-24T09:13:26.873Z",
606 "created_at": "2026-06-24T09:13:26.873Z"
607 }"#;
608
609 let snippet: QuerySnippet = serde_json::from_str(json).unwrap();
610 assert_eq!(snippet.id, 31);
611 assert_eq!(snippet.trigger, "reviewbot_e2e_action_ctcs");
612 assert_eq!(
613 snippet.description,
614 Some("Action-task gap compression CTEs".to_string())
615 );
616 assert!(snippet.snippet.contains("action_tasks AS"));
617 assert_eq!(snippet.user.unwrap().email, "jlorenzo@mozilla.com");
618 }
619
620 #[test]
621 fn test_query_snippet_missing_user_deserialization() {
622 let json = r#"{
623 "id": 12,
624 "trigger": "days_ago",
625 "description": "",
626 "snippet": "(CURRENT_DATE - INTERVAL '$1' DAY)",
627 "updated_at": "2018-04-05T19:36:28.831Z",
628 "created_at": "2018-04-05T19:09:55.265Z"
629 }"#;
630
631 let snippet: QuerySnippet = serde_json::from_str(json).unwrap();
632 assert_eq!(snippet.id, 12);
633 assert!(snippet.user.is_none());
634 }
635
636 #[test]
637 fn test_create_query_snippet_serialization() {
638 let create = CreateQuerySnippet {
639 trigger: "reviewbot_push_duration".to_string(),
640 description: Some("Corrected push duration formula".to_string()),
641 snippet: "TIMESTAMP_DIFF(a, b, SECOND)".to_string(),
642 };
643
644 let json = serde_json::to_string(&create).unwrap();
645 assert!(json.contains("\"trigger\":\"reviewbot_push_duration\""));
646 assert!(json.contains("\"snippet\":\"TIMESTAMP_DIFF(a, b, SECOND)\""));
647 assert!(!json.contains("\"id\""));
648 }
649
650 #[test]
651 fn test_create_query_snippet_omits_none_description() {
652 let create = CreateQuerySnippet {
653 trigger: "trig".to_string(),
654 description: None,
655 snippet: "SELECT 1".to_string(),
656 };
657
658 let json = serde_json::to_string(&create).unwrap();
659 assert!(!json.contains("description"));
660 }
661
662 #[test]
663 fn test_snippet_metadata_yaml_roundtrip() {
664 let yaml = r"
665id: 31
666trigger: reviewbot_e2e_action_ctcs
667description: Action-task gap compression CTEs
668";
669
670 let metadata: SnippetMetadata = serde_yaml::from_str(yaml).unwrap();
671 assert_eq!(metadata.id, 31);
672 assert_eq!(metadata.trigger, "reviewbot_e2e_action_ctcs");
673 assert_eq!(
674 metadata.description,
675 Some("Action-task gap compression CTEs".to_string())
676 );
677
678 let reserialized = serde_yaml::to_string(&metadata).unwrap();
679 let roundtripped: SnippetMetadata = serde_yaml::from_str(&reserialized).unwrap();
680 assert_eq!(roundtripped.id, metadata.id);
681 assert_eq!(roundtripped.trigger, metadata.trigger);
682 }
683
684 #[test]
685 fn test_datasource_deserialization() {
686 let json = r#"{
687 "id": 63,
688 "name": "Test DB",
689 "type": "bigquery",
690 "description": null,
691 "syntax": "sql",
692 "paused": 0,
693 "pause_reason": null,
694 "view_only": false,
695 "queue_name": "queries",
696 "scheduled_queue_name": "scheduled_queries",
697 "groups": {},
698 "options": {}
699 }"#;
700
701 let ds: DataSource = serde_json::from_str(json).unwrap();
702 assert_eq!(ds.id, 63);
703 assert_eq!(ds.name, "Test DB");
704 assert_eq!(ds.ds_type, "bigquery");
705 assert_eq!(ds.syntax, Some("sql".to_string()));
706 assert_eq!(ds.description, None);
707 assert_eq!(ds.paused, 0);
708 assert!(!ds.view_only);
709 assert_eq!(ds.queue_name, Some("queries".to_string()));
710 }
711
712 #[test]
713 fn test_datasource_with_nulls() {
714 let json = r#"{
715 "id": 10,
716 "name": "Minimal DB",
717 "type": "pg",
718 "description": "Test description",
719 "syntax": null,
720 "paused": 1,
721 "pause_reason": "Maintenance",
722 "view_only": true,
723 "queue_name": null,
724 "scheduled_queue_name": null,
725 "groups": null,
726 "options": null
727 }"#;
728
729 let ds: DataSource = serde_json::from_str(json).unwrap();
730 assert_eq!(ds.id, 10);
731 assert_eq!(ds.name, "Minimal DB");
732 assert_eq!(ds.ds_type, "pg");
733 assert_eq!(ds.description, Some("Test description".to_string()));
734 assert_eq!(ds.syntax, None);
735 assert_eq!(ds.paused, 1);
736 assert_eq!(ds.pause_reason, Some("Maintenance".to_string()));
737 assert!(ds.view_only);
738 assert_eq!(ds.queue_name, None);
739 }
740
741 #[test]
742 fn test_datasource_schema_deserialization() {
743 let json = r#"{
744 "schema": [
745 {
746 "name": "table1",
747 "columns": [
748 {"name": "col1", "type": "STRING"},
749 {"name": "col2", "type": "INTEGER"}
750 ]
751 },
752 {
753 "name": "table2",
754 "columns": [{"name": "id", "type": "INTEGER"}]
755 }
756 ]
757 }"#;
758
759 let schema: DataSourceSchema = serde_json::from_str(json).unwrap();
760 assert_eq!(schema.schema.len(), 2);
761 assert_eq!(schema.schema[0].name, "table1");
762 assert_eq!(schema.schema[0].columns.len(), 2);
763 assert_eq!(schema.schema[0].columns[0].name, "col1");
764 assert_eq!(schema.schema[0].columns[0].column_type, "STRING");
765 assert_eq!(schema.schema[1].name, "table2");
766 assert_eq!(schema.schema[1].columns.len(), 1);
767 }
768
769 #[test]
770 fn test_schema_table_structure() {
771 let json = r#"{
772 "name": "users",
773 "columns": [
774 {"name": "id", "type": "INTEGER"},
775 {"name": "name", "type": "STRING"},
776 {"name": "email", "type": "STRING"}
777 ]
778 }"#;
779
780 let table: SchemaTable = serde_json::from_str(json).unwrap();
781 assert_eq!(table.name, "users");
782 assert_eq!(table.columns.len(), 3);
783 assert_eq!(table.columns[0].name, "id");
784 assert_eq!(table.columns[0].column_type, "INTEGER");
785 assert_eq!(table.columns[1].name, "name");
786 assert_eq!(table.columns[1].column_type, "STRING");
787 assert_eq!(table.columns[2].name, "email");
788 assert_eq!(table.columns[2].column_type, "STRING");
789 }
790
791 #[test]
792 fn test_datasource_serialization() {
793 let ds = DataSource {
794 id: 123,
795 name: "My DB".to_string(),
796 ds_type: "mysql".to_string(),
797 syntax: Some("sql".to_string()),
798 description: Some("Test".to_string()),
799 paused: 0,
800 pause_reason: None,
801 view_only: false,
802 queue_name: Some("queries".to_string()),
803 scheduled_queue_name: None,
804 groups: None,
805 options: None,
806 };
807
808 let json = serde_json::to_string(&ds).unwrap();
809 assert!(json.contains("\"id\":123"));
810 assert!(json.contains("\"name\":\"My DB\""));
811 assert!(json.contains("\"type\":\"mysql\""));
812 assert!(json.contains("\"syntax\":\"sql\""));
813 }
814
815 #[test]
816 fn test_dashboard_deserialization() {
817 let json = r#"{
818 "id": 2570,
819 "name": "Test Dashboard",
820 "slug": "test-dashboard",
821 "user_id": 530,
822 "is_archived": false,
823 "is_draft": false,
824 "dashboard_filters_enabled": true,
825 "tags": ["tag1", "tag2"],
826 "widgets": []
827 }"#;
828
829 let dashboard: Dashboard = serde_json::from_str(json).unwrap();
830 assert_eq!(dashboard.id, 2570);
831 assert_eq!(dashboard.name, "Test Dashboard");
832 assert_eq!(dashboard.slug, "test-dashboard");
833 assert_eq!(dashboard.user_id, 530);
834 assert!(!dashboard.is_archived);
835 assert!(!dashboard.is_draft);
836 assert!(dashboard.filters_enabled);
837 assert_eq!(dashboard.tags, vec!["tag1", "tag2"]);
838 assert_eq!(dashboard.widgets.len(), 0);
839 }
840
841 #[test]
842 fn test_dashboard_with_widgets() {
843 let json = r##"{
844 "id": 2570,
845 "name": "Test Dashboard",
846 "slug": "test-dashboard",
847 "user_id": 530,
848 "is_archived": false,
849 "is_draft": false,
850 "dashboard_filters_enabled": false,
851 "tags": [],
852 "widgets": [
853 {
854 "id": 75035,
855 "dashboard_id": 2570,
856 "width": 1,
857 "text": "# Test Widget",
858 "options": {
859 "position": {
860 "col": 0,
861 "row": 0,
862 "sizeX": 6,
863 "sizeY": 2
864 }
865 }
866 },
867 {
868 "id": 75029,
869 "dashboard_id": 2570,
870 "width": 1,
871 "visualization_id": 279588,
872 "visualization": {
873 "id": 279588,
874 "name": "Total MAU",
875 "query": {
876 "id": 114049,
877 "name": "MAU Query"
878 }
879 },
880 "text": "",
881 "options": {
882 "position": {
883 "col": 3,
884 "row": 2,
885 "sizeX": 3,
886 "sizeY": 8
887 },
888 "parameterMappings": {
889 "channel": {
890 "name": "channel",
891 "type": "dashboard-level"
892 }
893 }
894 }
895 }
896 ]
897 }"##;
898
899 let dashboard: Dashboard = serde_json::from_str(json).unwrap();
900 assert_eq!(dashboard.widgets.len(), 2);
901 assert_eq!(dashboard.widgets[0].id, 75035);
902 assert_eq!(dashboard.widgets[0].text, "# Test Widget");
903 assert!(dashboard.widgets[0].visualization_id.is_none());
904 assert_eq!(dashboard.widgets[1].id, 75029);
905 assert_eq!(dashboard.widgets[1].visualization_id, Some(279_588));
906 let viz = dashboard.widgets[1].visualization.as_ref().unwrap();
907 assert_eq!(viz.id, 279_588);
908 assert_eq!(viz.query.id, 114_049);
909 }
910
911 #[test]
912 fn test_widget_position_serde() {
913 let json = r#"{
914 "col": 3,
915 "row": 5,
916 "sizeX": 6,
917 "sizeY": 4
918 }"#;
919
920 let position: WidgetPosition = serde_json::from_str(json).unwrap();
921 assert_eq!(position.col, 3);
922 assert_eq!(position.row, 5);
923 assert_eq!(position.size_x, 6);
924 assert_eq!(position.size_y, 4);
925
926 let serialized = serde_json::to_string(&position).unwrap();
927 assert!(serialized.contains("\"sizeX\":6"));
928 assert!(serialized.contains("\"sizeY\":4"));
929 }
930
931 #[test]
932 fn test_dashboard_metadata_yaml() {
933 let yaml = r"
934id: 2570
935name: Test Dashboard
936slug: test-dashboard
937user_id: 530
938is_draft: false
939is_archived: false
940dashboard_filters_enabled: true
941tags:
942 - tag1
943 - tag2
944widgets:
945 - id: 75035
946 visualization_id: null
947 query_id: null
948 visualization_name: null
949 text: '# Test Widget'
950 options:
951 position:
952 col: 0
953 row: 0
954 sizeX: 6
955 sizeY: 2
956 parameter_mappings: null
957";
958
959 let metadata: DashboardMetadata = serde_yaml::from_str(yaml).unwrap();
960 assert_eq!(metadata.id, 2570);
961 assert_eq!(metadata.name, "Test Dashboard");
962 assert_eq!(metadata.slug, "test-dashboard");
963 assert_eq!(metadata.user_id, 530);
964 assert!(!metadata.is_draft);
965 assert!(!metadata.is_archived);
966 assert!(metadata.filters_enabled);
967 assert_eq!(metadata.tags, vec!["tag1", "tag2"]);
968 assert_eq!(metadata.widgets.len(), 1);
969 assert_eq!(metadata.widgets[0].id, 75035);
970 assert_eq!(metadata.widgets[0].text, "# Test Widget");
971 }
972
973 #[test]
974 fn test_widget_metadata_text_widget() {
975 let yaml = r"
976id: 75035
977visualization_id: null
978query_id: null
979visualization_name: null
980text: '## Section Header'
981options:
982 position:
983 col: 0
984 row: 0
985 sizeX: 6
986 sizeY: 2
987 parameter_mappings: null
988";
989
990 let widget: WidgetMetadata = serde_yaml::from_str(yaml).unwrap();
991 assert_eq!(widget.id, 75035);
992 assert!(widget.visualization_id.is_none());
993 assert!(widget.query_id.is_none());
994 assert!(widget.visualization_name.is_none());
995 assert_eq!(widget.text, "## Section Header");
996 assert_eq!(widget.options.position.col, 0);
997 assert_eq!(widget.options.position.size_x, 6);
998 }
999
1000 #[test]
1001 fn test_widget_metadata_viz_widget() {
1002 let yaml = r"
1003id: 75029
1004visualization_id: 279588
1005query_id: 114049
1006visualization_name: Total MAU
1007text: ''
1008options:
1009 position:
1010 col: 3
1011 row: 2
1012 sizeX: 3
1013 sizeY: 8
1014 parameterMappings:
1015 channel:
1016 name: channel
1017 type: dashboard-level
1018";
1019
1020 let widget: WidgetMetadata = serde_yaml::from_str(yaml).unwrap();
1021 assert_eq!(widget.id, 75029);
1022 assert_eq!(widget.visualization_id, Some(279_588));
1023 assert_eq!(widget.query_id, Some(114_049));
1024 assert_eq!(widget.visualization_name, Some("Total MAU".to_string()));
1025 assert_eq!(widget.text, "");
1026 assert!(widget.options.parameter_mappings.is_some());
1027 }
1028
1029 #[test]
1030 fn test_create_widget_serialization() {
1031 let widget = CreateWidget {
1032 dashboard_id: 2570,
1033 visualization_id: Some(279_588),
1034 text: String::new(),
1035 width: 1,
1036 options: WidgetOptions {
1037 position: WidgetPosition {
1038 col: 0,
1039 row: 0,
1040 size_x: 3,
1041 size_y: 2,
1042 },
1043 parameter_mappings: None,
1044 },
1045 };
1046
1047 let json = serde_json::to_string(&widget).unwrap();
1048 assert!(json.contains("\"dashboard_id\":2570"));
1049 assert!(json.contains("\"visualization_id\":279588"));
1050 assert!(json.contains("\"sizeX\":3"));
1051 assert!(json.contains("\"sizeY\":2"));
1052 }
1053
1054 #[test]
1055 fn test_create_text_widget_serialization() {
1056 let widget = CreateWidget {
1057 dashboard_id: 2570,
1058 visualization_id: None,
1059 text: "Some text".to_string(),
1060 width: 1,
1061 options: WidgetOptions {
1062 position: WidgetPosition {
1063 col: 0,
1064 row: 0,
1065 size_x: 3,
1066 size_y: 2,
1067 },
1068 parameter_mappings: None,
1069 },
1070 };
1071
1072 let json = serde_json::to_string(&widget).unwrap();
1073 assert!(json.contains("\"visualization_id\":null"));
1074 }
1075
1076 #[test]
1077 fn test_dashboards_response() {
1078 let json = r#"{
1079 "results": [
1080 {
1081 "id": 2570,
1082 "name": "Dashboard 1",
1083 "slug": "dashboard-1",
1084 "is_draft": false,
1085 "is_archived": false
1086 },
1087 {
1088 "id": 2558,
1089 "name": "Dashboard 2",
1090 "slug": "dashboard-2",
1091 "is_draft": true,
1092 "is_archived": false
1093 }
1094 ],
1095 "count": 2
1096 }"#;
1097
1098 let response: DashboardsResponse = serde_json::from_str(json).unwrap();
1099 assert_eq!(response.results.len(), 2);
1100 assert_eq!(response.count, 2);
1101 assert_eq!(response.results[0].id, 2570);
1102 assert_eq!(response.results[0].name, "Dashboard 1");
1103 assert_eq!(response.results[0].slug, "dashboard-1");
1104 assert!(!response.results[0].is_draft);
1105 assert!(!response.results[0].is_archived);
1106 assert_eq!(response.results[1].id, 2558);
1107 assert_eq!(response.results[1].slug, "dashboard-2");
1108 assert!(response.results[1].is_draft);
1109 }
1110
1111 #[test]
1112 fn test_build_dashboard_level_parameter_mappings_empty() {
1113 let result = build_dashboard_level_parameter_mappings(&[]);
1114 assert_eq!(result, serde_json::json!({}));
1115 }
1116
1117 #[test]
1118 fn test_build_dashboard_level_parameter_mappings_with_params() {
1119 let params = vec![
1120 Parameter {
1121 name: "channel".to_string(),
1122 title: "Channel".to_string(),
1123 param_type: "enum".to_string(),
1124 value: None,
1125 enum_options: None,
1126 query_id: None,
1127 multi_values_options: None,
1128 },
1129 Parameter {
1130 name: "date".to_string(),
1131 title: "Date".to_string(),
1132 param_type: "date".to_string(),
1133 value: None,
1134 enum_options: None,
1135 query_id: None,
1136 multi_values_options: None,
1137 },
1138 ];
1139
1140 let result = build_dashboard_level_parameter_mappings(¶ms);
1141
1142 let expected = serde_json::json!({
1143 "channel": {
1144 "mapTo": "channel",
1145 "name": "channel",
1146 "title": "",
1147 "type": "dashboard-level",
1148 "value": null,
1149 },
1150 "date": {
1151 "mapTo": "date",
1152 "name": "date",
1153 "title": "",
1154 "type": "dashboard-level",
1155 "value": null,
1156 },
1157 });
1158
1159 assert_eq!(result, expected);
1160 }
1161
1162 #[test]
1163 fn test_schedule_interval_as_integer() {
1164 let s: Schedule = serde_json::from_str(r#"{"interval": 3600}"#).unwrap();
1165 assert_eq!(s.interval, Some(3600));
1166 }
1167
1168 #[test]
1169 fn test_schedule_interval_as_string() {
1170 let s: Schedule = serde_json::from_str(r#"{"interval": "3600"}"#).unwrap();
1171 assert_eq!(s.interval, Some(3600));
1172 }
1173
1174 #[test]
1175 fn test_schedule_interval_null() {
1176 let s: Schedule = serde_json::from_str(r#"{"interval": null}"#).unwrap();
1177 assert_eq!(s.interval, None);
1178 }
1179
1180 #[test]
1181 fn test_schedule_interval_absent() {
1182 let s: Schedule = serde_json::from_str(r"{}").unwrap();
1183 assert_eq!(s.interval, None);
1184 }
1185
1186 #[test]
1187 fn test_column_type_null() {
1188 let c: Column = serde_json::from_str(
1189 r#"{"name": "revision", "friendly_name": "revision", "type": null}"#,
1190 )
1191 .unwrap();
1192 assert_eq!(c.name, "revision");
1193 assert_eq!(c.type_name, None);
1194 }
1195
1196 #[test]
1197 fn test_column_type_present() {
1198 let c: Column = serde_json::from_str(
1199 r#"{"name": "time", "friendly_name": "time", "type": "datetime"}"#,
1200 )
1201 .unwrap();
1202 assert_eq!(c.type_name, Some("datetime".to_string()));
1203 }
1204}