generic_relation_helpers 0.1.0

Traits et helpers génériques pour jointures parent/enfant
Documentation
# generic_relation_helpers

> Package Rust de traits et helpers génériques pour construire des objets enrichis à partir de relations entre tables — avec ou sans base de données.

---

## Problème résolu

Dans la plupart des applications, on charge des données depuis plusieurs tables et on veut les **présenter sous forme d'objets imbriqués** :

```json
// Ce qu'on veut retourner à l'API ou utiliser dans le code
{
  "nom": "HiveHotel",
  "adresse": "12 rue des Abeilles, Paris",
  "responsables": [
    { "nom_complet": "Martin Dupont", "telephone": "06 10 11 12 13" },
    { "nom_complet": "Claire Fontaine", "telephone": "06 30 31 32 33" }
  ]
}
```

```sql
-- Ce qu'on a en base : 3 tables plates
SELECT * FROM Hotel;
SELECT * FROM HotelResponsable;   -- table de jointure
SELECT * FROM Responsable;        -- données enrichies
```

Sans ce package, cette transformation se réécrit à chaque fois pour chaque entité.  
Avec ce package, **une seule fonction générique** couvre tous les cas.

---

## Installation

```toml
[dependencies]
generic_relation_helpers = { path = "../generic_relation_helpers" }
# ou depuis crates.io
generic_relation_helpers = "0.1.0"
```

---

## Couverture des cas de relations entre tables

### ✅ Cas supportés

#### 1. Relation N:N via table de jointure

```
Hotel ──── HotelResponsable ──── Responsable
            (table de jointure)   (données enrichies)
```

Cas typiques : hôtels/responsables, étudiants/cours, utilisateurs/rôles, auteurs/livres.

```rust
get_parents_with_their_childrens(
    hotels,              // Vec<Hotel>
    &hotels_responsables, // &Vec<HotelResponsable>  ← jointure
    &responsables,        // &Vec<Responsable>        ← données enrichies
    Some("equal".to_string()),
    Some("HiveHotel".to_string()),
)
```

---

#### 2. Relation 1:N avec FK directe dans l'enfant

```
Categorie ──── Produit
               (FK: code_categorie dans Produit)
```

Pas de table de jointure — `Produit` joue les deux rôles :

```rust
get_parents_with_their_childrens(
    categories,
    &produits,   // ← source_son   (filtre via FK)
    &produits,   // ← source_enfant (données enrichies)
    None,
    None,
)
```

> **Note :** passer la même `Vec` deux fois nécessite un `.clone()` pour éviter le double emprunt Rust.

---

#### 3. N:N avec attributs sur la jointure

```
Commande ──── LigneCommande ──── Produit
               quantite: 3        nom: "Smartphone"
               prix_unitaire: 899
```

Les attributs de la jointure (`quantite`, `prix_unitaire`) sont **fusionnés automatiquement** dans `TEnfant` via `HasFields` — sans code supplémentaire dans le projet.

```rust
// LigneCommande expose ses attributs via HasFields
impl HasFields for LigneCommande {
    fn field_names() -> Vec<&'static str> {
        vec!["reference_produit", "quantite", "prix_unitaire"]
    }
    // ...
}

// Produit les reçoit via BuildFromSon
impl BuildFromSon for Produit {
    fn build(fields: HashMap<String, Value>) -> Self {
        Produit {
            reference:     get_string(&fields, "reference"),
            nom:           get_string(&fields, "nom"),
            quantite:      get_i64(&fields, "quantite"),     // ← fusionné auto
            prix_unitaire: get_f64(&fields, "prix_unitaire"), // ← fusionné auto
        }
    }
}
```

---

#### 4. N:N — gestion des droits et rôles

```
Utilisateur ──── UtilisateurRole ──── Role
                  (jointure)           niveau_acces: 100
```

Cas typiques : permissions, profils, groupes d'accès.

---

#### 5. Filtrage intégré — insensible à la casse

Cinq modes disponibles sans code supplémentaire :

