🎓 Guide intermédiaire

Maîtriser le protocole pour des cas d’usage avancés

🎯 Objectifs de ce guide

Après avoir maîtrisé les bases, ce guide vous aidera à :

  • 🏗️ Architecturer des formulaires complexes avec validation orchestrée
  • 🔌 Intégrer le protocole avec vos frameworks existants
  • ⚡ Optimiser les performances et la gestion du cache
  • 🔒 Implémenter des validations conditionnelles et avancées
  • 🌐 Gérer les cas d’edge et la robustesse en production

📋 Cas d’usage : Formulaire de création de projet

Nous allons construire un formulaire complexe de création de projet qui démontre les capacités avancées du protocole.

Architecture du formulaire

graph TB
    subgraph "Formulaire Projet"
        NAME[📝 Nom du projet]
        TYPE[🏷️ Type de projet]
        LEAD[👤 Chef de projet]
        TEAM[👥 Équipe]
        TAGS[🏷️ Tags]
        BUDGET[💰 Budget]
        DEADLINE[📅 Date limite]
    end
    
    subgraph "Dépendances"
        TYPE --> TEAM
        TYPE --> BUDGET
        LEAD --> TEAM
    end
    
    subgraph "Validations"
        NAME --> VAL1[Unicité nom]
        LEAD --> VAL2[Permissions utilisateur]
        TEAM --> VAL3[Taille équipe selon type]
        BUDGET --> VAL4[Limites budgétaires]
    end
    
    classDef field fill:#e3f2fd
    classDef validation fill:#fff3e0
    classDef dependency fill:#f3e5f5
    
    class NAME,TYPE,LEAD,TEAM,TAGS,BUDGET,DEADLINE field
    class VAL1,VAL2,VAL3,VAL4 validation
    class TYPE,LEAD dependency

🏗️ Architecture côté serveur

1. Spécifications des champs avec dépendances

// TypeScript - Configuration avancée des champs
import { InputFieldSpec, DataType, ConstraintDescriptor } from '@cyfko/input-spec';

export class ProjectFormSpecifications {
  
  // Champ nom avec validation d'unicité
  static getProjectNameSpec(): InputFieldSpec {
    return {
      displayName: "Nom du projet",
      description: "Nom unique du projet (3-50 caractères)",
      dataType: DataType.STRING,
      expectMultipleValues: false,
      required: true,
      constraints: [
        {
          name: "minLenConstraint",
          type: "minLength", 
          params: { value: 3 }
        },
        {
          name: "maxLenConstraint",
          type: "maxLength",
          params: { value: 50 }
        },
        {
          name: "formatConstraint",
          type: "pattern",
          params: { pattern: "^[a-zA-Z0-9\\s\\-_]+$" }
        }
      ],
      valuesEndpoint: {
        protocol: "HTTPS",
        uri: "/api/projects/validate-name",
        method: "POST",
        mode: "CLOSED",
        debounceMs: 500,
        minSearchLength: 3,
        responseMapping: {
          dataField: "isAvailable"
        },
        cacheStrategy: "NONE" // Pas de cache pour l'unicité
      }
    };
  }

  // Champ type avec impact sur autres champs
  static getProjectTypeSpec(): InputFieldSpec {
    return {
      displayName: "Type de projet",
      description: "Catégorie qui détermine les contraintes et options disponibles",
      dataType: DataType.STRING,
      expectMultipleValues: false,
      required: true,
      constraints: [
        {
          name: "typeSelection",
          type: "membership",
          params: { 
            allowedValues: ["SMALL", "MEDIUM", "LARGE", "RESEARCH", "MAINTENANCE"]
          }
        }
      ],
      // v2: enumValues est supprimé. Utiliser un valuesEndpoint INLINE pour les listes statiques.
      valuesEndpoint: {
        protocol: "INLINE",
        mode: "CLOSED",
        values: [
          { value: "SMALL", label: "Petit projet (1-5 personnes)" },
          { value: "MEDIUM", label: "Projet moyen (6-15 personnes)" },
          { value: "LARGE", label: "Grand projet (16+ personnes)" },
          { value: "RESEARCH", label: "Projet de recherche" },
          { value: "MAINTENANCE", label: "Maintenance" }
        ]
      }
    };
  }

