Skip to content

Commit 9511a14

Browse files
committed
feat: add spec-based marshalling system to resolve circular dependencies
1 parent 59f27f4 commit 9511a14

File tree

3 files changed

+1354
-0
lines changed

3 files changed

+1354
-0
lines changed

SPEC_MARSHALLING_POC.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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+
unmarshalHumanunmarshalAddressunmarshalHuman
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

Comments
 (0)