# 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 :
| `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
| 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
| 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
| 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