Skip to main content

stmo_cli/
models.rs

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(&params);
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}