metabase_api_rs/core/models/
card.rs

1use super::common::CardId;
2use super::parameter::{Parameter, ParameterMapping};
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Card type enumeration as per Metabase API specification
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
9#[serde(rename_all = "lowercase")]
10pub enum CardType {
11    #[default]
12    Question,
13    Metric,
14    Model,
15}
16
17/// Query type enumeration
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
19#[serde(rename_all = "lowercase")]
20pub enum QueryType {
21    #[default]
22    Query,
23    Native,
24}
25
26/// Represents a Metabase Card (saved question)
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Card {
29    pub id: Option<CardId>,
30    pub name: String,
31    /// Required field as per API specification
32    #[serde(rename = "type", default)]
33    pub card_type: CardType,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub description: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub collection_id: Option<i32>,
38    #[serde(default = "default_display")]
39    pub display: String,
40    #[serde(default)]
41    pub visualization_settings: Value,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub dataset_query: Option<Value>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub created_at: Option<DateTime<Utc>>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub updated_at: Option<DateTime<Utc>>,
48    #[serde(default)]
49    pub archived: bool,
50    #[serde(default)]
51    pub enable_embedding: bool,
52    #[serde(default)]
53    pub embedding_params: Value,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub result_metadata: Option<Value>,
56    // Fields verified from Metabase API documentation
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub creator_id: Option<i32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub database_id: Option<i32>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub table_id: Option<i32>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub query_type: Option<QueryType>,
65    // Additional fields from API specification
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub entity_id: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub cache_ttl: Option<i32>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub collection_position: Option<i32>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub dashboard_tab_id: Option<i32>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub dashboard_id: Option<i32>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub public_uuid: Option<String>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub made_public_by_id: Option<i32>,
80    #[serde(default)]
81    pub parameters: Vec<Parameter>,
82    #[serde(default)]
83    pub parameter_mappings: Vec<ParameterMapping>,
84}
85
86fn default_display() -> String {
87    "table".to_string()
88}
89
90impl Card {
91    /// Create a new Card with minimal required fields
92    pub fn new(id: Option<CardId>, name: String, card_type: CardType) -> Self {
93        Self {
94            id,
95            name,
96            card_type,
97            description: None,
98            collection_id: None,
99            display: default_display(),
100            visualization_settings: Value::Object(serde_json::Map::new()),
101            dataset_query: None,
102            created_at: None,
103            updated_at: None,
104            archived: false,
105            enable_embedding: false,
106            embedding_params: Value::Object(serde_json::Map::new()),
107            result_metadata: None,
108            creator_id: None,
109            database_id: None,
110            table_id: None,
111            query_type: None,
112            entity_id: None,
113            cache_ttl: None,
114            collection_position: None,
115            dashboard_tab_id: None,
116            dashboard_id: None,
117            public_uuid: None,
118            made_public_by_id: None,
119            parameters: Vec::new(),
120            parameter_mappings: Vec::new(),
121        }
122    }
123
124    // Getters
125    pub fn id(&self) -> Option<CardId> {
126        self.id
127    }
128
129    pub fn name(&self) -> &str {
130        &self.name
131    }
132
133    pub fn card_type(&self) -> &CardType {
134        &self.card_type
135    }
136
137    pub fn description(&self) -> Option<&str> {
138        self.description.as_deref()
139    }
140
141    pub fn collection_id(&self) -> Option<i32> {
142        self.collection_id
143    }
144
145    pub fn display(&self) -> &str {
146        &self.display
147    }
148
149    pub fn visualization_settings(&self) -> &Value {
150        &self.visualization_settings
151    }
152
153    pub fn dataset_query(&self) -> Option<&Value> {
154        self.dataset_query.as_ref()
155    }
156
157    pub fn archived(&self) -> bool {
158        self.archived
159    }
160
161    pub fn enable_embedding(&self) -> bool {
162        self.enable_embedding
163    }
164}
165
166/// Builder for creating Card instances
167pub struct CardBuilder {
168    id: Option<CardId>,
169    name: String,
170    card_type: CardType,
171    description: Option<String>,
172    collection_id: Option<i32>,
173    display: String,
174    visualization_settings: Value,
175    dataset_query: Option<Value>,
176    created_at: Option<DateTime<Utc>>,
177    updated_at: Option<DateTime<Utc>>,
178    archived: bool,
179    enable_embedding: bool,
180    embedding_params: Value,
181    result_metadata: Option<Value>,
182    creator_id: Option<i32>,
183    database_id: Option<i32>,
184    table_id: Option<i32>,
185    query_type: Option<QueryType>,
186    entity_id: Option<String>,
187    cache_ttl: Option<i32>,
188    collection_position: Option<i32>,
189    dashboard_tab_id: Option<i32>,
190    dashboard_id: Option<i32>,
191    public_uuid: Option<String>,
192    made_public_by_id: Option<i32>,
193    parameters: Vec<Parameter>,
194    parameter_mappings: Vec<ParameterMapping>,
195}
196
197impl CardBuilder {
198    /// Create a new CardBuilder with required fields
199    pub fn new(id: Option<CardId>, name: String, card_type: CardType) -> Self {
200        Self {
201            id,
202            name,
203            card_type,
204            description: None,
205            collection_id: None,
206            display: default_display(),
207            visualization_settings: Value::Object(serde_json::Map::new()),
208            dataset_query: None,
209            created_at: None,
210            updated_at: None,
211            archived: false,
212            enable_embedding: false,
213            embedding_params: Value::Object(serde_json::Map::new()),
214            result_metadata: None,
215            creator_id: None,
216            database_id: None,
217            table_id: None,
218            query_type: None,
219            entity_id: None,
220            cache_ttl: None,
221            collection_position: None,
222            dashboard_tab_id: None,
223            dashboard_id: None,
224            public_uuid: None,
225            made_public_by_id: None,
226            parameters: Vec::new(),
227            parameter_mappings: Vec::new(),
228        }
229    }
230
231    /// Create a new CardBuilder for creating a new card (ID will be assigned by server)
232    pub fn new_card(name: impl Into<String>) -> Self {
233        Self::new(None, name.into(), CardType::default())
234    }
235
236    pub fn description<S: Into<String>>(mut self, desc: S) -> Self {
237        self.description = Some(desc.into());
238        self
239    }
240
241    pub fn collection_id(mut self, id: i32) -> Self {
242        self.collection_id = Some(id);
243        self
244    }
245
246    pub fn display<S: Into<String>>(mut self, display: S) -> Self {
247        self.display = display.into();
248        self
249    }
250
251    pub fn visualization_settings(mut self, settings: Value) -> Self {
252        self.visualization_settings = settings;
253        self
254    }
255
256    pub fn dataset_query(mut self, query: Value) -> Self {
257        self.dataset_query = Some(query);
258        self
259    }
260
261    pub fn created_at(mut self, dt: DateTime<Utc>) -> Self {
262        self.created_at = Some(dt);
263        self
264    }
265
266    pub fn updated_at(mut self, dt: DateTime<Utc>) -> Self {
267        self.updated_at = Some(dt);
268        self
269    }
270
271    pub fn archived(mut self, archived: bool) -> Self {
272        self.archived = archived;
273        self
274    }
275
276    pub fn enable_embedding(mut self, enable: bool) -> Self {
277        self.enable_embedding = enable;
278        self
279    }
280
281    pub fn embedding_params(mut self, params: Value) -> Self {
282        self.embedding_params = params;
283        self
284    }
285
286    pub fn result_metadata(mut self, metadata: Value) -> Self {
287        self.result_metadata = Some(metadata);
288        self
289    }
290
291    pub fn card_type(mut self, card_type: CardType) -> Self {
292        self.card_type = card_type;
293        self
294    }
295
296    pub fn entity_id<S: Into<String>>(mut self, id: S) -> Self {
297        self.entity_id = Some(id.into());
298        self
299    }
300
301    pub fn cache_ttl(mut self, ttl: i32) -> Self {
302        self.cache_ttl = Some(ttl);
303        self
304    }
305
306    pub fn collection_position(mut self, position: i32) -> Self {
307        self.collection_position = Some(position);
308        self
309    }
310
311    pub fn dashboard_tab_id(mut self, id: i32) -> Self {
312        self.dashboard_tab_id = Some(id);
313        self
314    }
315
316    pub fn dashboard_id(mut self, id: i32) -> Self {
317        self.dashboard_id = Some(id);
318        self
319    }
320
321    pub fn parameters(mut self, params: Vec<Parameter>) -> Self {
322        self.parameters = params;
323        self
324    }
325
326    pub fn parameter_mappings(mut self, mappings: Vec<ParameterMapping>) -> Self {
327        self.parameter_mappings = mappings;
328        self
329    }
330
331    pub fn creator_id(mut self, id: i32) -> Self {
332        self.creator_id = Some(id);
333        self
334    }
335
336    pub fn database_id(mut self, id: i32) -> Self {
337        self.database_id = Some(id);
338        self
339    }
340
341    pub fn table_id(mut self, id: i32) -> Self {
342        self.table_id = Some(id);
343        self
344    }
345
346    pub fn query_type(mut self, query_type: QueryType) -> Self {
347        self.query_type = Some(query_type);
348        self
349    }
350
351    pub fn public_uuid<S: Into<String>>(mut self, uuid: S) -> Self {
352        self.public_uuid = Some(uuid.into());
353        self
354    }
355
356    pub fn made_public_by_id(mut self, id: i32) -> Self {
357        self.made_public_by_id = Some(id);
358        self
359    }
360
361    /// Build the Card instance
362    pub fn build(self) -> Card {
363        Card {
364            id: self.id,
365            name: self.name,
366            card_type: self.card_type,
367            description: self.description,
368            collection_id: self.collection_id,
369            display: self.display,
370            visualization_settings: self.visualization_settings,
371            dataset_query: self.dataset_query,
372            created_at: self.created_at,
373            updated_at: self.updated_at,
374            archived: self.archived,
375            enable_embedding: self.enable_embedding,
376            embedding_params: self.embedding_params,
377            result_metadata: self.result_metadata,
378            creator_id: self.creator_id,
379            database_id: self.database_id,
380            table_id: self.table_id,
381            query_type: self.query_type,
382            entity_id: self.entity_id,
383            cache_ttl: self.cache_ttl,
384            collection_position: self.collection_position,
385            dashboard_tab_id: self.dashboard_tab_id,
386            dashboard_id: self.dashboard_id,
387            public_uuid: self.public_uuid,
388            made_public_by_id: self.made_public_by_id,
389            parameters: self.parameters,
390            parameter_mappings: self.parameter_mappings,
391        }
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::core::models::parameter::{ParameterTarget, VariableTarget};
399
400    #[test]
401    fn test_card_creation() {
402        let card = Card::new(Some(CardId(1)), "Test Card".to_string(), CardType::Question);
403
404        assert_eq!(card.id(), Some(CardId(1)));
405        assert_eq!(card.name(), "Test Card");
406        assert_eq!(card.card_type(), &CardType::Question);
407        assert!(card.description().is_none());
408        assert!(card.collection_id().is_none());
409    }
410
411    #[test]
412    fn test_card_with_builder() {
413        let card = CardBuilder::new(
414            Some(CardId(2)),
415            "Builder Card".to_string(),
416            CardType::Metric,
417        )
418        .description("A test card created with builder")
419        .collection_id(10)
420        .display("table")
421        .cache_ttl(300)
422        .build();
423
424        assert_eq!(card.id(), Some(CardId(2)));
425        assert_eq!(card.name(), "Builder Card");
426        assert_eq!(card.card_type(), &CardType::Metric);
427        assert_eq!(card.description(), Some("A test card created with builder"));
428        assert_eq!(card.collection_id(), Some(10));
429        assert_eq!(card.display(), "table");
430        assert_eq!(card.cache_ttl, Some(300));
431    }
432
433    #[test]
434    fn test_card_deserialize_from_json() {
435        let json_str = r#"{
436            "id": 123,
437            "name": "Sales Dashboard Card",
438            "type": "question",
439            "description": "Monthly sales overview",
440            "collection_id": 5,
441            "display": "line",
442            "visualization_settings": {
443                "graph.dimensions": ["date"],
444                "graph.metrics": ["count"]
445            },
446            "created_at": "2023-08-08T10:00:00Z",
447            "updated_at": "2023-08-08T12:00:00Z",
448            "archived": false,
449            "enable_embedding": true,
450            "cache_ttl": 600,
451            "entity_id": "abc123",
452            "creator_id": 10,
453            "database_id": 1,
454            "table_id": 42,
455            "query_type": "native",
456            "public_uuid": "1234-5678-9012",
457            "made_public_by_id": 15
458        }"#;
459
460        let card: Card = serde_json::from_str(json_str).unwrap();
461
462        assert_eq!(card.id(), Some(CardId(123)));
463        assert_eq!(card.name(), "Sales Dashboard Card");
464        assert_eq!(card.card_type(), &CardType::Question);
465        assert_eq!(card.description(), Some("Monthly sales overview"));
466        assert_eq!(card.collection_id(), Some(5));
467        assert_eq!(card.display(), "line");
468        assert!(!card.archived());
469        assert!(card.enable_embedding());
470        assert_eq!(card.cache_ttl, Some(600));
471        assert_eq!(card.entity_id, Some("abc123".to_string()));
472        assert_eq!(card.creator_id, Some(10));
473        assert_eq!(card.database_id, Some(1));
474        assert_eq!(card.table_id, Some(42));
475        assert_eq!(card.query_type, Some(QueryType::Native));
476        assert_eq!(card.public_uuid, Some("1234-5678-9012".to_string()));
477        assert_eq!(card.made_public_by_id, Some(15));
478    }
479
480    #[test]
481    fn test_card_type_serialization() {
482        assert_eq!(
483            serde_json::to_string(&CardType::Question).unwrap(),
484            r#""question""#
485        );
486        assert_eq!(
487            serde_json::to_string(&CardType::Metric).unwrap(),
488            r#""metric""#
489        );
490        assert_eq!(
491            serde_json::to_string(&CardType::Model).unwrap(),
492            r#""model""#
493        );
494    }
495
496    #[test]
497    fn test_query_type_serialization() {
498        assert_eq!(
499            serde_json::to_string(&QueryType::Query).unwrap(),
500            r#""query""#
501        );
502        assert_eq!(
503            serde_json::to_string(&QueryType::Native).unwrap(),
504            r#""native""#
505        );
506    }
507
508    #[test]
509    fn test_card_with_new_fields() {
510        let card = CardBuilder::new(
511            Some(CardId(100)),
512            "Analytics Card".to_string(),
513            CardType::Question,
514        )
515        .description("Advanced analytics")
516        .database_id(1)
517        .table_id(5)
518        .query_type(QueryType::Native)
519        .creator_id(42)
520        .public_uuid("uuid-1234")
521        .made_public_by_id(10)
522        .build();
523
524        assert_eq!(card.database_id, Some(1));
525        assert_eq!(card.table_id, Some(5));
526        assert_eq!(card.query_type, Some(QueryType::Native));
527        assert_eq!(card.creator_id, Some(42));
528        assert_eq!(card.public_uuid, Some("uuid-1234".to_string()));
529        assert_eq!(card.made_public_by_id, Some(10));
530    }
531
532    #[test]
533    fn test_card_with_parameters() {
534        let parameter = Parameter {
535            id: "date_param".to_string(),
536            param_type: "date/relative".to_string(),
537            name: "Date Filter".to_string(),
538            slug: "date".to_string(),
539            default: Some(serde_json::json!("past7days")),
540            required: false,
541            options: None,
542            values_source_type: None,
543            values_source_config: None,
544        };
545
546        let parameter_mapping = ParameterMapping {
547            parameter_id: "date_param".to_string(),
548            card_id: 100,
549            target: ParameterTarget::Variable(VariableTarget {
550                target_type: "variable".to_string(),
551                id: "start_date".to_string(),
552            }),
553        };
554
555        let card = CardBuilder::new(
556            Some(CardId(100)),
557            "Parameterized Card".to_string(),
558            CardType::Question,
559        )
560        .parameters(vec![parameter.clone()])
561        .parameter_mappings(vec![parameter_mapping.clone()])
562        .build();
563
564        assert_eq!(card.parameters.len(), 1);
565        assert_eq!(card.parameters[0].id, "date_param");
566        assert_eq!(card.parameter_mappings.len(), 1);
567        assert_eq!(card.parameter_mappings[0].parameter_id, "date_param");
568    }
569}