  // Champ chef de projet avec validation de permissions
  static getProjectLeadSpec(): InputFieldSpec {
    return {
      displayName: "Chef de projet",
      description: "Utilisateur responsable du projet (doit avoir les permissions appropriées)",
      dataType: DataType.STRING,
      expectMultipleValues: false,
      required: true,
      valuesEndpoint: {
        protocol: "HTTPS",
        uri: "/api/users/project-leads",
        method: "GET",
        mode: "SUGGESTIONS",
        searchField: "name",
        paginationStrategy: "PAGE_NUMBER",
        debounceMs: 300,
        minSearchLength: 2,
        responseMapping: {
          dataField: "users",
          totalField: "total",
          hasNextField: "hasNext"
        },
        requestParams: {
          pageParam: "page",
          limitParam: "limit",
          searchParam: "search",
          defaultLimit: 15
        },
        cacheStrategy: "SHORT_TERM"
      }
    };
  }

  // Champ équipe avec contraintes conditionnelles
  static getTeamMembersSpec(projectType?: string): InputFieldSpec {
    const constraints: ConstraintDescriptor[] = [];

    // Contraintes conditionnelles selon le type de projet
    if (projectType) {
      const teamSizeConstraints = this.getTeamSizeConstraints(projectType);
      constraints.unshift(teamSizeConstraints);
    }

    return {
      displayName: "Membres de l'équipe",
      description: "Sélectionnez les membres qui participeront au projet",
      dataType: DataType.STRING,
      expectMultipleValues: true,
      required: false,
      constraints: [
        ...constraints
      ],
      // v2: valuesEndpoint déplacé au niveau du champ
      valuesEndpoint: {
        protocol: "HTTPS",
        uri: "/api/users/team-members",
        method: "GET",
        mode: "SUGGESTIONS",
        searchField: "name", 
        paginationStrategy: "PAGE_NUMBER",
        debounceMs: 300,
        responseMapping: {
          dataField: "users",
          totalField: "total",
          hasNextField: "hasNext"
        },
        requestParams: {
          pageParam: "page",
          limitParam: "limit", 
          searchParam: "search",
          defaultLimit: 20
        },
        cacheStrategy: "SHORT_TERM"
      }
    };
  }

  private static getTeamSizeConstraints(projectType: string): ConstraintDescriptor {
    const sizeRules = {
      'SMALL': { min: 1, max: 5 },
      'MEDIUM': { min: 2, max: 15 },
      'LARGE': { min: 5, max: 50 },
      'RESEARCH': { min: 1, max: 8 },
      'MAINTENANCE': { min: 1, max: 3 }
    };

    const rule = sizeRules[projectType] || { min: 1, max: 10 };

    return {
      name: "teamSizeConstraint",
      type: "arraySize",
      params: { 
        minItems: rule.min,
        maxItems: rule.max
      },
      errorMessage: `L'équipe doit avoir entre ${rule.min} et ${rule.max} membres pour ce type de projet`
    };
  }
}

2. Endpoints avec logique métier

// Java Spring Boot - Endpoints avancés
@RestController
@RequestMapping("/api")
public class ProjectFormController {
    
    @Autowired
    private ProjectService projectService;
    
    @Autowired
    private UserService userService;
    
    // Validation de l'unicité du nom de projet
    @PostMapping("/projects/validate-name")
    public ProjectNameValidationResponse validateProjectName(
            @RequestBody ProjectNameRequest request) {
        
        boolean isAvailable = !projectService.existsByName(request.getName());
        
        ProjectNameValidationResponse response = new ProjectNameValidationResponse();
        response.setIsAvailable(isAvailable);
        response.setMessage(isAvailable ? 
            "Nom disponible" : 
            "Ce nom de projet existe déjà");
        
        return response;
    }
    