| Mode | Comportement | Exemple |
|---|---|---|
| `None` | Retourne tout ||
| `"equal"` | Égalité exacte | `"HiveHotel"` |
| `"begin"` | Commence par | `"Hive"` → HiveHotel |
| `"finished"` | Termine par | `"Hotel"` → tous |
| `"contains"` | Contient | `"tem"` → ArtemHotel |

Tous les filtres sont **insensibles à la casse** (`"bhotel"` trouve `"BHotel"`).

---

### ⚠️ Cas partiellement supportés

| Cas | Statut | Contournement |
|---|---|---|
| Attributs de jointure | ⚠️ Fusionnés automatiquement via `HasFields` | Déclarer les champs dans `HasFields` de la jointure |
| FK directe (1:N) | ✅ Supporté avec `.clone()` | Passer la même `Vec` deux fois |

---

### ❌ Cas non supportés

| Cas | Raison |
|---|---|
| Relation récursive (`Employe → Manager`) | Même table en parent et enfant |
| Relation ternaire (3 tables liées simultanément) | Architecture non prévue |
| Relation 1:1 simple | Pas de `Vec` d'enfants — inutile d'utiliser le package |

---

## Utilisation sans base de données

Le package est **totalement indépendant de tout ORM ou base de données**.  
Les `Vec` peuvent venir de n'importe quelle source : JSON, CSV, API externe, fichiers, mémoire.

```rust
// ── Données depuis une API externe ──
let hotels: Vec<Hotel> = reqwest::get("https://api.example.com/hotels")
    .await?.json().await?;

let responsables: Vec<Responsable> = reqwest::get("https://api.example.com/responsables")
    .await?.json().await?;

let jointure: Vec<HotelResponsable> = reqwest::get("https://api.example.com/hotel_responsable")
    .await?.json().await?;

// ── Même appel que avec Diesel / PostgreSQL ──
let result: Vec<HotelAndResponsable> = get_parents_with_their_childrens(
    hotels, &jointure, &responsables, None, None
);
```

```rust
// ── Données en mémoire pour les tests ──
let hotels = vec![
    Hotel { nom: "HiveHotel".to_string(), adresse: Some("Paris".to_string()) }
];
let responsables = vec![
    Responsable { nom_complet: "Martin Dupont".to_string() }
];
let jointure = vec![
    HotelResponsable { nom_hotel: "HiveHotel".to_string(), nom_complet_responsable: "Martin Dupont".to_string() }
];

let result: Vec<HotelAndResponsable> = get_parents_with_their_childrens(
    hotels, &jointure, &responsables, None, None
);
```

---

## Représentation des objets obtenus

### Avant — objets plats, relations invisibles

```rust
// Ce qu'on a : 3 Vec non liées
let hotels: Vec<Hotel> = vec![
    Hotel { nom: "HiveHotel", adresse: Some("Paris") },
    Hotel { nom: "StarHotel", adresse: Some("Lyon")  },
];

let responsables: Vec<Responsable> = vec![
    Responsable { nom_complet: "Martin Dupont",   telephone: "06 10 11 12 13" },
    Responsable { nom_complet: "Claire Fontaine", telephone: "06 30 31 32 33" },
    Responsable { nom_complet: "Sophie Lefebvre", telephone: "06 40 41 42 43" },
];

let jointure: Vec<HotelResponsable> = vec![
    HotelResponsable { nom_hotel: "HiveHotel", nom_complet_responsable: "Martin Dupont"   },
    HotelResponsable { nom_hotel: "HiveHotel", nom_complet_responsable: "Claire Fontaine" },
    HotelResponsable { nom_hotel: "StarHotel", nom_complet_responsable: "Sophie Lefebvre" },
];

// ❌ Relations invisibles — il faut croiser manuellement les 3 Vec
```

### Après — objets imbriqués, relations explicites

