|
| 1 | +# Spec-Based Marshalling - Proof of Concept ✅ |
| 2 | + |
| 3 | +## 📋 Contexte |
| 4 | + |
| 5 | +**Ticket**: DEVTOOLSDEVTOOLS-1090 - Do (un)marshalling using object specs instead of methods |
| 6 | + |
| 7 | +**Problème actuel** : Le SDK utilise des fonctions générées automatiquement pour marshaller/unmarshaller chaque type, ce qui peut causer : |
| 8 | +- 🔄 Des dépendances circulaires entre types |
| 9 | +- 🚫 Impossibilité de faire de la réflexion runtime |
| 10 | +- 📦 Beaucoup de code généré |
| 11 | + |
| 12 | +**Solution proposée** : Utiliser des spécifications déclaratives (objets) qui décrivent la structure des types, avec des fonctions génériques qui interprètent ces specs. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## ✅ Phase 1 : Proof of Concept (TERMINÉ) |
| 17 | + |
| 18 | +### 🎯 Ce qui a été fait |
| 19 | + |
| 20 | +#### 1. Nouveau système de specs (`packages/client/src/helpers/spec-marshalling.ts`) |
| 21 | + |
| 22 | +**Interfaces créées** : |
| 23 | +- `FieldSpec` : Spécification d'un champ individuel |
| 24 | +- `TypeSpec` : Spécification d'un type complet |
| 25 | +- `SpecRegistry` : Registre pour stocker et référencer les specs |
| 26 | + |
| 27 | +**Fonctions génériques implémentées** : |
| 28 | +- `unmarshalWithSpec()` : Unmarshalling générique basé sur spec |
| 29 | +- `marshalWithSpec()` : Marshalling générique basé sur spec |
| 30 | +- `unmarshal()` / `marshal()` : Fonctions de convenance |
| 31 | + |
| 32 | +**Features supportées** : |
| 33 | +- ✅ Types de base : string, number, boolean, date |
| 34 | +- ✅ Types complexes : arrays, maps, nested objects |
| 35 | +- ✅ Références entre types (résout les dépendances circulaires) |
| 36 | +- ✅ Conversion snake_case ↔ camelCase |
| 37 | +- ✅ Valeurs par défaut (defaultProjectId, etc.) |
| 38 | +- ✅ Champs optionnels |
| 39 | + |
| 40 | +#### 2. Tests complets (`packages/client/src/helpers/__tests__/spec-marshalling.test.ts`) |
| 41 | + |
| 42 | +**30 tests unitaires** couvrant : |
| 43 | +- Types de base |
| 44 | +- Conversions de nommage |
| 45 | +- Types complexes (arrays, maps) |
| 46 | +- Objets imbriqués avec références |
| 47 | +- Gestion d'erreurs |
| 48 | +- Valeurs par défaut |
| 49 | +- Exemple réel avec le type `Human` |
| 50 | + |
| 51 | +**Résultat** : ✅ 30/30 tests passent |
| 52 | + |
| 53 | +#### 3. Documentation et exemple (`packages/client/src/helpers/__examples__/human-spec-example.ts`) |
| 54 | + |
| 55 | +Un exemple complet montrant : |
| 56 | +- Comparaison approche ancienne vs nouvelle |
| 57 | +- Utilisation des specs |
| 58 | +- Avantages démontrés (réflexion, validation, etc.) |
| 59 | +- Stratégie de migration |
| 60 | + |
| 61 | +#### 4. Export public (`packages/client/src/internals.ts`) |
| 62 | + |
| 63 | +Les nouvelles fonctions sont maintenant exportées et disponibles : |
| 64 | +```typescript |
| 65 | +export type { FieldSpec, TypeSpec } |
| 66 | +export { |
| 67 | + specRegistry, |
| 68 | + SpecRegistry, |
| 69 | + unmarshal, |
| 70 | + unmarshalWithSpec, |
| 71 | + marshal, |
| 72 | + marshalWithSpec, |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## 📊 Comparaison des approches |
| 79 | + |
| 80 | +### ❌ Avant : Fonctions générées |
| 81 | + |
| 82 | +```typescript |
| 83 | +// ~25 lignes de code par type |
| 84 | +export const unmarshalHuman = (data: unknown): Human => { |
| 85 | + if (!isJSONObject(data)) { |
| 86 | + throw new TypeError(...) |
| 87 | + } |
| 88 | + return { |
| 89 | + name: data.name, |
| 90 | + age: data.age, |
| 91 | + createdAt: unmarshalDate(data.created_at), |
| 92 | + address: unmarshalAddress(data.address), // ⚠️ Dépendance circulaire possible |
| 93 | + // ... 10+ autres champs |
| 94 | + } as Human |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +**Problèmes** : |
| 99 | +- Dépendances entre fonctions |
| 100 | +- Pas de réflexion possible |
| 101 | +- Code verbeux |
| 102 | + |
| 103 | +### ✅ Après : Specs déclaratives |
| 104 | + |
| 105 | +```typescript |
| 106 | +// ~15 lignes de data par type |
| 107 | +export const HumanSpec: TypeSpec = { |
| 108 | + type: 'object', |
| 109 | + fields: { |
| 110 | + name: { source: 'name', type: 'string' }, |
| 111 | + age: { source: 'age', type: 'number' }, |
| 112 | + createdAt: { source: 'created_at', type: 'date' }, |
| 113 | + address: { source: 'address', type: 'reference', specName: 'Address' }, // ✅ Pas de dépendance |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +// Usage |
| 118 | +const human = unmarshal<Human>(data, HumanSpec) |
| 119 | +``` |
| 120 | + |
| 121 | +**Avantages** : |
| 122 | +- ✅ Pas de dépendances circulaires (juste des objets) |
| 123 | +- ✅ Réflexion possible runtime |
| 124 | +- ✅ Plus facile à tester et maintenir |
| 125 | +- ✅ Auto-documenté |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## 🔄 Stratégie de migration recommandée |
| 130 | + |
| 131 | +### Phase 1 : Migration douce (v3.0) |
| 132 | + |
| 133 | +Générer **les deux** pour assurer la rétrocompatibilité : |
| 134 | + |
| 135 | +```typescript |
| 136 | +// Nouveau : La spec |
| 137 | +export const HumanSpec: TypeSpec = { ... } |
| 138 | + |
| 139 | +// Ancien : Wrapper déprécié |
| 140 | +/** |
| 141 | + * @deprecated Use unmarshal(data, HumanSpec) instead |
| 142 | + */ |
| 143 | +export const unmarshalHuman = (data: unknown): Human => { |
| 144 | + return unmarshal<Human>(data, HumanSpec) |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +**Impact** : ❌ Aucun breaking change ! |
| 149 | + |
| 150 | +### Phase 2 : Période de transition (v3.1 - v3.x) |
| 151 | + |
| 152 | +- Les deux approches coexistent |
| 153 | +- Documentation mise à jour |
| 154 | +- Warnings de dépréciation |
| 155 | +- 6+ mois de transition |
| 156 | + |
| 157 | +### Phase 3 : Nettoyage (v4.0) |
| 158 | + |
| 159 | +- Supprimer les fonctions wrapper |
| 160 | +- Garder uniquement les specs |
| 161 | +- Breaking change assumé (mais minimal) |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## 📈 Bénéfices concrets |
| 166 | + |
| 167 | +### 1. Résolution des dépendances circulaires |
| 168 | + |
| 169 | +**Avant** : |
| 170 | +```typescript |
| 171 | +// ❌ Circular dependency error possible |
| 172 | +unmarshalHuman → unmarshalAddress → unmarshalHuman |
| 173 | +``` |
| 174 | + |
| 175 | +**Après** : |
| 176 | +```typescript |
| 177 | +// ✅ Juste des objets, pas de dépendances |
| 178 | +HumanSpec (data) → AddressSpec (data) → Resolved! |
| 179 | +``` |
| 180 | + |
| 181 | +### 2. Réflexion runtime |
| 182 | + |
| 183 | +```typescript |
| 184 | +// Inspecter les champs d'un type |
| 185 | +console.log(Object.keys(HumanSpec.fields)) |
| 186 | +// ['id', 'name', 'age', 'createdAt', ...] |
| 187 | + |
| 188 | +// Trouver tous les champs de type 'date' |
| 189 | +const dateFields = Object.entries(HumanSpec.fields) |
| 190 | + .filter(([_, spec]) => spec.type === 'date') |
| 191 | + .map(([name]) => name) |
| 192 | +// ['createdAt', 'updatedAt'] |
| 193 | +``` |
| 194 | + |
| 195 | +### 3. Validation et documentation automatique |
| 196 | + |
| 197 | +```typescript |
| 198 | +// Générer automatiquement la documentation |
| 199 | +function generateDoc(spec: TypeSpec) { |
| 200 | + return Object.entries(spec.fields).map(([name, field]) => |
| 201 | + `- ${name} (${field.type}): ${field.source}` |
| 202 | + ) |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +### 4. Réduction de la complexité du code généré |
| 207 | + |
| 208 | +- Fonctions : ~25 lignes × 50 types = **1250 lignes** |
| 209 | +- Specs : ~15 lignes × 50 types = **750 lignes** |
| 210 | +- **Gain : ~40% de code en moins** |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## 🚀 Prochaines étapes |
| 215 | + |
| 216 | +### Phase 2 : Modifier le générateur protobuf |
| 217 | + |
| 218 | +**Dossier** : `/Users/jonathanremy/Desktop/protoc-gen/internal/protoc-gen-sdk-ts/` |
| 219 | + |
| 220 | +**Fichiers à modifier** : |
| 221 | +1. `templates/marshalling.ts.tmpl` : Nouveau template pour générer les specs |
| 222 | +2. `message.go` : Logique de génération adaptée |
| 223 | +3. Tests : Mettre à jour les golden files |
| 224 | + |
| 225 | +**Objectif** : Générer automatiquement : |
| 226 | +```typescript |
| 227 | +// Au lieu de |
| 228 | +export const unmarshalHuman = (data: unknown) => { ... } |
| 229 | + |
| 230 | +// Générer |
| 231 | +export const HumanSpec: TypeSpec = { ... } |
| 232 | +export const unmarshalHuman = (data: unknown) => unmarshal(data, HumanSpec) // wrapper déprécié |
| 233 | +``` |
| 234 | + |
| 235 | +### Phase 3 : Tests et validation |
| 236 | + |
| 237 | +1. Régénérer tous les packages avec le nouveau système |
| 238 | +2. Lancer tous les tests du SDK |
| 239 | +3. Vérifier les performances |
| 240 | +4. Valider avec des cas d'usage réels |
| 241 | + |
| 242 | +### Phase 4 : Documentation et communication |
| 243 | + |
| 244 | +1. Mettre à jour le MIGRATION_GUIDE.md |
| 245 | +2. Créer des exemples de migration |
| 246 | +3. Annoncer le changement dans le CHANGELOG |
| 247 | +4. Donner 6+ mois avant v4.0 |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## 🎓 Pour aller plus loin |
| 252 | + |
| 253 | +### Types non encore implémentés |
| 254 | + |
| 255 | +Ces types nécessitent du marshalling custom (déjà disponible dans `custom-marshalling.ts`) : |
| 256 | + |
| 257 | +- `Money` : Conversion montant + devise |
| 258 | +- `TimeSeries` : Structure complexe de séries temporelles |
| 259 | +- `Decimal` : Précision décimale |
| 260 | +- `Blob`/`ScwFile` : Upload de fichiers |
| 261 | + |
| 262 | +**Action** : Intégrer ces marshallers dans le système de specs |
| 263 | + |
| 264 | +### Optimisations possibles |
| 265 | + |
| 266 | +1. **Cache des specs** : Éviter de reparsing à chaque appel |
| 267 | +2. **Validation de specs** : Vérifier la cohérence des specs au build |
| 268 | +3. **Génération de types** : Générer les types TS à partir des specs |
| 269 | +4. **Performance** : Benchmarks avant/après |
| 270 | + |
| 271 | +--- |
| 272 | + |
| 273 | +## 📝 Résumé |
| 274 | + |
| 275 | +| Critère | Avant | Après | Gain | |
| 276 | +|---------|-------|-------|------| |
| 277 | +| Dépendances circulaires | ⚠️ Possibles | ✅ Impossibles | 🎯 | |
| 278 | +| Réflexion runtime | ❌ Non | ✅ Oui | 🎯 | |
| 279 | +| Lignes de code | ~1250 | ~750 | -40% | |
| 280 | +| Maintenabilité | 😐 | 😊 | 🎯 | |
| 281 | +| Breaking changes | - | ❌ Aucun (Phase 1) | 🎯 | |
| 282 | +| Tests | - | ✅ 30/30 passent | 🎯 | |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +## ✅ Validation |
| 287 | + |
| 288 | +**Phase 1 (POC) : TERMINÉ** ✅ |
| 289 | + |
| 290 | +- [x] Interfaces TypeScript |
| 291 | +- [x] Fonctions génériques unmarshal/marshal |
| 292 | +- [x] Support types complexes |
| 293 | +- [x] Tests unitaires (30/30) |
| 294 | +- [x] Exemple concret avec Human |
| 295 | +- [x] Documentation |
| 296 | + |
| 297 | +**Prêt pour Phase 2** : Modification du générateur protobuf 🚀 |
| 298 | + |
| 299 | +--- |
| 300 | + |
| 301 | +**Date** : 31 octobre 2025 |
| 302 | +**Auteur** : @jonathan |
| 303 | +**Status** : ✅ POC validé, prêt pour implémentation complète |
| 304 | + |
0 commit comments