    // Recherche des chefs de projet éligibles
    @GetMapping("/users/project-leads")
    public UserSearchResponse getProjectLeads(
            @RequestParam(defaultValue = "") String search,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "15") int limit) {
        
        // Filtrer uniquement les utilisateurs avec permissions de chef de projet
        PageRequest pageRequest = PageRequest.of(page - 1, limit);
        Page<User> leaders = userService.findProjectLeadersByName(search, pageRequest);
        
        List<ValueAlias> userAliases = leaders.getContent().stream()
            .map(user -> new ValueAlias(user.getId().toString(), 
                user.getFullName() + " (" + user.getDepartment() + ")"))
            .collect(Collectors.toList());
        
        UserSearchResponse response = new UserSearchResponse();
        response.setUsers(userAliases);
        response.setTotal((int) leaders.getTotalElements());
        response.setHasNext(leaders.hasNext());
        response.setPage(page);
        
        return response;
    }
    
    // Spécification de champ dynamique selon le contexte
    @GetMapping("/fields/team-members")
    public InputFieldSpec getTeamMembersSpec(
            @RequestParam(required = false) String projectType,
            @RequestParam(required = false) String projectLeadId) {
        
        // Adapter la spécification selon le contexte
        InputFieldSpec baseSpec = ProjectFormSpecifications.getTeamMembersSpec(projectType);
        
        // Exclure le chef de projet de la liste des membres
        if (projectLeadId != null) {
            ConstraintDescriptor constraint = baseSpec.getConstraints().stream()
                .filter(c -> "team_selection".equals(c.getName()))
                .findFirst()
                .orElse(null);
                
            if (constraint != null && constraint.getValuesEndpoint() != null) {
                ValuesEndpoint endpoint = constraint.getValuesEndpoint();
                // Ajouter paramètre pour exclure le chef de projet
                if (endpoint.getRequestParams() == null) {
                    endpoint.setRequestParams(new RequestParams());
                }
                Map<String, Object> additionalParams = new HashMap<>();
                additionalParams.put("excludeUserId", projectLeadId);
                // En production, utiliser une approche plus propre pour les paramètres additionnels
            }
        }
        
        return baseSpec;
    }
}

🔧 Architecture côté client avancée

1. Gestionnaire de formulaire orchestré

// Client TypeScript - Orchestration avancée
import { 
  InputFieldSpec, 
  FieldValidator, 
  ValuesResolver, 
  ValidationResult 
} from '@cyfko/input-spec';

export class ProjectFormManager {
  private fieldSpecs: Map<string, InputFieldSpec> = new Map();
  private formData: Map<string, any> = new Map();
  private validationResults: Map<string, ValidationResult> = new Map();
  private fieldDependencies: Map<string, string[]> = new Map();
  
  private validator = new FieldValidator();
  private resolver: ValuesResolver;
  
  constructor(httpClient: HttpClient, cache: CacheProvider) {
    this.resolver = new ValuesResolver(httpClient, cache);
    this.setupFieldDependencies();
  }
  
  private setupFieldDependencies() {
    // Définir les dépendances entre champs
    this.fieldDependencies.set('projectType', ['teamMembers', 'budget']);
    this.fieldDependencies.set('projectLead', ['teamMembers']);
  }
  
  // Charger les spécifications avec dépendances
  async loadFieldSpec(fieldName: string, context?: Record<string, any>): Promise<InputFieldSpec> {
    let endpoint = `/api/fields/${fieldName}`;
    
    // Ajouter le contexte comme paramètres de requête
    if (context && Object.keys(context).length > 0) {
      const params = new URLSearchParams();
      Object.entries(context).forEach(([key, value]) => {
        if (value !== null && value !== undefined) {
          params.append(key, value.toString());
        }
      });
      endpoint += `?${params.toString()}`;
    }
    
    const response = await fetch(endpoint);
    const fieldSpec = await response.json();
    
    this.fieldSpecs.set(fieldName, fieldSpec);
    return fieldSpec;
  }
  
