Skip to main content

ryo_mutations/
serializable.rs

1//! Serializable mutation representation for storage and replay.
2//!
3//! This module provides a serializable enum that captures all mutation data,
4//! enabling exact replay of recorded sessions.
5//!
6//! ## Design
7//!
8//! The [`SerializableMutation`] enum mirrors all mutation types but uses
9//! simple, serializable fields. Each mutation implements [`ToSerializable`]
10//! to convert to this format.
11//!
12//! ## Future Extensions
13//!
14//! To add a new mutation type:
15//! 1. Add a variant to [`SerializableMutation`]
16//! 2. Implement [`ToSerializable`] for the mutation
17//! 3. Add reconstruction in [`SerializableMutation::to_mutation`]
18
19use ryo_source::pure::PureVis;
20use serde::{Deserialize, Serialize};
21
22/// Serializable representation of any mutation.
23///
24/// This enum captures all data needed to reconstruct and replay a mutation.
25/// Currently supports basic mutations; complex mutations store description only.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(tag = "type")]
28pub enum SerializableMutation {
29    // =========================================================================
30    // Basic Mutations (fully replayable)
31    // =========================================================================
32    /// Rename a symbol
33    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
34    Rename { symbol_id: String, to: String },
35
36    /// Change visibility of an item
37    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
38    ChangeVisibility {
39        symbol_id: String,
40        visibility: String,
41    },
42
43    /// Add a function
44    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
45    AddFunction {
46        symbol_id: String,
47        name: String,
48        params: Vec<(String, String)>,
49        return_type: Option<String>,
50        body: String,
51        is_pub: bool,
52    },
53
54    /// Remove a function
55    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
56    RemoveFunction { symbol_id: String },
57
58    /// Add a struct
59    AddStruct {
60        name: String,
61        fields: Vec<(String, String, bool)>, // (name, type, is_pub)
62        is_pub: bool,
63    },
64
65    /// Remove a struct
66    RemoveStruct { name: String },
67
68    /// Add a field to a struct
69    AddField {
70        struct_name: String,
71        field_name: String,
72        field_type: String,
73        is_pub: bool,
74    },
75
76    /// Remove a field from a struct
77    RemoveField {
78        struct_name: String,
79        field_name: String,
80    },
81
82    /// Add a use statement
83    AddUse { path: String },
84
85    /// Remove a use statement
86    RemoveUse { path: String },
87
88    /// Add derive macros
89    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
90    AddDerive {
91        symbol_id: String,
92        derives: Vec<String>,
93    },
94
95    /// Remove derive macros
96    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
97    RemoveDerive {
98        symbol_id: String,
99        derives: Vec<String>,
100    },
101
102    /// Add an enum
103    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
104    AddEnum {
105        symbol_id: String,
106        name: String,
107        variants: Vec<String>,
108        derives: Vec<String>,
109        is_pub: bool,
110    },
111
112    /// Remove an enum
113    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
114    RemoveEnum { symbol_id: String },
115
116    /// Add a variant to an enum (unit variant only)
117    AddVariant {
118        enum_name: String,
119        variant_name: String,
120    },
121
122    /// Remove a variant from an enum
123    RemoveVariant {
124        enum_name: String,
125        variant_name: String,
126    },
127
128    /// Add a match arm to a function's match expression
129    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
130    AddMatchArm {
131        symbol_id: String,
132        enum_name: String,
133        pattern: String,
134        body: String,
135    },
136
137    /// Remove a match arm from a function's match expression
138    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
139    RemoveMatchArm {
140        symbol_id: String,
141        enum_name: String,
142        pattern: String,
143    },
144
145    /// Replace a match arm (pattern + body) in a function's match expression
146    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
147    ReplaceMatchArm {
148        symbol_id: String,
149        enum_name: String,
150        old_pattern: String,
151        new_pattern: String,
152        new_body: String,
153    },
154
155    /// Add a field to all struct literals of a given type
156    AddStructLiteralField {
157        struct_name: String,
158        field_name: String,
159        value: String,
160    },
161
162    /// Add a const
163    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
164    AddConst {
165        symbol_id: String,
166        name: String,
167        ty: String,
168        value: String,
169        is_pub: bool,
170    },
171
172    /// Remove a const
173    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
174    RemoveConst { symbol_id: String },
175
176    /// Add a type alias
177    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
178    AddTypeAlias {
179        symbol_id: String,
180        name: String,
181        ty: String,
182        is_pub: bool,
183    },
184
185    /// Remove a type alias
186    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
187    RemoveTypeAlias { symbol_id: String },
188
189    /// Remove a trait
190    RemoveTrait { name: String },
191
192    /// Add an impl block
193    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
194    AddImpl {
195        symbol_id: String,
196        target: String,
197        trait_name: Option<String>,
198    },
199
200    /// Remove an impl block
201    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
202    RemoveImpl { symbol_id: String },
203
204    /// Add an item from raw source code content
205    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
206    AddItem { symbol_id: String, content: String },
207
208    /// Remove an item by SymbolId
209    /// NOTE: symbol_id is stored as string format "indexVversion" (e.g., "165v1")
210    RemoveItem {
211        symbol_id: String,
212        item_kind: String,
213    },
214
215    // =========================================================================
216    // Complex Mutations (stored for reference, may not be fully replayable)
217    // =========================================================================
218    /// Generic mutation that stores description only
219    /// Used for complex mutations that can't be easily serialized
220    Generic {
221        mutation_type: String,
222        description: String,
223    },
224}
225
226impl SerializableMutation {
227    /// Get the mutation type name.
228    pub fn mutation_type(&self) -> &str {
229        match self {
230            Self::Rename { .. } => "Rename",
231            Self::ChangeVisibility { .. } => "ChangeVisibility",
232            Self::AddFunction { .. } => "AddFunction",
233            Self::RemoveFunction { .. } => "RemoveFunction",
234            Self::AddStruct { .. } => "AddStruct",
235            Self::RemoveStruct { .. } => "RemoveStruct",
236            Self::AddField { .. } => "AddField",
237            Self::RemoveField { .. } => "RemoveField",
238            Self::AddUse { .. } => "AddUse",
239            Self::RemoveUse { .. } => "RemoveUse",
240            Self::AddDerive { .. } => "AddDerive",
241            Self::RemoveDerive { .. } => "RemoveDerive",
242            Self::AddEnum { .. } => "AddEnum",
243            Self::RemoveEnum { .. } => "RemoveEnum",
244            Self::AddVariant { .. } => "AddVariant",
245            Self::RemoveVariant { .. } => "RemoveVariant",
246            Self::AddMatchArm { .. } => "AddMatchArm",
247            Self::RemoveMatchArm { .. } => "RemoveMatchArm",
248            Self::ReplaceMatchArm { .. } => "ReplaceMatchArm",
249            Self::AddStructLiteralField { .. } => "AddStructLiteralField",
250            Self::AddConst { .. } => "AddConst",
251            Self::RemoveConst { .. } => "RemoveConst",
252            Self::AddTypeAlias { .. } => "AddTypeAlias",
253            Self::RemoveTypeAlias { .. } => "RemoveTypeAlias",
254            Self::RemoveTrait { .. } => "RemoveTrait",
255            Self::AddImpl { .. } => "AddImpl",
256            Self::RemoveImpl { .. } => "RemoveImpl",
257            Self::AddItem { .. } => "AddItem",
258            Self::RemoveItem { .. } => "RemoveItem",
259            Self::Generic { mutation_type, .. } => mutation_type,
260        }
261    }
262
263    /// Convert to a boxed Mutation trait object.
264    ///
265    /// Returns None for Generic variants or unsupported mutations.
266    pub fn to_mutation(&self) -> Option<Box<dyn super::Mutation>> {
267        use super::*;
268
269        match self {
270            Self::Rename { symbol_id, to } => {
271                let id = ryo_symbol::SymbolId::parse(symbol_id)?;
272                Some(Box::new(RenameMutation::new(id, to)))
273            }
274
275            Self::ChangeVisibility {
276                symbol_id,
277                visibility,
278            } => {
279                let id = ryo_symbol::SymbolId::parse(symbol_id)?;
280                let vis = parse_visibility(visibility);
281                Some(Box::new(ChangeVisibilityMutation::new(id, vis)))
282            }
283
284            // AddFunction requires SymbolId which cannot be deserialized from JSON.
285            // Use MutationSpec -> Converter flow instead.
286            Self::AddFunction { .. } => None,
287
288            // RemoveFunction requires SymbolId which cannot be deserialized from JSON.
289            // Use MutationSpec -> Converter flow instead.
290            Self::RemoveFunction { .. } => None,
291
292            // AddStruct requires SymbolId (parent) which cannot be deserialized from JSON.
293            // Use MutationSpec -> Converter flow instead.
294            Self::AddStruct { .. } => None,
295
296            // RemoveStruct requires SymbolId which cannot be deserialized from JSON.
297            // Use MutationSpec -> Converter flow instead.
298            Self::RemoveStruct { .. } => None,
299
300            // AddField requires SymbolId which cannot be deserialized from JSON.
301            // Use MutationSpec -> Converter flow instead.
302            Self::AddField { .. } => None,
303
304            // RemoveField requires SymbolId which cannot be deserialized from JSON.
305            // Use MutationSpec -> Converter flow instead.
306            Self::RemoveField { .. } => None,
307
308            Self::AddUse { path } => Some(Box::new(AddUseMutation::new(path))),
309
310            Self::RemoveUse { path } => Some(Box::new(RemoveUseMutation::new(path))),
311
312            // AddDerive requires SymbolId which cannot be deserialized from JSON.
313            // Use MutationSpec -> Converter flow instead.
314            Self::AddDerive { .. } => None,
315
316            // RemoveDerive requires SymbolId which cannot be deserialized from JSON.
317            // Use MutationSpec -> Converter flow instead.
318            Self::RemoveDerive { .. } => None,
319
320            // AddEnum requires SymbolId which cannot be deserialized from JSON.
321            // Use MutationSpec -> Converter flow instead.
322            Self::AddEnum { .. } => None,
323
324            // RemoveEnum requires SymbolId which cannot be deserialized from JSON.
325            // Use MutationSpec -> Converter flow instead.
326            Self::RemoveEnum { .. } => None,
327
328            // AddVariant requires SymbolId which SerializableMutation doesn't have
329            Self::AddVariant { .. } => None,
330
331            // RemoveVariant requires SymbolId which SerializableMutation doesn't have
332            Self::RemoveVariant { .. } => None,
333
334            // AddMatchArm requires SymbolId which cannot be deserialized from JSON.
335            // Use MutationSpec -> Converter flow instead.
336            Self::AddMatchArm { .. } => None,
337
338            // RemoveMatchArm requires SymbolId which cannot be deserialized from JSON.
339            // Use MutationSpec -> Converter flow instead.
340            Self::RemoveMatchArm { .. } => None,
341
342            // ReplaceMatchArm requires SymbolId which cannot be deserialized from JSON.
343            // Use MutationSpec -> Converter flow instead.
344            Self::ReplaceMatchArm { .. } => None,
345
346            // AddStructLiteralField requires SymbolId which cannot be deserialized from JSON.
347            // Use MutationSpec -> Converter flow instead.
348            Self::AddStructLiteralField { .. } => None,
349
350            // AddConst requires SymbolId which cannot be deserialized from JSON.
351            // Use MutationSpec -> Converter flow instead.
352            Self::AddConst { .. } => None,
353
354            // RemoveConst requires SymbolId which cannot be deserialized from JSON.
355            // Use MutationSpec -> Converter flow instead.
356            Self::RemoveConst { .. } => None,
357
358            // AddTypeAlias requires SymbolId which cannot be deserialized from JSON.
359            // Use MutationSpec -> Converter flow instead.
360            Self::AddTypeAlias { .. } => None,
361
362            // RemoveTypeAlias requires SymbolId which cannot be deserialized from JSON.
363            // Use MutationSpec -> Converter flow instead.
364            Self::RemoveTypeAlias { .. } => None,
365
366            // RemoveTrait requires SymbolId which cannot be deserialized from JSON.
367            // Use MutationSpec -> Converter flow instead.
368            Self::RemoveTrait { .. } => None,
369
370            // AddImpl requires SymbolId which cannot be deserialized from JSON.
371            // Use MutationSpec -> Converter flow instead.
372            Self::AddImpl { .. } => None,
373
374            // RemoveImpl requires SymbolId which cannot be deserialized from JSON.
375            // Use MutationSpec -> Converter flow instead.
376            Self::RemoveImpl { .. } => None,
377
378            // AddItem requires SymbolId which cannot be deserialized from JSON.
379            // Use MutationSpec -> Converter flow instead.
380            Self::AddItem { .. } => None,
381
382            // RemoveItem requires SymbolId which cannot be deserialized from JSON.
383            // Use MutationSpec -> Converter flow instead.
384            Self::RemoveItem { .. } => None,
385
386            Self::Generic { .. } => None,
387        }
388    }
389
390    /// Convert to JSON Value for storage.
391    pub fn to_json(&self) -> serde_json::Value {
392        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
393    }
394
395    /// Parse from JSON Value.
396    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
397        serde_json::from_value(value.clone()).ok()
398    }
399}
400
401/// Trait for converting a mutation to its serializable form.
402pub trait ToSerializable {
403    fn to_serializable(&self) -> SerializableMutation;
404}
405
406// ============================================================================
407// Helper functions
408// ============================================================================
409
410fn parse_visibility(s: &str) -> PureVis {
411    match s {
412        "pub" => PureVis::Public,
413        "pub(crate)" => PureVis::Crate,
414        "pub(super)" => PureVis::Super,
415        "" | "private" => PureVis::Private,
416        other if other.starts_with("pub(in ") => {
417            let path = other
418                .strip_prefix("pub(in ")
419                .and_then(|s| s.strip_suffix(')'));
420            if let Some(p) = path {
421                PureVis::In(p.to_string())
422            } else {
423                PureVis::Private
424            }
425        }
426        _ => PureVis::Private,
427    }
428}
429
430fn visibility_to_string(vis: &PureVis) -> String {
431    match vis {
432        PureVis::Public => "pub".to_string(),
433        PureVis::Crate => "pub(crate)".to_string(),
434        PureVis::Super => "pub(super)".to_string(),
435        PureVis::Private => String::new(),
436        PureVis::In(path) => format!("pub(in {})", path),
437    }
438}
439
440// ============================================================================
441// ToSerializable implementations
442// ============================================================================
443
444impl ToSerializable for super::RenameMutation {
445    fn to_serializable(&self) -> SerializableMutation {
446        SerializableMutation::Rename {
447            symbol_id: format!("{:?}", self.symbol_id),
448            to: self.to.clone(),
449        }
450    }
451}
452
453impl ToSerializable for super::ChangeVisibilityMutation {
454    fn to_serializable(&self) -> SerializableMutation {
455        SerializableMutation::ChangeVisibility {
456            symbol_id: format!("{:?}", self.symbol_id),
457            visibility: visibility_to_string(&self.to),
458        }
459    }
460}
461
462impl ToSerializable for super::AddFunctionMutation {
463    fn to_serializable(&self) -> SerializableMutation {
464        SerializableMutation::AddFunction {
465            symbol_id: format!("{}", self.parent),
466            name: self.name.clone(),
467            params: self.params.clone(),
468            return_type: self.return_type.clone(),
469            body: self.body.clone(),
470            is_pub: self.is_pub,
471        }
472    }
473}
474
475impl ToSerializable for super::RemoveFunctionMutation {
476    fn to_serializable(&self) -> SerializableMutation {
477        SerializableMutation::RemoveFunction {
478            symbol_id: format!("{}", self.symbol_id),
479        }
480    }
481}
482
483impl ToSerializable for super::AddStructMutation {
484    fn to_serializable(&self) -> SerializableMutation {
485        SerializableMutation::AddStruct {
486            name: self.name.clone(),
487            fields: self
488                .fields
489                .iter()
490                .map(|f| (f.name.clone(), f.ty.clone(), f.is_pub))
491                .collect(),
492            is_pub: self.is_pub,
493        }
494    }
495}
496
497impl ToSerializable for super::RemoveStructMutation {
498    fn to_serializable(&self) -> SerializableMutation {
499        SerializableMutation::RemoveStruct {
500            name: self.struct_id.to_string(),
501        }
502    }
503}
504
505impl ToSerializable for super::AddFieldMutation {
506    fn to_serializable(&self) -> SerializableMutation {
507        SerializableMutation::AddField {
508            // SymbolId cannot be converted back to struct_name without registry context.
509            // Use Debug format for logging/debugging purposes only.
510            struct_name: format!("{:?}", self.struct_id),
511            field_name: self.field_name.clone(),
512            field_type: self.field_type.clone(),
513            is_pub: self.is_pub,
514        }
515    }
516}
517
518impl ToSerializable for super::RemoveFieldMutation {
519    fn to_serializable(&self) -> SerializableMutation {
520        SerializableMutation::RemoveField {
521            // SymbolId cannot be converted back to struct_name without registry context.
522            struct_name: format!("{:?}", self.struct_id),
523            field_name: self.field_name.clone(),
524        }
525    }
526}
527
528impl ToSerializable for super::AddUseMutation {
529    fn to_serializable(&self) -> SerializableMutation {
530        SerializableMutation::AddUse {
531            path: self.path.clone(),
532        }
533    }
534}
535
536impl ToSerializable for super::RemoveUseMutation {
537    fn to_serializable(&self) -> SerializableMutation {
538        SerializableMutation::RemoveUse {
539            path: self.path.clone(),
540        }
541    }
542}
543
544impl ToSerializable for super::AddDeriveMutation {
545    fn to_serializable(&self) -> SerializableMutation {
546        SerializableMutation::AddDerive {
547            symbol_id: format!("{:?}", self.symbol_id),
548            derives: self.derives.clone(),
549        }
550    }
551}
552
553impl ToSerializable for super::RemoveDeriveMutation {
554    fn to_serializable(&self) -> SerializableMutation {
555        SerializableMutation::RemoveDerive {
556            symbol_id: format!("{:?}", self.symbol_id),
557            derives: self.derives.clone(),
558        }
559    }
560}
561
562impl ToSerializable for super::AddEnumMutation {
563    fn to_serializable(&self) -> SerializableMutation {
564        SerializableMutation::AddEnum {
565            symbol_id: format!("{:?}", self.parent),
566            name: self.name.clone(),
567            // Convert (String, PureFields) to just String (variant name)
568            variants: self.variants.iter().map(|(name, _)| name.clone()).collect(),
569            derives: self.derives.clone(),
570            is_pub: self.is_pub,
571        }
572    }
573}
574
575impl ToSerializable for super::RemoveEnumMutation {
576    fn to_serializable(&self) -> SerializableMutation {
577        SerializableMutation::RemoveEnum {
578            symbol_id: format!("{:?}", self.symbol_id),
579        }
580    }
581}
582
583impl ToSerializable for super::AddVariantMutation {
584    fn to_serializable(&self) -> SerializableMutation {
585        SerializableMutation::AddVariant {
586            enum_name: format!("{:?}", self.enum_id),
587            variant_name: self.variant_name.clone(),
588        }
589    }
590}
591
592impl ToSerializable for super::RemoveVariantMutation {
593    fn to_serializable(&self) -> SerializableMutation {
594        SerializableMutation::RemoveVariant {
595            enum_name: format!("{:?}", self.enum_id),
596            variant_name: self.variant_name.clone(),
597        }
598    }
599}
600
601impl ToSerializable for super::AddMatchArmMutation {
602    fn to_serializable(&self) -> SerializableMutation {
603        SerializableMutation::AddMatchArm {
604            symbol_id: self.function_id.to_string(),
605            enum_name: self.enum_name.clone(),
606            pattern: self.pattern.clone(),
607            body: self.body.clone(),
608        }
609    }
610}
611
612impl ToSerializable for super::RemoveMatchArmMutation {
613    fn to_serializable(&self) -> SerializableMutation {
614        SerializableMutation::RemoveMatchArm {
615            symbol_id: self.function_id.to_string(),
616            enum_name: self.enum_name.clone(),
617            pattern: self.pattern.clone(),
618        }
619    }
620}
621
622impl ToSerializable for super::ReplaceMatchArmMutation {
623    fn to_serializable(&self) -> SerializableMutation {
624        SerializableMutation::ReplaceMatchArm {
625            symbol_id: self.function_id.to_string(),
626            enum_name: self.enum_name.clone(),
627            old_pattern: self.old_pattern.clone(),
628            new_pattern: self.new_pattern.clone(),
629            new_body: self.new_body.clone(),
630        }
631    }
632}
633
634impl ToSerializable for super::AddStructLiteralFieldMutation {
635    fn to_serializable(&self) -> SerializableMutation {
636        SerializableMutation::AddStructLiteralField {
637            struct_name: format!("{:?}", self.struct_id),
638            field_name: self.field_name.clone(),
639            value: self.value.clone(),
640        }
641    }
642}
643
644impl ToSerializable for super::AddConstMutation {
645    fn to_serializable(&self) -> SerializableMutation {
646        SerializableMutation::AddConst {
647            symbol_id: format!("{:?}", self.symbol_id),
648            name: self.name.clone(),
649            ty: self.ty.clone(),
650            value: self.value.clone(),
651            is_pub: self.is_pub,
652        }
653    }
654}
655
656impl ToSerializable for super::RemoveConstMutation {
657    fn to_serializable(&self) -> SerializableMutation {
658        SerializableMutation::RemoveConst {
659            symbol_id: format!("{:?}", self.symbol_id),
660        }
661    }
662}
663
664impl ToSerializable for super::AddTypeAliasMutation {
665    fn to_serializable(&self) -> SerializableMutation {
666        SerializableMutation::AddTypeAlias {
667            symbol_id: format!("{:?}", self.symbol_id),
668            name: self.name.clone(),
669            ty: self.ty.clone(),
670            is_pub: self.is_pub,
671        }
672    }
673}
674
675impl ToSerializable for super::RemoveTypeAliasMutation {
676    fn to_serializable(&self) -> SerializableMutation {
677        SerializableMutation::RemoveTypeAlias {
678            symbol_id: format!("{:?}", self.symbol_id),
679        }
680    }
681}
682
683impl ToSerializable for super::RemoveTraitMutation {
684    fn to_serializable(&self) -> SerializableMutation {
685        SerializableMutation::RemoveTrait {
686            name: self.trait_id.to_string(),
687        }
688    }
689}
690
691impl ToSerializable for super::AddImplMutation {
692    fn to_serializable(&self) -> SerializableMutation {
693        SerializableMutation::AddImpl {
694            symbol_id: self.parent.to_string(),
695            target: self.target.clone(),
696            trait_name: self.trait_name.clone(),
697        }
698    }
699}
700
701impl ToSerializable for super::RemoveImplMutation {
702    fn to_serializable(&self) -> SerializableMutation {
703        SerializableMutation::RemoveImpl {
704            symbol_id: self.symbol_id.to_string(),
705        }
706    }
707}
708
709impl ToSerializable for super::AddItemMutation {
710    fn to_serializable(&self) -> SerializableMutation {
711        SerializableMutation::AddItem {
712            symbol_id: self.parent.to_string(),
713            content: self.content.clone(),
714        }
715    }
716}
717
718impl ToSerializable for super::RemoveItemMutation {
719    fn to_serializable(&self) -> SerializableMutation {
720        SerializableMutation::RemoveItem {
721            symbol_id: self.symbol_id.to_string(),
722            item_kind: format!("{:?}", self.item_kind),
723        }
724    }
725}
726
727// ============================================================================
728// Fallback for mutations without ToSerializable
729// ============================================================================
730
731/// Create a generic serializable mutation from any mutation trait object.
732/// Use this for mutations that don't have a specific ToSerializable impl.
733pub fn to_generic(mutation: &dyn super::Mutation) -> SerializableMutation {
734    SerializableMutation::Generic {
735        mutation_type: mutation.mutation_type().to_string(),
736        description: mutation.describe(),
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
744
745    #[test]
746    fn test_rename_roundtrip() {
747        let mut registry = SymbolRegistry::new();
748        let path = SymbolPath::parse("test_crate::foo").unwrap();
749        let symbol_id = registry.register(path, SymbolKind::Function).unwrap();
750
751        let original = super::super::RenameMutation::new(symbol_id, "bar");
752
753        let serializable = original.to_serializable();
754        let json = serializable.to_json();
755        let restored = SerializableMutation::from_json(&json).unwrap();
756        let mutation = restored.to_mutation().unwrap();
757
758        assert_eq!(mutation.mutation_type(), "Rename");
759        // describe now uses symbol_id format
760        assert!(mutation.describe().contains("bar"));
761    }
762
763    #[test]
764    fn test_add_function_roundtrip() {
765        let mut registry = SymbolRegistry::new();
766        let path = SymbolPath::parse("test_crate").unwrap();
767        let parent_id = registry.register(path, SymbolKind::Mod).unwrap();
768
769        let original = super::super::AddFunctionMutation::new(parent_id, "my_func")
770            .with_params(vec![("x".to_string(), "i32".to_string())])
771            .with_return_type("i32")
772            .with_body("x + 1")
773            .public();
774
775        let serializable = original.to_serializable();
776        let json = serializable.to_json();
777        let restored = SerializableMutation::from_json(&json).unwrap();
778
779        // AddFunction now requires SymbolId, so to_mutation() returns None
780        assert!(restored.to_mutation().is_none());
781        assert_eq!(restored.mutation_type(), "AddFunction");
782    }
783
784    #[test]
785    fn test_generic_fallback() {
786        let mut registry = SymbolRegistry::new();
787        let path = SymbolPath::parse("test_crate::a").unwrap();
788        let symbol_id = registry.register(path, SymbolKind::Function).unwrap();
789
790        let mutation = super::super::RenameMutation::new(symbol_id, "b");
791        let generic = to_generic(&mutation);
792
793        assert_eq!(generic.mutation_type(), "Rename");
794        if let SerializableMutation::Generic { description, .. } = generic {
795            assert!(description.contains("b"));
796        }
797    }
798}