Skip to main content

vize_musea/
types.rs

1//! Type definitions for vize_musea.
2//!
3//! This module contains the core data structures for representing
4//! Art files (*.art.vue) and their components.
5//!
6//! All types are designed for zero-copy parsing with arena allocation.
7
8use serde::{Deserialize, Serialize};
9use vize_carton::{Bump, FxHashMap, String, ToCompactString, Vec as BumpVec};
10
11/// Parsed Art file descriptor.
12///
13/// Uses arena allocation for all collections.
14/// String data is borrowed directly from source.
15#[derive(Debug)]
16pub struct ArtDescriptor<'a> {
17    /// Source filename
18    pub filename: &'a str,
19
20    /// Original source code (borrowed)
21    pub source: &'a str,
22
23    /// Art metadata from `<art>` block attributes
24    pub metadata: ArtMetadata<'a>,
25
26    /// Variant definitions from `<variant>` blocks (arena-allocated)
27    pub variants: BumpVec<'a, ArtVariant<'a>>,
28
29    /// Script setup block (if present)
30    pub script_setup: Option<ArtScriptBlock<'a>>,
31
32    /// Regular script block (if present)
33    pub script: Option<ArtScriptBlock<'a>>,
34
35    /// Style blocks (arena-allocated)
36    pub styles: BumpVec<'a, ArtStyleBlock<'a>>,
37}
38
39/// Art metadata extracted from `<art>` block attributes.
40#[derive(Debug)]
41pub struct ArtMetadata<'a> {
42    /// Display title (required) - borrowed from source
43    pub title: &'a str,
44
45    /// Description text - borrowed from source
46    pub description: Option<&'a str>,
47
48    /// Path to the target component - borrowed from source
49    pub component: Option<&'a str>,
50
51    /// Category for organization - borrowed from source
52    pub category: Option<&'a str>,
53
54    /// Tags for filtering/searching (arena-allocated)
55    pub tags: BumpVec<'a, &'a str>,
56
57    /// Status indicator
58    pub status: ArtStatus,
59
60    /// Display order (lower = first)
61    pub order: Option<u32>,
62}
63
64/// Art status indicator.
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "lowercase")]
67pub enum ArtStatus {
68    /// Work in progress
69    Draft,
70    /// Ready for use
71    #[default]
72    Ready,
73    /// No longer recommended
74    Deprecated,
75}
76
77/// A single variant definition from `<variant>` block.
78#[derive(Debug)]
79pub struct ArtVariant<'a> {
80    /// Variant name (required) - borrowed from source
81    pub name: &'a str,
82
83    /// Template content inside `<variant>` - borrowed from source
84    pub template: &'a str,
85
86    /// Whether this is the default variant
87    pub is_default: bool,
88
89    /// Props/args override for this variant
90    pub args: FxHashMap<&'a str, serde_json::Value>,
91
92    /// Viewport configuration for VRT
93    pub viewport: Option<ViewportConfig>,
94
95    /// Skip this variant in VRT
96    pub skip_vrt: bool,
97
98    /// Source location (byte offsets for fast access)
99    pub loc: Option<SourceLocation>,
100}
101
102/// Script block in Art file.
103#[derive(Debug)]
104pub struct ArtScriptBlock<'a> {
105    /// Script content - borrowed from source
106    pub content: &'a str,
107
108    /// Language (ts, js, tsx, jsx)
109    pub lang: Option<&'a str>,
110
111    /// Whether this is a setup script
112    pub setup: bool,
113
114    /// Source location
115    pub loc: Option<SourceLocation>,
116}
117
118/// Style block in Art file.
119#[derive(Debug)]
120pub struct ArtStyleBlock<'a> {
121    /// Style content - borrowed from source
122    pub content: &'a str,
123
124    /// Language (css, scss, less, etc.)
125    pub lang: Option<&'a str>,
126
127    /// Whether scoped
128    pub scoped: bool,
129
130    /// Source location
131    pub loc: Option<SourceLocation>,
132}
133
134/// Viewport configuration for VRT.
135#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct ViewportConfig {
138    /// Width in pixels
139    pub width: u32,
140    /// Height in pixels
141    pub height: u32,
142    /// Device scale factor (default: 1.0)
143    pub device_scale_factor: Option<f32>,
144}
145
146/// Source location information (byte offsets for fast access).
147#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
148pub struct SourceLocation {
149    /// Start byte offset
150    pub start: u32,
151    /// End byte offset
152    pub end: u32,
153    /// Start line (1-indexed, computed lazily)
154    pub start_line: u32,
155    /// Start column (0-indexed)
156    pub start_column: u32,
157}
158
159/// Parse options for Art files.
160#[derive(Debug, Clone, Default)]
161pub struct ArtParseOptions {
162    /// Filename for error messages
163    pub filename: String,
164}
165
166/// Parse result containing descriptor or errors.
167pub type ArtParseResult<'a> = Result<ArtDescriptor<'a>, ArtParseError>;
168
169/// Error type for Art parsing.
170#[derive(Debug, Clone, thiserror::Error)]
171pub enum ArtParseError {
172    #[error("Missing required 'title' attribute in <art> block")]
173    MissingTitle,
174
175    #[error("Missing required 'name' attribute in <variant> block at line {line}")]
176    MissingVariantName { line: u32 },
177
178    #[error("No <art> block found in file")]
179    NoArtBlock,
180
181    #[error("Invalid attribute value for '{attr}': {message}")]
182    InvalidAttribute { attr: String, message: String },
183
184    #[error("Parse error at line {line}: {message}")]
185    ParseError { line: u32, message: String },
186}
187
188/// Output of Storybook CSF transformation.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct CsfOutput {
192    /// Generated CSF code
193    pub code: String,
194    /// Suggested filename (e.g., "Button.stories.ts")
195    pub filename: String,
196}
197
198impl<'a> ArtDescriptor<'a> {
199    /// Create a new descriptor with arena allocation.
200    #[inline]
201    pub fn new(allocator: &'a Bump, filename: &'a str, source: &'a str) -> Self {
202        Self {
203            filename,
204            source,
205            metadata: ArtMetadata::new(allocator),
206            variants: BumpVec::new_in(allocator),
207            script_setup: None,
208            script: None,
209            styles: BumpVec::new_in(allocator),
210        }
211    }
212
213    /// Get the default variant, or the first one if none is marked default.
214    #[inline]
215    pub fn default_variant(&self) -> Option<&ArtVariant<'a>> {
216        self.variants
217            .iter()
218            .find(|v| v.is_default)
219            .or_else(|| self.variants.first())
220    }
221}
222
223impl<'a> ArtMetadata<'a> {
224    /// Create default metadata with arena allocation.
225    #[inline]
226    pub fn new(allocator: &'a Bump) -> Self {
227        Self {
228            title: "",
229            description: None,
230            component: None,
231            category: None,
232            tags: BumpVec::new_in(allocator),
233            status: ArtStatus::default(),
234            order: None,
235        }
236    }
237}
238
239impl<'a> ArtVariant<'a> {
240    /// Create a new variant.
241    #[inline]
242    pub fn new(name: &'a str, template: &'a str) -> Self {
243        Self {
244            name,
245            template,
246            is_default: false,
247            args: FxHashMap::default(),
248            viewport: None,
249            skip_vrt: false,
250            loc: None,
251        }
252    }
253}
254
255impl Default for ViewportConfig {
256    #[inline]
257    fn default() -> Self {
258        Self {
259            width: 1280,
260            height: 720,
261            device_scale_factor: Some(1.0),
262        }
263    }
264}
265
266impl SourceLocation {
267    /// Create a new source location.
268    #[inline]
269    pub const fn new(start: u32, end: u32, start_line: u32, start_column: u32) -> Self {
270        Self {
271            start,
272            end,
273            start_line,
274            start_column,
275        }
276    }
277}
278
279// ============================================================================
280// Serialization support for WASM/NAPI
281// ============================================================================
282
283/// Owned version of ArtDescriptor for serialization.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct ArtDescriptorOwned {
287    pub filename: String,
288    pub source: String,
289    pub metadata: ArtMetadataOwned,
290    pub variants: Vec<ArtVariantOwned>,
291    pub script_setup: Option<ArtScriptBlockOwned>,
292    pub script: Option<ArtScriptBlockOwned>,
293    pub styles: Vec<ArtStyleBlockOwned>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct ArtMetadataOwned {
299    pub title: String,
300    pub description: Option<String>,
301    pub component: Option<String>,
302    pub category: Option<String>,
303    pub tags: Vec<String>,
304    pub status: ArtStatus,
305    pub order: Option<u32>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct ArtVariantOwned {
311    pub name: String,
312    pub template: String,
313    pub is_default: bool,
314    pub args: FxHashMap<String, serde_json::Value>,
315    pub viewport: Option<ViewportConfig>,
316    pub skip_vrt: bool,
317    pub loc: Option<SourceLocation>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct ArtScriptBlockOwned {
323    pub content: String,
324    pub lang: Option<String>,
325    pub setup: bool,
326    pub loc: Option<SourceLocation>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct ArtStyleBlockOwned {
332    pub content: String,
333    pub lang: Option<String>,
334    pub scoped: bool,
335    pub loc: Option<SourceLocation>,
336}
337
338impl<'a> ArtDescriptor<'a> {
339    /// Convert to owned version for serialization.
340    pub fn into_owned(self) -> ArtDescriptorOwned {
341        ArtDescriptorOwned {
342            filename: self.filename.to_compact_string(),
343            source: self.source.to_compact_string(),
344            metadata: self.metadata.into_owned(),
345            variants: self.variants.into_iter().map(|v| v.into_owned()).collect(),
346            script_setup: self.script_setup.map(|s| s.into_owned()),
347            script: self.script.map(|s| s.into_owned()),
348            styles: self.styles.into_iter().map(|s| s.into_owned()).collect(),
349        }
350    }
351}
352
353impl<'a> ArtMetadata<'a> {
354    /// Convert to owned version.
355    pub fn into_owned(self) -> ArtMetadataOwned {
356        ArtMetadataOwned {
357            title: self.title.to_compact_string(),
358            description: self.description.map(|s| s.to_compact_string()),
359            component: self.component.map(|s| s.to_compact_string()),
360            category: self.category.map(|s| s.to_compact_string()),
361            tags: self
362                .tags
363                .into_iter()
364                .map(|s| s.to_compact_string())
365                .collect(),
366            status: self.status,
367            order: self.order,
368        }
369    }
370}
371
372impl<'a> ArtVariant<'a> {
373    /// Convert to owned version.
374    pub fn into_owned(self) -> ArtVariantOwned {
375        ArtVariantOwned {
376            name: self.name.to_compact_string(),
377            template: self.template.to_compact_string(),
378            is_default: self.is_default,
379            args: self
380                .args
381                .into_iter()
382                .map(|(k, v)| (k.to_compact_string(), v))
383                .collect(),
384            viewport: self.viewport,
385            skip_vrt: self.skip_vrt,
386            loc: self.loc,
387        }
388    }
389}
390
391impl<'a> ArtScriptBlock<'a> {
392    /// Convert to owned version.
393    pub fn into_owned(self) -> ArtScriptBlockOwned {
394        ArtScriptBlockOwned {
395            content: self.content.to_compact_string(),
396            lang: self.lang.map(|s| s.to_compact_string()),
397            setup: self.setup,
398            loc: self.loc,
399        }
400    }
401}
402
403impl<'a> ArtStyleBlock<'a> {
404    /// Convert to owned version.
405    pub fn into_owned(self) -> ArtStyleBlockOwned {
406        ArtStyleBlockOwned {
407            content: self.content.to_compact_string(),
408            lang: self.lang.map(|s| s.to_compact_string()),
409            scoped: self.scoped,
410            loc: self.loc,
411        }
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::{ArtDescriptor, ArtStatus, ViewportConfig};
418    use vize_carton::Bump;
419
420    #[test]
421    fn test_art_descriptor_new() {
422        let allocator = Bump::new();
423        let desc = ArtDescriptor::new(&allocator, "test.art.vue", "<art></art>");
424        assert_eq!(desc.filename, "test.art.vue");
425        assert!(desc.variants.is_empty());
426    }
427
428    #[test]
429    fn test_art_status_default() {
430        assert_eq!(ArtStatus::default(), ArtStatus::Ready);
431    }
432
433    #[test]
434    fn test_viewport_default() {
435        let vp = ViewportConfig::default();
436        assert_eq!(vp.width, 1280);
437        assert_eq!(vp.height, 720);
438    }
439}