  // Mettre à jour une valeur et propager les changements
  async updateField(fieldName: string, value: any): Promise<void> {
    const oldValue = this.formData.get(fieldName);
    this.formData.set(fieldName, value);
    
    // Valider le champ modifié
    await this.validateField(fieldName);
    
    // Propager les changements aux champs dépendants
    const dependents = this.fieldDependencies.get(fieldName);
    if (dependents && oldValue !== value) {
      await this.updateDependentFields(fieldName, dependents);
    }
  }
  
  private async updateDependentFields(changedField: string, dependentFields: string[]): Promise<void> {
    const context = this.buildContext();
    
    for (const dependentField of dependentFields) {
      // Recharger la spécification avec le nouveau contexte
      await this.loadFieldSpec(dependentField, context);
      
      // Revalider le champ dépendant
      await this.validateField(dependentField);
    }
  }
  
  private buildContext(): Record<string, any> {
    return {
      projectType: this.formData.get('projectType'),
      projectLeadId: this.formData.get('projectLead'),
      teamSize: Array.isArray(this.formData.get('teamMembers')) 
        ? this.formData.get('teamMembers').length 
        : 0
    };
  }
  
  async validateField(fieldName: string): Promise<ValidationResult> {
    const fieldSpec = this.fieldSpecs.get(fieldName);
    const value = this.formData.get(fieldName);
    
    if (!fieldSpec) {
      throw new Error(`Spécification non trouvée pour le champ: ${fieldName}`);
    }
    
    const result = await this.validator.validate(fieldSpec, value);
    this.validationResults.set(fieldName, result);
    
    return result;
  }
  
  async validateForm(): Promise<{ isValid: boolean; errors: Record<string, string[]> }> {
    const errors: Record<string, string[]> = {};
    let isValid = true;
    
    // Valider tous les champs
    for (const [fieldName] of this.fieldSpecs) {
      const result = await this.validateField(fieldName);
      
      if (!result.isValid) {
        isValid = false;
        errors[fieldName] = result.errors.map(e => e.message);
      }
    }
    
    return { isValid, errors };
  }
  
  // Recherche avec cache intelligent et debouncing
  async searchValues(fieldName: string, query: string, page: number = 1): Promise<FetchValuesResult> {
    const fieldSpec = this.fieldSpecs.get(fieldName);
    
    if (!fieldSpec) {
      throw new Error(`Spécification introuvable pour le champ: ${fieldName}`);
    }
    if (!fieldSpec.valuesEndpoint) {
      throw new Error(`Pas de valuesEndpoint défini au niveau du champ (v2) pour: ${fieldName}`);
    }
    // v2: utilisation directe du fieldSpec.valuesEndpoint
    
    return this.resolver.resolveValues(fieldSpec.valuesEndpoint, {
      search: query,
      page,
      limit: fieldSpec.valuesEndpoint.requestParams?.defaultLimit || 20
    });
  }
  
  // Export des données du formulaire
  getFormData(): Record<string, any> {
    const data: Record<string, any> = {};
    this.formData.forEach((value, key) => {
      data[key] = value;
    });
    return data;
  }
  
  // Import de données existantes (mode édition)
  async setFormData(data: Record<string, any>): Promise<void> {
    // Charger d'abord toutes les spécifications de base
    await this.loadFieldSpec('projectName');
    await this.loadFieldSpec('projectType');
    await this.loadFieldSpec('projectLead');
    
    // Définir les valeurs de base
    Object.entries(data).forEach(([key, value]) => {
      this.formData.set(key, value);
    });
    
    // Charger les spécifications dépendantes avec contexte
    const context = this.buildContext();
    await this.loadFieldSpec('teamMembers', context);
    await this.loadFieldSpec('budget', context);
    
    // Valider tous les champs
    for (const [fieldName] of this.fieldSpecs) {
      await this.validateField(fieldName);
    }
  }
}

2. Composants React avancés

