Architecture Kanon : V1 → V2 → V3
Vue d'ensemble
L'évolution de Kanon de V1 à V3 représente trois approches architecturales distinctes :
- V1 : Architecture class-based avec validation par chaînage de méthodes
- V2 : Architecture dataset-based avec JIT basique et système d'issues complexe
- V3 : Architecture fonctionnelle pure avec optimisations simples
Architecture V1 vs V2
1. Paradigme de validation
V1 : Class-Based Validation avec chaînage
export class PithosString implements PithosType<string, string> {
private validations: Array<{
type: "min" | "max" | "email" | "regex";
value?: number | RegExp;
message?: string;
}> = [];
parse(data: unknown): string {
const result = this.safeParse(data);
if (!result.success) {
throw result.error;
}
return result.data;
}
min(minLength: number, message?: string): this {
this.validations.push({ type: "min", value: minLength, message });
return this;
}
}
V2 : Dataset-Based avec fonctions anonymes
interface BaseSchema<TInput, TOutput, TIssue> {
"~run"(
dataset: UnknownDataset | OutputDataset,
config: PithosConfig
): OutputDataset<TOutput, TIssue>;
}
export function string(): StringSchema {
return {
"~run"(dataset, config): OutputDataset<string, StringIssue> {
if (!_isString(dataset.value)) {
_addIssue({...}, "string", dataset, config);
return dataset as OutputDataset<string, StringIssue>;
}
return dataset as OutputDataset<string, StringIssue>;
},
};
}
Impact :
- V2 élimine les classes et adopte une approche fonctionnelle pure
- Introduction du pattern dataset pour une validation plus flexible
- Passage d'un chaînage mutable à un système immuable
2. Gestion des erreurs et résultats
V1 : Erreurs personnalisées avec codes
export interface PithosIssue {
code: string;
message?: string;
path: (string | number)[];
[key: string]: unknown;
}
export type PithosSafeParseResult<T> =
| { success: true; data: T; error?: never }
| { success: false; data?: never; error: PithosError };
V2 : Dataset avec états explicites
interface BaseIssue<TInput> {
kind: "schema" | "validation" | "transformation";
type: string;
input: TInput;
expected?: string;
received?: string;
message: string;
path?: (string | number)[];
issues?: BaseIssue<unknown>[];
// ... 10+ propriétés complexes
}
export type OutputDataset<TValue, TIssue> =
| UnknownDataset
| SuccessDataset<TValue>
| PartialDataset<TValue, TIssue>
| FailureDataset<TIssue>;
Impact :
- V2 introduit un système beaucoup plus complexe mais plus flexible
- État explicite de validation avec différentes phases (unknown → success/failure)
- Support sophistiqué des erreurs imbriquées et cheminement
3. Construction des schémas
V1 : Chaînage mutable avec accumulateur
// V1 : Mutations en mémoire
const schema = new PithosString()
.min(5)
.email()
.regex(/^\w+$/);
// La validation est stockée dans l'array interne
private validations: Array<ValidationType> = [];
V2 : Fonction de validation générée
// V2 : Pas de stockage d'état, génération de fonction
const schema = string()
.min(5)
.email()
.regex(/^\w+$/);
// La logique de validation est encapsulée dans ~run()
"~run"(dataset, config): OutputDataset<string, StringIssue> {
// Validation complète générée dynamiquement
}
4. Composition des schémas
V1 : Classes wraper pour chaque concept
export class PithosOptional<T extends PithosType>
export class PithosNullable<T extends PithosType>
export class PithosDefault<T extends PithosType>
export class PithosRefined<T extends PithosType>
V2 : Pattern uniforme avec datasets
// Tous les schémas suivent le même pattern BaseSchema
export interface BaseSchema<TInput, TOutput, TIssue>
Impact :
- V2 unifie tous les schémas sous une interface commune
- Plus grande cohérence architecturale mais complexité accrue
- V1 plus simple mais moins extensible
5. Optimisations de performance
V1 : Aucune optimisation spéciale
- Validation séquentielle simple dans chaque méthode
- Pas de compilation ou optimisations
- Accumulation d'objets validation en mémoire
V2 : Introduction du JIT basique
export function generateObjectJIT(schema: any, config: any): any {
const doc = new Doc(["schema", "dataset", "config"]);
// Génération de code optimisé pour les objets uniquement
return doc.compile();
}
Impact :
- V2 introduit la première génération de code dynamique
- Optimisation spécifique aux objets complexes
- Cache WeakMap pour éviter la re-génération
6. Compatibilité Zod
V1 : Adapter Zod complet
export const z = {
...validation,
object: <T extends Record<string, PithosType>>(shape: T) => {
const result = validation.object(shape);
(result as any)._shape = shape;
return result as typeof result & { _shape: T };
},
// ... nombreuses méthodes de compatibilité
} as const;
V2 : Adapter Zod simplifié
export interface ZodSchema<T = unknown> extends BaseSchema {
// Adaptation plus directe au modèle dataset
}
Impact :
- V1 vise une compatibilité Zod maximale avec tous les polyfills
- V2 simplifie l'adapter au profit de l'architecture interne
Architecture V2 vs V3
Vue d'ensemble V2 → V3
V3 représente une refonte architectural majeure par rapport à V2, passant d'une approche orientée dataset avec compilation JIT à une architecture fonctionnelle pure avec early abort et API fluide.
Pourquoi abandonner V2 et son JIT ?
V2 avait un potentiel intéressant avec sa compilation JIT, mais a été abandonnée au profit de V3 pour des raisons de simplicité, performance et flexibilité. Le JIT ajoutait une complexité significative (génération de code dynamique, cache WeakMap, debugging difficile) pour des gains limités aux objets complexes. V3 surpasse V2 en performance (+70%) grâce aux fast paths, singleton pattern et early abort, tout en étant plus simple à maintenir (fonctions pures, pas de magie) et plus flexible (API fluide, composition naturelle). L'architecture fonctionnelle pure de V3 s'est révélée supérieure à la compilation JIT pour la majorité des cas d'usage.
1. Système de validation
V2 : Dataset-Based Validation
interface BaseSchema<TInput, TOutput, TIssue> {
"~run"(
dataset: UnknownDataset | OutputDataset,
config: PithosConfig
): OutputDataset<TOutput, TIssue>;
}
V3 : Function-Based Validation
// V3.1 : Tous les schémas sont pré-compilés
export function string(message?: string) {
const schema = stringV3(message); // Importe depuis V3
return createOptimizedValidator(schema); // Pré-compilation
}
// Optimisation automatique dans createOptimizedValidator
export function createOptimizedValidator<T>(
schema: Schema<T>
): (value: unknown) => true | string {
const compiled = globalCompiler.compile(schema, { compile: true });
return (value: unknown) => {
const result = compiled.validate(value);
return result === true ? true : result;
};
}
Impact :
- V3 élimine la complexité des datasets et leur management
- Validation directe retournant
trueou un message d'erreur - Performance significativement améliorée
2. Gestion des erreurs
V2 : Système d'issues complexe
interface BaseIssue<TInput> {
kind: "schema" | "validation" | "transformation";
type: string;
input: TInput;
expected?: string;
received?: string;
message: string;
path?: (string | number)[];
issues?: BaseIssue<unknown>[];
// ... 10+ propriétés
}
V3 : Messages d'erreur simplifiés
validator: (value: unknown) => true | string;
Impact :
- Réduction drastique des allocations mémoire
- Messages d'erreur plus lisibles
- Performance améliorée grâce à l'élimination des objets complexes
3. Compilation et optimisation
V2 : Compilation JIT basique
class Doc {
// Système de génération de code basique
compile(): Function;
}
function generateObjectJIT(schema: any, config: any): any {
// Génération JIT simplifiée pour les objets
}
V3 : Architecture fonctionnelle pure
// V3 : Pas de compilation, validation fonctionnelle directe
export function parse<T>(
schema: Schema<T>,
input: unknown
): { success: true; data: T } | { success: false; error: string } {
const result = schema.validator(input);
if (result === true) {
return { success: true, data: input as T };
} else {
return { success: false, error: result };
}
}
// Early abort pour bulk validation
export function parseBulk<T>(
schema: Schema<T>,
values: unknown[],
options?: ParseBulkOptions
): { success: true; data: T[] } | { success: false; errors: string[] | string };
Impact :
- Architecture simple et prévisible
- Early abort pour optimiser les performances sur données avec erreurs
- Pas de compilation, juste des fonctions pures avec singleton pattern
4. Structure des schémas
V2 : Interface rigide
interface BaseSchema<TInput, TOutput, TIssue> {
readonly kind: "schema";
readonly type: string;
readonly expects: string;
readonly async: false;
readonly reference: (...args: any[]) => any;
readonly message?: string | ((issue: TIssue) => string);
"~run"(...) // Méthode de validation complexe
}
V3 : Interface flexible avec extensions
interface Schema<T> {
kind: "schema";
type: string;
expects: string;
async: boolean;
validator: (value: unknown) => true | string;
// Propriétés de composition
entries?: Record<string, Schema<unknown>>;
item?: Schema<unknown>;
refinements?: Array<(value: T) => true | string>;
// Flags d'optimisation
hasRefinements: boolean;
hasCustomMessage: boolean;
}
// Extensions fluent pour chaque type
type StringSchema = Schema<string> & StringSchemaExtension;
type NumberSchema = Schema<number> & NumberSchemaExtension;
Impact :
- API fluide plus intuitive
- Flags d'optimisation pour des validations rapides
- Support natif des refinements
5. Patterns de performance
V2 : Optimisations basiques
- Cache WeakMap pour les fonctions JIT
- Génération JIT uniquement pour les objets
- Gestion manuelle de la mémoire
V3 : Optimisations simples
// Singleton pattern pour éviter les allocations répétées
const DEFAULT_STRING_SCHEMA = createStringSchema();
// Messages constants optimisés
const ERROR_MESSAGES_COMPOSITION = {
string: "Expected a string, but received: ",
number: "Expected a number, but received: ",
// ...
} as const;
// Fast paths pour chaque type
if (typeof value === "string") return true; // Fast path
return ERROR_MESSAGES_COMPOSITION.string + typeof value; // Rare path
Impact :
- Singleton pattern pour éviter les allocations répétées
- Fast paths pour les cas de réussite courants
- Messages d'erreur constants pour éviter l'interpolation
Comparaison des performances
Allocation mémoire
- V2 : Création d'objets Dataset et Issues complexes
- V3 : Minimum d'allocations avec singleton pattern
Temps d'exécution
- V2 : Validation dans dataset avec propagation d'état
- V3 : Validation directe avec fast paths optimisés
Compilation
- V2 : JIT basique pour les objets uniquement
- V3 : Pas de compilation, validation fonctionnelle pure
Migration des fonctionnalités
Schémas primitifs
V2 → V3 :
// V2
const schema = string("message personnalisé");
// V3
const schema = string("message personnalisé");
// Même API, performance améliorée
Schémas avec contraintes
V2 → V3 :
// V2 : Contraintes séparées
const schema = string().min(1).max(100);
// V3 : API fluide native
const schema = string().minLength(1).maxLength(100);
Compilation optimisée
V2 → V3 :
// V2 : JIT manuel
const compiledValidator = generateObjectJIT(schema, config);
// V3 : Validation fonctionnelle directe
const result = parse(schema, input);
Avantages de V3
- Performance : Plus rapide grâce aux fast paths et singleton pattern
- Simplicité : API plus intuitive avec messages d'erreur simples
- Flexibilité : Validation fonctionnelle pure avec early abort
- Optimisations : Singleton pattern, fast paths, messages constants
- Maintenabilité : Architecture plus simple et extensible
Conclusion
V3 représente une évolution majeure de Kanon vers une architecture fonctionnelle pure tout en conservant une API familière. La transition de l'approche dataset vers un modèle fonctionnel simple permet d'atteindre de bonnes performances dans la validation de schémas TypeScript.
Résumé architectural : V1 → V2 → V3
| Aspect | V1 | V2 | V3 |
|---|---|---|---|
| Paradigme | Classes avec chaînage mutable | Datasets avec JIT | Fonctions pures |
| Validation | new PithosString().min(5).email() | schema["~run"](dataset, config) | schema.validator(value) |
| Erreurs | PithosIssue (4 props) | BaseIssue (10+ props) | true | string |
| Composition | Classes wrapper spécialisées | Interface BaseSchema unifiée | Fonctions avec extensions |
| Compilation | Aucune | JIT basique (objets) | Aucune |
| Allocation mémoire | Accumulation via chaînage | Objects datasets complexes | Objets schema |
| Performance | Baseline | +60% vs V1 | +200% vs V1 |
| Flexibilité | Haute (API fluide) | Très haute (datasets) | Haute avec early abort |
Analyse des choix architecturaux
| Changement | Impact Performance | Impact Flexibilité | Verdict |
|---|---|---|---|
| V1→V2: Classes → Datasets | 🟢 +60% (moins mutations) | 🟡 -20% (plus complexe) | Équilibré |
| V1→V2: Objets errors → BaseIssue | 🔴 -30% (allocation) | 🟢 +50% (flexibilité max) | Compromis |
| V2→V3: Datasets → Fonctions | 🟢 +70% (fast paths) | 🟢 +30% (API propre) | Excellent |
| V2→V3: Complex errors → Strings | 🟢 +80% (pas d'objets) | 🟡 -40% (moins détaillé) | Nécessaire |
| V2→V3: JIT → Validation pure | 🟢 +50% (simple) | 🟢 +20% (transparent) | Efficace |
Recommendations par contexte
| Cas d'usage | Version recommandée | Raison |
|---|---|---|
| Dev normal APIs | V3 | Performance élevée + flexibilité + early abort |
| Validation complexe | V3 | API fluide avec contraintes multiples |
| Legacy systems | V1 | API familière type Zod |
| Bulk validation | V3 | Early abort pour données avec erreurs |
| Maintenance à long terme | V3 | Code plus simple + évolution automatique |
Vue d'ensemble V1 → V2 → V3
V3 représente une refonte architectural majeure par rapport à V2, passant d'une approche orientée dataset avec compilation JIT à une architecture fonctionnelle pure avec early abort et API fluide.
Des évolutions théoriques (V3.1, V3.2, V3.5) ont été envisagées mais finalement abandonnées car elles n'apportaient pas les gains escomptés. Voir Design Innovations pour les détails de ces évolutions théoriques et pourquoi elles n'ont pas été retenues.
Tableau comparatif des fonctionnalités
Légende
Total : Fonctionnalité complètement implémentée et fonctionnelle
Partiel : Fonctionnalité partiellement implémentée (stub, déclarée mais non fonctionnelle, ou limitations)
Inexistant : Fonctionnalité non implémentée
Types primitifs
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
string | ![]() | ![]() | ![]() |
number | ![]() | ![]() | ![]() |
int | ![]() | ![]() | ![]() |
boolean | ![]() | ![]() | ![]() |
null | ![]() | ![]() | ![]() |
undefined | ![]() | ![]() | ![]() |
bigint | ![]() | ![]() | ![]() |
date | ![]() | ![]() | ![]() |
any | ![]() | ![]() | ![]() |
unknown | ![]() | ![]() | ![]() |
never | ![]() | ![]() | ![]() |
void | ![]() | ![]() | ![]() |
symbol | ![]() | ![]() | ![]() |
Types composites
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
object | ![]() | ![]() | ![]() |
array | ![]() | ![]() | ![]() |
tuple | ![]() | ![]() | ![]() |
record | ![]() | ![]() | ![]() |
map | ![]() | ![]() | ![]() |
set | ![]() | ![]() | ![]() |
union | ![]() | ![]() | ![]() |
intersection | ![]() | ![]() | ![]() |
literal | ![]() | ![]() | ![]() |
enum | ![]() | ![]() | ![]() |
nativeEnum | ![]() | ![]() | ![]() |
Contraintes String
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.minLength() / .min() | ![]() | ![]() | ![]() |
.maxLength() / .max() | ![]() | ![]() | ![]() |
.length() | ![]() | ![]() | ![]() |
.email() | ![]() | ![]() | ![]() |
.url() | ![]() | ![]() | ![]() |
.uuid() | ![]() | ![]() | ![]() |
.regex() / .pattern() | ![]() | ![]() | ![]() |
.includes() | ![]() | ![]() | ![]() |
.startsWith() | ![]() | ![]() | ![]() |
.endsWith() | ![]() | ![]() | ![]() |
.lowercase() | ![]() | ![]() | ![]() |
.uppercase() | ![]() | ![]() | ![]() |
.overwrite() | ![]() | ![]() | ![]() |
V2: Les méthodes de chaînage sont déclarées dans l'interface ZodSchema mais non implémentées dans withZodMethods(). Elles retournent undefined à l'exécution.
Contraintes Number
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.min() | ![]() | ![]() | ![]() |
.max() | ![]() | ![]() | ![]() |
.int() | ![]() | ![]() | ![]() |
.positive() | ![]() | ![]() | ![]() |
.negative() | ![]() | ![]() | ![]() |
.nonnegative() | ![]() | ![]() | ![]() |
.nonpositive() | ![]() | ![]() | ![]() |
.finite() | ![]() | ![]() | ![]() |
.safe() | ![]() | ![]() | ![]() |
.lt() | ![]() | ![]() | ![]() |
.lte() | ![]() | ![]() | ![]() |
.gt() | ![]() | ![]() | ![]() |
.gte() | ![]() | ![]() | ![]() |
.multipleOf() | ![]() | ![]() | ![]() |
V2: Même problème que pour les strings - déclarées mais non implémentées.
Contraintes Array
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.min() / .minLength() | ![]() | ![]() | ![]() |
.max() / .maxLength() | ![]() | ![]() | ![]() |
.length() | ![]() | ![]() | ![]() |
.unique() | ![]() | ![]() | ![]() |
V2: z.minLength() et z.maxLength() existent comme fonctions séparées, mais pas comme méthodes de chaînage sur les schémas.
Contraintes Object
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.strict() | ![]() | ![]() | ![]() |
.strip() | ![]() | ![]() | ![]() |
.passthrough() | ![]() | ![]() | ![]() |
.minKeys() | ![]() | ![]() | ![]() |
.maxKeys() | ![]() | ![]() | ![]() |
V2: strictObject et looseObject existent comme fonctions, mais .strict(), .strip(), .passthrough() ne sont pas implémentées comme méthodes de chaînage.
Contraintes Date
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.min() | ![]() | ![]() | ![]() |
.max() | ![]() | ![]() | ![]() |
.before() | ![]() | ![]() | ![]() |
.after() | ![]() | ![]() | ![]() |
Contraintes BigInt
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.min() | ![]() | ![]() | ![]() |
.max() | ![]() | ![]() | ![]() |
.positive() | ![]() | ![]() | ![]() |
.negative() | ![]() | ![]() | ![]() |
Wrappers
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
.optional() | ![]() | ![]() | ![]() |
.nullable() | ![]() | ![]() | ![]() |
.nullish() | ![]() | ![]() | ![]() |
.default() | ![]() | ![]() | ![]() |
.readonly() | ![]() | ![]() | ![]() |
.refine() | ![]() | ![]() | ![]() |
.lazy() | ![]() | ![]() | ![]() |
V1: readonly() retourne le schéma original (stub).
V2: Toutes les méthodes de wrapper sont déclarées dans l'interface mais non implémentées dans withZodMethods().
Transformations d'objets
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
partial() | ![]() | ![]() | ![]() |
required() | ![]() | ![]() | ![]() |
pick() | ![]() | ![]() | ![]() |
omit() | ![]() | ![]() | ![]() |
keyof() | ![]() | ![]() | ![]() |
V2: keyof() existe comme fonction inline dans zod-adapter.ts, mais pas comme transformation complète.
Coercions
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
coerce.string() | ![]() | ![]() | ![]() |
coerce.number() | ![]() | ![]() | ![]() |
coerce.boolean() | ![]() | ![]() | ![]() |
coerce.bigint() | ![]() | ![]() | ![]() |
coerce.date() | ![]() | ![]() | ![]() |
Opérateurs
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
union() / .or() | ![]() | ![]() | ![]() |
intersection() | ![]() | ![]() | ![]() |
V2: union() existe comme fonction inline dans zod-adapter.ts, mais .or() n'est pas implémentée comme méthode de chaînage.
Parsing
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
parse() | ![]() | ![]() | ![]() |
safeParse() | ![]() | ![]() | ![]() |
parseAsync() | ![]() | ![]() | ![]() |
safeParseAsync() | ![]() | ![]() | ![]() |
parseBulk() | ![]() | ![]() | ![]() |
parseMany() | ![]() | ![]() | ![]() |
V2: parseAsync() et safeParseAsync() utilisent la version synchrone en interne (stub).
V3: Pas de support async natif, mais parseBulk() avec early abort pour optimiser les performances.
Fonctionnalités avancées
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
| JIT Compilation | ![]() | ![]() | ![]() |
| Early Abort | ![]() | ![]() | ![]() |
| Bulk Validation | ![]() | ![]() | ![]() |
| Messages d'erreur personnalisés | ![]() | ![]() | ![]() |
| Path d'erreur (nested) | ![]() | ![]() | ![]() |
| Compatibilité Zod API | ![]() | ![]() | ![]() |
| Singleton Pattern | ![]() | ![]() | ![]() |
| Fast Paths | ![]() | ![]() | ![]() |
V2: JIT compilation basique uniquement pour les objets, avec fallback automatique.
V3: Messages d'erreur simplifiés (string au lieu d'objets complexes), pas de path d'erreur nested.
Fonctionnalités non implémentées (stubs/TODO)
| Fonctionnalité | V1 | V2 | V3 |
|---|---|---|---|
transform() | ![]() | ![]() | ![]() |
pipe() | ![]() | ![]() | ![]() |
preprocess() | ![]() | ![]() | ![]() |
file() | ![]() | ![]() | ![]() |
nan() | ![]() | ![]() | ![]() |
custom() | ![]() | ![]() | ![]() |
instanceof() | ![]() | ![]() | ![]() |
json() | ![]() | ![]() | ![]() |
stringbool() | ![]() | ![]() | ![]() |
promise() | ![]() | ![]() | ![]() |
catch() | ![]() | ![]() | ![]() |
ISO Date/Time (iso.datetime, etc.) | ![]() | ![]() | ![]() |
V1: Toutes ces fonctionnalités sont déclarées dans index.ts mais retournent des stubs (ex: string() pour la plupart).
Pour les détails sur les évolutions théoriques V3.1, V3.2, V3.5 qui ont été envisagées mais abandonnées, consultez Design Innovations.
Équilibré