```rust
// Ce qu'on obtient : Vec<HotelAndResponsable> prêt à l'emploi
let result: Vec<HotelAndResponsable> = get_parents_with_their_childrens(
    hotels, &jointure, &responsables, None, None
);

// result[0] → HotelAndResponsable
//   .nom       = "HiveHotel"
//   .adresse   = Some("Paris")
//   .responsables = [
//       Responsable { nom_complet: "Martin Dupont",   telephone: "06 10 11 12 13" },
//       Responsable { nom_complet: "Claire Fontaine", telephone: "06 30 31 32 33" },
//   ]

// result[1] → HotelAndResponsable
//   .nom       = "StarHotel"
//   .adresse   = Some("Lyon")
//   .responsables = [
//       Responsable { nom_complet: "Sophie Lefebvre", telephone: "06 40 41 42 43" },
//   ]
```

**Sérialisé en JSON** (retour d'API REST) :

```json
[
  {
    "nom": "HiveHotel",
    "adresse": "Paris",
    "responsables": [
      { "nom_complet": "Martin Dupont",   "telephone": "06 10 11 12 13" },
      { "nom_complet": "Claire Fontaine", "telephone": "06 30 31 32 33" }
    ]
  },
  {
    "nom": "StarHotel",
    "adresse": "Lyon",
    "responsables": [
      { "nom_complet": "Sophie Lefebvre", "telephone": "06 40 41 42 43" }
    ]
  }
]
```

---

## Traits à implémenter

Pour chaque entité, le projet déclare uniquement les traits nécessaires :

```
Struct parent        →  GetValuePropertyOfKeyParent   (expose sa clé PK)
                         GetValueFieldForSourceParent  (expose ses champs + reçoit les enfants)
                         HasFields                     (liste les champs)

Struct jointure      →  HasNomHotelAndFieldOfSourceSecondary  (FK parent + FK enfant)
                         GetValueFieldForSourceSecondary       (expose ses champs pour fusion)
                         HasFields                             (liste les champs)

Struct enfant        →  HasMatchKey     (clé de correspondance avec la jointure)
                         BuildFromSon   (construit l'enfant depuis une HashMap)
                         HasFields      (liste les champs)
                         Clone          (requis pour la copie)
                         Serialize      (requis pour la sérialisation JSON)

Struct destination   →  BuildFromParent  (construit l'objet final depuis une HashMap)
```

---

## Fonctions exposées

```rust
// ── Construit Vec<SourceDestination> depuis 3 sources ──
pub fn get_parents_with_their_childrens<ParentSource, SourceSon, TEnfant, SourceDestination>(
    source_parent:  Vec<ParentSource>,
    source_son:     &Vec<SourceSon>,    // ← table de jointure
    source_enfants: &Vec<TEnfant>,      // ← données enrichies
    type_filter:    Option<String>,     // ← "equal" | "begin" | "finished" | "contains" | None
    filter_by_name: Option<String>,     // ← valeur du filtre
) -> Vec<SourceDestination>

// ── Construit Vec<TEnfant> pour un seul parent ──
pub fn get_childrens_of_parent<TEnfant, TJointure>(
    parent_single:  &impl GetValuePropertyOfKeyParent,
    jointure_many:  &Vec<TJointure>,
    enfants_source: &Vec<TEnfant>,
) -> Vec<TEnfant>
```

---

## Helpers de conversion inclus

```rust
get_string(&fields, "nom")                // → String
get_string_opt(&fields, "adresse")        // → Option<String>
get_bool(&fields, "actif")                // → bool
get_i64(&fields, "quantite")              // → i64
get_f64(&fields, "prix")                  // → f64
get_time(&fields, "heure_ouverture")      // → Option<NaiveTime>
get_date(&fields, "date_creation")        // → Option<NaiveDate>
get_datetime(&fields, "created_at")       // → Option<NaiveDateTime>
```

---

## Compatibilité testée

| ORM / Source | Statut |
|---|---|
| Diesel + PostgreSQL | ✅ Testé |
| Données en mémoire (tests unitaires) | ✅ Testé |
| API externe (Vec désérialisée depuis JSON) | ✅ Compatible |
| SQLx | ✅ Compatible (Vec<T> standard) |
| Fichiers CSV / JSON | ✅ Compatible |

---

## Licence

MIT