// Composant React avec gestion avancée
import React, { useState, useEffect, useCallback } from 'react';
import { ProjectFormManager } from './ProjectFormManager';
import { FetchHttpClient, MemoryCacheProvider } from '@cyfko/input-spec';

const ProjectForm: React.FC<{ 
  initialData?: Record<string, any>,
  onSubmit: (data: Record<string, any>) => Promise<void> 
}> = ({ initialData, onSubmit }) => {
  
  const [formManager] = useState(() => 
    new ProjectFormManager(new FetchHttpClient(), new MemoryCacheProvider())
  );
  
  const [formData, setFormData] = useState<Record<string, any>>({});
  const [validationErrors, setValidationErrors] = useState<Record<string, string[]>>({});
  const [isLoading, setIsLoading] = useState(true);
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // Initialisation du formulaire
  useEffect(() => {
    const initializeForm = async () => {
      try {
        if (initialData) {
          await formManager.setFormData(initialData);
        } else {
          // Charger les spécifications de base pour un nouveau formulaire
          await formManager.loadFieldSpec('projectName');
          await formManager.loadFieldSpec('projectType');
          await formManager.loadFieldSpec('projectLead');
        }
        
        setFormData(formManager.getFormData());
        setIsLoading(false);
      } catch (error) {
        console.error('Erreur d\'initialisation du formulaire:', error);
        setIsLoading(false);
      }
    };
    
    initializeForm();
  }, [formManager, initialData]);
  
  // Gestionnaire de changement de champ
  const handleFieldChange = useCallback(async (fieldName: string, value: any) => {
    try {
      await formManager.updateField(fieldName, value);
      setFormData(formManager.getFormData());
      
      // Effacer les erreurs de validation précédentes pour ce champ
      setValidationErrors(prev => ({
        ...prev,
        [fieldName]: []
      }));
      
    } catch (error) {
      console.error(`Erreur de mise à jour du champ ${fieldName}:`, error);
    }
  }, [formManager]);
  
  // Soumission du formulaire
  const handleSubmit = useCallback(async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      const validation = await formManager.validateForm();
      
      if (validation.isValid) {
        const data = formManager.getFormData();
        await onSubmit(data);
      } else {
        setValidationErrors(validation.errors);
      }
    } catch (error) {
      console.error('Erreur de soumission:', error);
    } finally {
      setIsSubmitting(false);
    }
  }, [formManager, onSubmit]);
  
  if (isLoading) {
    return <div className="form-loading">Chargement du formulaire...</div>;
  }
  
  return (
    <form onSubmit={handleSubmit} className="project-form">
      <ProjectNameField
        value={formData.projectName || ''}
        onChange={(value) => handleFieldChange('projectName', value)}
        errors={validationErrors.projectName}
        formManager={formManager}
      />
      
      <ProjectTypeField
        value={formData.projectType || ''}
        onChange={(value) => handleFieldChange('projectType', value)}
        errors={validationErrors.projectType}
        formManager={formManager}
      />
      
      <ProjectLeadField
        value={formData.projectLead || ''}
        onChange={(value) => handleFieldChange('projectLead', value)}
        errors={validationErrors.projectLead}
        formManager={formManager}
      />
      
      {formData.projectType && (
        <TeamMembersField
          value={formData.teamMembers || []}
          onChange={(value) => handleFieldChange('teamMembers', value)}
          errors={validationErrors.teamMembers}
          formManager={formManager}
          excludeUserId={formData.projectLead}
        />
      )}
      
      <div className="form-actions">
        <button 
          type="submit" 
          disabled={isSubmitting}
          className="submit-button"
        >
          {isSubmitting ? 'Création...' : 'Créer le projet'}
        </button>
      </div>
    </form>
  );
};

⚡ Optimisations de performance

1. Stratégies de cache avancées

// Cache provider avec éviction intelligente
export class IntelligentCacheProvider implements CacheProvider {
  private cache = new Map<string, CacheEntry>();
  private maxSize = 1000;
  private metrics = {
    hits: 0,
    misses: 0,
    evictions: 0
  };
  
  get<T>(key: string): T | null {
    const entry = this.cache.get(key);
    
    if (!entry) {
      this.metrics.misses++;
      return null;
    }
    
    if (this.isExpired(entry)) {
      this.cache.delete(key);
      this.metrics.misses++;
      return null;
    }
    
    // Mettre à jour l'usage pour LRU
    entry.lastAccessed = Date.now();
    this.metrics.hits++;
    
    return entry.value as T;
  }
  
  set<T>(key: string, value: T, ttlMs?: number): void {
    // Éviction si cache plein
    if (this.cache.size >= this.maxSize) {
      this.evictLeastRecentlyUsed();
    }
    
    const entry: CacheEntry = {
      value,
      createdAt: Date.now(),
      lastAccessed: Date.now(),
      expiresAt: ttlMs ? Date.now() + ttlMs : undefined
    };
    
    this.cache.set(key, entry);
  }
  
  private evictLeastRecentlyUsed(): void {
    let oldestKey = '';
    let oldestTime = Infinity;
    
    for (const [key, entry] of this.cache) {
      if (entry.lastAccessed < oldestTime) {
        oldestTime = entry.lastAccessed;
        oldestKey = key;
      }
    }
    
    if (oldestKey) {
      this.cache.delete(oldestKey);
      this.metrics.evictions++;
    }
  }
  
  getMetrics() {
    const total = this.metrics.hits + this.metrics.misses;
    return {
      ...this.metrics,
      hitRate: total > 0 ? this.metrics.hits / total : 0,
      size: this.cache.size
    };
  }
}

interface CacheEntry {
  value: any;
  createdAt: number;
  lastAccessed: number;
  expiresAt?: number;
}

2. Batch validation et optimisations réseau

// Validation par batch pour formulaires complexes
export class BatchValidator {
  private pendingValidations = new Map<string, Promise<ValidationResult>>();
  private validationQueue: ValidationRequest[] = [];
  private batchTimeout?: NodeJS.Timeout;
  
  async validate(
    fieldName: string, 
    fieldSpec: InputFieldSpec, 
    value: any
  ): Promise<ValidationResult> {
    
    // Si validation déjà en cours, retourner la promesse existante
    const existing = this.pendingValidations.get(fieldName);
    if (existing) {
      return existing;
    }
    
    // Pour les validations côté serveur, utiliser le batching
    if (this.requiresServerValidation(fieldSpec)) {
      return this.scheduleServerValidation(fieldName, fieldSpec, value);
    }
    
    // Validation locale immédiate
    const validator = new FieldValidator();
    return validator.validate(fieldSpec, value);
  }
  
  private scheduleServerValidation(
    fieldName: string,
    fieldSpec: InputFieldSpec,
    value: any
  ): Promise<ValidationResult> {
    
    const promise = new Promise<ValidationResult>((resolve, reject) => {
      this.validationQueue.push({
        fieldName,
        fieldSpec,
        value,
        resolve,
        reject
      });
    });
    
    this.pendingValidations.set(fieldName, promise);
    
    // Démarrer le timer de batch si pas déjà en cours
    if (!this.batchTimeout) {
      this.batchTimeout = setTimeout(() => {
        this.processBatch();
      }, 200); // Attendre 200ms pour grouper les validations
    }
    
    return promise;
  }
  
  private async processBatch(): Promise<void> {
    if (this.validationQueue.length === 0) {
      return;
    }
    
    const batch = [...this.validationQueue];
    this.validationQueue = [];
    this.batchTimeout = undefined;
    
    try {
      // Grouper par endpoint de validation
      const endpointGroups = new Map<string, ValidationRequest[]>();
      
      batch.forEach(request => {
        const endpoint = this.getValidationEndpoint(request.fieldSpec);
        if (endpoint) {
          if (!endpointGroups.has(endpoint)) {
            endpointGroups.set(endpoint, []);
          }
          endpointGroups.get(endpoint)!.push(request);
        }
      });
      
      // Exécuter les validations par groupe
      for (const [endpoint, requests] of endpointGroups) {
        await this.validateBatchForEndpoint(endpoint, requests);
      }
      
    } catch (error) {
      // En cas d'erreur, rejeter toutes les promesses
      batch.forEach(request => request.reject(error));
    } finally {
      // Nettoyer les validations en attente
      batch.forEach(request => {
        this.pendingValidations.delete(request.fieldName);
      });
    }
  }
}

🔒 Sécurité et validation robuste

1. Validation côté serveur avec sanitisation

// Java - Validation sécurisée côté serveur
@Service
public class SecureFieldValidator {
    
    private static final int MAX_STRING_LENGTH = 10000;
    private static final int MAX_ARRAY_SIZE = 1000;
    
    @Autowired
    private InputSanitizer sanitizer;
    
    public ValidationResult validateSecurely(
            InputFieldSpec fieldSpec, 
            Object value, 
            SecurityContext context) {
        
        List<ValidationError> errors = new ArrayList<>();
        
        try {
            // 1. Sanitisation préventive
            Object sanitizedValue = sanitizer.sanitize(value, fieldSpec.getDataType());
            
            // 2. Validation des limites de sécurité
            errors.addAll(validateSecurityLimits(sanitizedValue, fieldSpec));
            
            // 3. Validation des permissions contextuelles
            errors.addAll(validatePermissions(fieldSpec, context));
            
            // 4. Validation métier standard
            if (errors.isEmpty()) {
                FieldValidator validator = new FieldValidator();
                ValidationResult standardResult = validator.validate(fieldSpec, sanitizedValue);
                errors.addAll(standardResult.getErrors());
            }
            
        } catch (SecurityException e) {
            errors.add(new ValidationError("security", 
                "Violation de sécurité détectée: " + e.getMessage()));
        }
        
        return new ValidationResult(errors.isEmpty(), errors);
    }
    
    private List<ValidationError> validateSecurityLimits(Object value, InputFieldSpec fieldSpec) {
        List<ValidationError> errors = new ArrayList<>();
        
        if (fieldSpec.getDataType() == DataType.STRING && value instanceof String) {
            String str = (String) value;
            if (str.length() > MAX_STRING_LENGTH) {
                errors.add(new ValidationError("security", 
                    "Chaîne trop longue (max: " + MAX_STRING_LENGTH + ")"));
            }
        }
        
        if (fieldSpec.isExpectMultipleValues() && value instanceof List) {
            List<?> list = (List<?>) value;
            if (list.size() > MAX_ARRAY_SIZE) {
                errors.add(new ValidationError("security", 
                    "Tableau trop grand (max: " + MAX_ARRAY_SIZE + ")"));
            }
        }
        
        return errors;
    }
    
    private List<ValidationError> validatePermissions(
            InputFieldSpec fieldSpec, 
            SecurityContext context) {
        
        List<ValidationError> errors = new ArrayList<>();
        
        // Exemple: certains champs nécessitent des permissions spéciales
        if ("budget".equals(fieldSpec.getDisplayName()) && 
            !context.hasRole("BUDGET_MANAGER")) {
            errors.add(new ValidationError("permission", 
                "Permissions insuffisantes pour modifier le budget"));
        }
        
        return errors;
    }
}

📊 Monitoring et analytics

1. Métriques de performance des formulaires

// Analytics et monitoring
export class FormAnalytics {
  private metrics = {
    validationTimes: new Map<string, number[]>(),
    searchTimes: new Map<string, number[]>(),
    errorRates: new Map<string, number>(),
    abandonmentPoints: new Map<string, number>()
  };
  
  trackValidation(fieldName: string, duration: number, isValid: boolean) {
    // Enregistrer les temps de validation
    if (!this.metrics.validationTimes.has(fieldName)) {
      this.metrics.validationTimes.set(fieldName, []);
    }
    this.metrics.validationTimes.get(fieldName)!.push(duration);
    
    // Tracker le taux d'erreur
    if (!isValid) {
      const current = this.metrics.errorRates.get(fieldName) || 0;
      this.metrics.errorRates.set(fieldName, current + 1);
    }
  }
  
  trackSearchPerformance(fieldName: string, query: string, duration: number, resultCount: number) {
    if (!this.metrics.searchTimes.has(fieldName)) {
      this.metrics.searchTimes.set(fieldName, []);
    }
    this.metrics.searchTimes.get(fieldName)!.push(duration);
    
    // Envoyer à un service d'analytics
    this.sendSearchMetrics({
      fieldName,
      query: query.length, // Ne pas envoyer la query exacte pour la confidentialité
      duration,
      resultCount,
      timestamp: Date.now()
    });
  }
  
  getPerformanceReport(): FormPerformanceReport {
    const report: FormPerformanceReport = {
      fields: {}
    };
    
    for (const [fieldName, times] of this.metrics.validationTimes) {
      const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
      const errorRate = this.metrics.errorRates.get(fieldName) || 0;
      
      report.fields[fieldName] = {
        avgValidationTime: avgTime,
        errorRate: errorRate / times.length,
        totalValidations: times.length
      };
    }
    
    return report;
  }
  
  private async sendSearchMetrics(metrics: SearchMetrics) {
    try {
      await fetch('/api/analytics/search', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(metrics)
      });
    } catch (error) {
      // Fail silently pour ne pas impacter l'UX
      console.debug('Analytics send failed:', error);
    }
  }
}

interface FormPerformanceReport {
  fields: Record<string, {
    avgValidationTime: number;
    errorRate: number;
    totalValidations: number;
  }>;
}

🎯 Cas d’usage avancés

1. Formulaires conditionnels complexes

// Gestion de formulaires avec logique conditionnelle avancée
export class ConditionalFormBuilder {
  
  buildProjectForm(userRole: string, projectContext?: ProjectContext): FormConfiguration {
    const config: FormConfiguration = {
      fields: [],
      layout: 'vertical',
      submitButton: { text: 'Créer le projet', variant: 'primary' }
    };
    
    // Champs de base pour tous les utilisateurs
    config.fields.push(
      this.createField('projectName', { required: true }),
      this.createField('projectDescription', { required: false })
    );
    
    // Champs conditionnels selon le rôle
    if (userRole === 'PROJECT_MANAGER' || userRole === 'ADMIN') {
      config.fields.push(
        this.createField('projectType', { required: true }),
        this.createField('projectLead', { required: true })
      );
    }
    
    if (userRole === 'ADMIN') {
      config.fields.push(
        this.createField('budget', { required: false }),
        this.createField('priority', { required: true })
      );
    }
    
    // Adapter selon le contexte d'édition
    if (projectContext?.isEditing) {
      config.submitButton.text = 'Mettre à jour';
      // Pré-remplir les valeurs existantes
      config.initialData = projectContext.existingData;
    }
    
    return config;
  }
  
  private createField(name: string, options: FieldOptions): FieldConfiguration {
    return {
      name,
      spec: () => this.loadFieldSpec(name, options),
      validation: options.required ? 'immediate' : 'onBlur',
      layout: options.layout || 'default'
    };
  }
}

🎉 Conclusion

Ce guide intermédiaire vous a montré comment :

  • 🏗️ Architecturer des formulaires complexes avec gestion des dépendances
  • Optimiser les performances avec cache intelligent et batch validation
  • 🔒 Sécuriser vos validations côté serveur et client
  • 📊 Monitorer les performances pour optimiser l’expérience utilisateur
  • 🎯 Gérer des cas d’usage avancés avec logique conditionnelle

Prochaines étapes

  1. 🔧 Guide expert - Architecture interne et contributions
  2. 📚 Exemples concrets - Implémentations complètes
  3. 🤝 Contributions - Participer au développement

Temps estimé : 30-45 minutes • Difficulté : Intermédiaire


© 2025 Protocol Documentation. Built with Just the Docs.