# generic_relation_helpers
> Package Rust pour construire facilement des objets enrichis à partir de données liées entre elles — avec ou sans base de données.
---
## À qui s'adresse ce package ?
Ce package s'adresse à tout développeur Rust qui travaille avec des données réparties dans **plusieurs tables ou plusieurs listes**, et qui souhaite les **combiner proprement** pour les utiliser ou les retourner via une API.
---
## Le problème de départ — comprendre avant de coder
### Imagine une plateforme scolaire
Tu développes une application pour une école. Les élèves s'inscrivent à des cours. Ces informations sont stockées dans une base de données, réparties dans **3 tables séparées** :
```
Table Eleve
┌─────────────────┬───────────────────────┐
│ nom_complet │ email │
├─────────────────┼───────────────────────┤
│ Lucas Martin │ lucas@ecole.fr │
│ Emma Dubois │ emma@ecole.fr │
│ Noah Bernard │ noah@ecole.fr │
└─────────────────┴───────────────────────┘
Table Cours
┌──────────────────┬──────────────────────────────┐
│ intitule │ description │
├──────────────────┼──────────────────────────────┤
│ Mathématiques │ Algèbre et géométrie │
│ Informatique │ Bases de la programmation │
│ Anglais │ Langue et littérature │
└──────────────────┴──────────────────────────────┘
Table Inscription ← table de liaison (qui suit quel cours ?)
┌─────────────────┬──────────────────┐
│ nom_eleve │ intitule_cours │
├─────────────────┼──────────────────┤
│ Lucas Martin │ Mathématiques │
│ Lucas Martin │ Informatique │
│ Emma Dubois │ Informatique │
│ Emma Dubois │ Anglais │
│ Noah Bernard │ Mathématiques │
└─────────────────┴──────────────────┘
```
Ton objectif : **retourner à l'application ou à l'API** un objet qui ressemble à ça :
```json
[
{
"nom_complet": "Lucas Martin",
"email": "lucas@ecole.fr",
"cours": [
{ "intitule": "Mathématiques", "description": "Algèbre et géométrie" },
{ "intitule": "Informatique", "description": "Bases de la programmation" }
]
},
{
"nom_complet": "Emma Dubois",
"email": "emma@ecole.fr",
"cours": [
{ "intitule": "Informatique", "description": "Bases de la programmation" },
{ "intitule": "Anglais", "description": "Langue et littérature" }
]
},
{
"nom_complet": "Noah Bernard",
"email": "noah@ecole.fr",
"cours": [
{ "intitule": "Mathématiques", "description": "Algèbre et géométrie" }
]
}
]
```
Chaque élève est accompagné de la **liste détaillée de ses cours** — pas juste leurs noms, mais toutes leurs informations complètes.
---
## Sans le package — ce que tu dois écrire toi-même
Sans outil dédié, tu dois écrire **manuellement** le code qui croise les 3 listes entre elles. Voici à quoi ça ressemble :
```rust
// ── Étape 1 : charger les 3 tables séparément ──
let eleves = vec![ /* ... données Eleve ... */ ];
let cours = vec![ /* ... données Cours ... */ ];
let inscriptions = vec![ /* ... données Inscription ... */ ];
// ── Étape 2 : construire manuellement les objets combinés ──
let mut resultat: Vec<EleveAvecCours> = Vec::new();
for eleve in &eleves {
// Pour chaque élève, trouver ses cours
let mut cours_de_cet_eleve: Vec<Cours> = Vec::new();
// Parcourir la table d'inscription
for inscription in &inscriptions {
if inscription.nom_eleve == eleve.nom_complet {
// Cette inscription concerne cet élève
// Chercher le cours correspondant dans la liste des cours
for un_cours in &cours {
if un_cours.intitule == inscription.intitule_cours {
cours_de_cet_eleve.push(un_cours.clone());
}
}
}
}
// Construire l'objet combiné pour cet élève
resultat.push(EleveAvecCours {
nom_complet: eleve.nom_complet.clone(),
email: eleve.email.clone(),
cours: cours_de_cet_eleve,
});
}
```
### Les problèmes de cette approche
**Problème 1 — C'est long à écrire.**
3 boucles imbriquées pour une simple combinaison. Si ton application a 10 types de relations (élèves/cours, professeurs/classes, classes/salles...), tu écris ce code **10 fois**.
**Problème 2 — C'est fragile.**
Si tu renommes le champ `nom_eleve` en `identifiant_eleve`, tu dois retrouver et corriger ce code à la main — et espérer ne rien oublier.
**Problème 3 — Ce n'est pas réutilisable.**
Ce code ne fonctionne que pour `Eleve` et `Cours`. Pour `Professeur` et `Matiere`, tu réécriras exactement les mêmes 3 boucles avec des noms différents.
**Problème 4 — Difficile à lire.**
Un développeur qui découvre ce code doit décortiquer les 3 boucles imbriquées avant de comprendre ce que fait la fonction. L'intention — "combiner des élèves avec leurs cours" — est noyée dans les détails techniques.
---
## Avec le package — ce que tu écris à la place
```rust
use generic_relation_helpers::propers_link_childrens_with_parent::get_parents_with_their_childrens;
// ── Étape 1 : charger les 3 tables séparément (identique) ──
let eleves = vec![ /* ... données Eleve ... */ ];
let cours = vec![ /* ... données Cours ... */ ];
let inscriptions = vec![ /* ... données Inscription ... */ ];
// ── Étape 2 : une seule ligne pour tout combiner ──
let resultat: Vec<EleveAvecCours> = get_parents_with_their_childrens(
eleves, // ← les parents (élèves)
&inscriptions, // ← la table de liaison
&cours, // ← les enfants enrichis (cours complets)
None, // ← pas de filtre
None,
);
```
**C'est tout.** Le package fait les 3 boucles à ta place, de manière générique, pour n'importe quel type de données.
---
## La différence en un coup d'œil
```
Sans le package Avec le package
───────────────────────────────── ──────────────────────────────────
~20 lignes de code par relation 1 ligne par relation
À réécrire pour chaque entité Réutilisable pour tous les types
Logique mêlée au métier Logique séparée et invisible
Risque d'oubli ou d'erreur Testé et fiable
```
---
## Comment le package sait quoi combiner ?
Le package ne connaît pas tes structs à l'avance. Tu lui expliques **une seule fois**, via de petits **traits** (des fiches d'identité), comment accéder aux informations dont il a besoin.
> **C'est quoi un trait ?**
> En Rust, un trait est un contrat que tu fais signer à une struct.
> Tu dis : "cette struct sait répondre à ces questions".
> Une fois le contrat rempli, le package peut utiliser ta struct sans la connaître à l'avance.
Voici les fiches à remplir pour notre exemple école :
```rust
// ── Fiche 1 : "Comment identifier un élève ?" ──
impl GetValuePropertyOfKeyParent for Eleve {
fn get_value(&self) -> &str {
&self.nom_complet // ← la clé d'identification de l'élève
}
}
// ── Fiche 2 : "Quels champs exposer pour l'élève ?" ──
impl HasFields for Eleve {
fn field_names() -> Vec<&'static str> {
vec!["nom_complet", "email"]
}
fn get_fields(&self, name: &str) -> Value {
match name {
"nom_complet" => json!(self.nom_complet),
"email" => json!(self.email),
_ => Value::Null,
}
}
}
// ── Fiche 3 : "Comment appeler la liste des cours dans l'objet final ?" ──
impl GetValueFieldForSourceParent<Cours> for Eleve {
fn enfants_key() -> &'static str { "cours" }
// ↑ ce sera le nom de la clé dans le JSON retourné
}
// ── Fiche 4 : "Comment relier une inscription à un cours ?" ──
impl HasMatchKey for Cours {
fn get_match_key(&self) -> &str {
&self.intitule // ← le cours est identifié par son intitulé
}
}
// ── Fiche 5 : "Quelle colonne de la table Inscription pointe vers l'élève,
// et laquelle pointe vers le cours ?" ──
impl GetValuePKFieldForSourceSecondary for Inscription {
fn get_fk_field_of_source_secondary(&self) -> &str { &self.nom_eleve }
fn get_field_of_source_secondary(&self) -> &str { &self.intitule_cours }
}
// ── Fiche 6 : "Comment construire un objet Cours depuis une HashMap ?" ──
impl BuildFromSon for Cours {
fn build(fields: HashMap<String, Value>) -> Self {
Cours {
intitule: get_string(&fields, "intitule"),
description: get_string_opt(&fields, "description"),
}
}
}
// ── Fiche 7 : "Comment construire l'objet final EleveAvecCours ?" ──
impl BuildFromParent for EleveAvecCours {
fn build(fields: HashMap<String, Value>) -> Self {
EleveAvecCours {
nom_complet: get_string(&fields, "nom_complet"),
email: get_string_opt(&fields, "email"),
cours: fields.get("cours")
.and_then(|v| v.as_array()).unwrap_or(&vec![])
.iter()
.map(|v| Cours::build(
serde_json::from_value::<HashMap<String, Value>>(v.clone()).unwrap()
)).collect(),
}
}
}
```
Tu remplis ces fiches **une seule fois** par type de relation.
Ensuite, tu appelles `get_parents_with_their_childrens` autant de fois que tu veux, pour autant de relations que tu as dans ton application.
---
## Les types de relations supportées ✅
### Type 1 — Relation N:N via table de liaison
*Un élève suit plusieurs cours, un cours accueille plusieurs élèves.*
```
Eleve ──── Inscription ──── Cours
(table de liaison)
```
```rust
get_parents_with_their_childrens(eleves, &inscriptions, &cours, None, None)
```
---
### Type 2 — Relation 1:N avec FK directe dans l'enfant
*Un professeur publie plusieurs articles, chaque article appartient à un seul professeur.*
```
Professeur ──── Article
(champ nom_professeur dans Article)
```
Il n'y a pas de table de liaison séparée. `Article` contient directement le nom du professeur. On passe donc `Article` **deux fois** :
```rust
let articles = /* charger Article */;
let articles_clone = articles.clone(); // ← copie nécessaire
get_parents_with_their_childrens(professeurs, &articles, &articles_clone, None, None)
// ↑ filtre ↑ données enrichies
```
---
### Type 3 — Relation N:N avec attributs sur la liaison
*Un élève obtient une note et une date d'examen pour chaque cours — ces informations sont des attributs de la liaison, pas du cours lui-même.*
```
┌─── Cours ───────────────────────────┐
│ intitule: "Mathématiques" │
Eleve ──── Inscription ──────────────>│ description: "Algèbre et géométrie" │
note: 17 └─────────────────────────────────────┘
date_examen: 2025-06-15
```
> `note` et `date_examen` appartiennent à `Inscription`.
> `intitule` et `description` appartiennent à `Cours`.
Les attributs de `Inscription` (`note`, `date_examen`) sont **fusionnés automatiquement** dans l'objet `Cours` retourné — sans code supplémentaire de ta part. Le résultat final combine les champs des deux tables :
```json
{
"nom_complet": "Lucas Martin",
"cours": [
{
"intitule": "Mathématiques",
"description": "Algèbre et géométrie",
"note": 17,
"date_examen": "2025-06-15"
}
]
}
```
---
## Les types de relations non supportées ❌
Certaines configurations de relations entre tables sont **en dehors du périmètre** de ce package. Voici lesquelles, et pourquoi.
---
### ❌ Relation 1:1 — deux tables liées par une seule correspondance
> **Exemple :** Un utilisateur a exactement un profil, un profil appartient à exactement un utilisateur.
```
Utilisateur ──── Profil
(un seul profil par utilisateur)
```
**Pourquoi non supporté ?**
Le package est conçu pour associer un parent à une **liste** d'enfants (`Vec<TEnfant>`). Dans une relation 1:1, il n'y a pas de liste — juste un objet unique en face. Utiliser le package pour ça fonctionnerait techniquement mais retournerait un `Vec` avec un seul élément, ce qui est sémantiquement incorrect et inutilement complexe.
**Contournement :** Une simple requête avec jointure ou un accès direct suffit.
---
### ❌ Relation récursive — une table liée à elle-même
> **Exemple :** Un employé a un manager, qui est lui-même un employé de la même table.
```
Employe ──── Employe
id: 1 id: 2 (manager de l'employé 1)
manager_id: 2
```
**Pourquoi non supporté ?**
Le package distingue clairement un type "parent" et un type "enfant". Ici, `Employe` joue les deux rôles simultanément — parent et enfant sont la même struct dans la même table. Le système de traits génériques ne peut pas résoudre cette ambiguïté sans conflit de types.
**Contournement :** Une requête SQL récursive (`WITH RECURSIVE`) ou une fonction Rust dédiée qui parcourt l'arborescence manuellement.
---
### ❌ Relation ternaire — 3 tables liées simultanément par une seule liaison
> **Exemple :** Un professeur enseigne un cours dans une salle spécifique — les trois sont liés en même temps dans une seule table de liaison.
```
Professeur ──── Planning ──── Cours
│
Salle
```
```
Table Planning
┌──────────────────┬──────────────────┬───────────┐
│ nom_professeur │ intitule_cours │ nom_salle │
├──────────────────┼──────────────────┼───────────┤
│ M. Durand │ Mathématiques │ Salle A │
└──────────────────┴──────────────────┴───────────┘
```
**Pourquoi non supporté ?**
Le package est conçu pour relier **deux entités** entre elles (un parent et ses enfants). Une relation ternaire implique 3 entités liées simultanément dans la même ligne de jointure. La fonction `get_parents_with_their_childrens` n'accepte qu'un seul type de parent et un seul type d'enfant — pas trois.
**Contournement :** Décomposer en deux appels successifs — d'abord lier `Professeur` à `Cours`, puis enrichir chaque `Cours` avec sa `Salle`.
---
### ❌ Relation polymorphique — une liaison qui pointe vers plusieurs types différents
> **Exemple :** Un commentaire peut appartenir soit à un article, soit à une vidéo, soit à une photo — selon un champ `type_cible`.
```
Commentaire
cible_id: 42
type_cible: "Article" ← ou "Video", ou "Photo"
```
**Pourquoi non supporté ?**
Rust est un langage à typage statique fort. Le package doit connaître à la compilation le type exact du parent et de l'enfant. Une relation polymorphique implique que le type de la cible varie à l'exécution selon la valeur d'un champ — ce que le système de types de Rust ne peut pas résoudre avec des génériques simples.
**Contournement :** Gérer chaque type de cible séparément avec trois appels distincts, filtrés par `type_cible`.
---
### ❌ Relation avec héritage de table — parent et enfants dans des tables séparées par sous-type
> **Exemple :** Une table `Personne` avec des sous-types `Etudiant` et `Enseignant` dans des tables séparées.
```
Personne ──── Etudiant (données spécifiques à l'étudiant)
└─── Enseignant (données spécifiques à l'enseignant)
```
**Pourquoi non supporté ?**
Ce pattern (appelé "héritage de table" ou *table inheritance*) suppose qu'une même entité est décrite par plusieurs tables selon son sous-type. Le package ne gère qu'une seule table source par rôle (parent, liaison, enfant) — il ne sait pas fusionner deux tables pour reconstituer un objet complet avant de l'utiliser.
**Contournement :** Reconstituer manuellement les structs complètes avant de les passer au package, ou utiliser une vue SQL qui fusionne les tables.
---
## Résumé des cas supportés et non supportés
| N:N via table de liaison | ✅ |
| 1:N avec FK directe dans l'enfant | ✅ |
| N:N avec attributs sur la liaison | ✅ |
| 1:1 (un parent, un enfant unique) | ❌ |
| Récursive (une table liée à elle-même) | ❌ |
| Ternaire (3 entités liées en même temps)| ❌ |
| Polymorphique (type de cible variable) | ❌ |
| Héritage de table (sous-types séparés) | ❌ |
---
## Le filtrage intégré
Chaque appel peut être filtré directement, sans requête supplémentaire vers la base de données :
```rust
// Retourner uniquement Lucas Martin
get_parents_with_their_childrens(
eleves, &inscriptions, &cours,
Some("equal".to_string()),
Some("Lucas Martin".to_string())
)
// Retourner tous les élèves dont le nom commence par "E"
get_parents_with_their_childrens(
eleves, &inscriptions, &cours,
Some("begin".to_string()),
Some("E".to_string())
)
```
| `None` | Retourne tout | — |
| `"equal"` | Nom exactement égal | `"Lucas Martin"` → Lucas Martin |
| `"begin"` | Nom commence par... | `"Em"` → Emma Dubois |
| `"contains"` | Nom contient... | `"oa"` → Noah Bernard |
| `"finished"` | Nom se termine par... | `"Dubois"` → Emma Dubois |
Tous les filtres sont **insensibles à la casse** — `"lucas martin"` trouve `"Lucas Martin"`.
---
## Utilisation sans base de données
Le package fonctionne avec **n'importe quelle source de données** — pas seulement une base de données relationnelle.
```rust
// ── Depuis une API externe ──
let eleves: Vec<Eleve> = appel_api("https://api.ecole.fr/eleves").await;
let cours: Vec<Cours> = appel_api("https://api.ecole.fr/cours").await;
let inscriptions: Vec<Inscription> = appel_api("https://api.ecole.fr/inscriptions").await;
let resultat = get_parents_with_their_childrens(eleves, &inscriptions, &cours, None, None);
// ── Depuis des données en mémoire (tests unitaires) ──
let eleves = vec![
Eleve { nom_complet: "Lucas Martin".into(), email: "lucas@ecole.fr".into() }
];
let cours = vec![
Cours { intitule: "Mathématiques".into(), description: Some("Algèbre".into()) }
];
let inscriptions = vec![
Inscription { nom_eleve: "Lucas Martin".into(), intitule_cours: "Mathématiques".into() }
];
let resultat = get_parents_with_their_childrens(eleves, &inscriptions, &cours, None, None);
// ── Depuis Diesel + PostgreSQL ──
let eleves = eleve.load::<Eleve>(&mut conn).unwrap();
let cours = cours_table.load::<Cours>(&mut conn).unwrap();
let inscriptions = inscription.load::<Inscription>(&mut conn).unwrap();
let resultat = get_parents_with_their_childrens(eleves, &inscriptions, &cours, None, None);
```
Dans les trois cas, l'appel `get_parents_with_their_childrens` est **identique** — seule la source des données change.
---
## Fonctions disponibles
```rust
// ── Combiner une liste de parents avec leurs enfants ──
pub fn get_parents_with_their_childrens(
source_parent: Vec<ParentSource>, // ← ex: Vec<Eleve>
source_son: &Vec<SourceSon>, // ← ex: &Vec<Inscription> (liaison)
source_enfants: &Vec<TEnfant>, // ← ex: &Vec<Cours> (données enrichies)
type_filter: Option<String>, // ← "equal" | "begin" | "contains" | "finished" | None
filter_by_name: Option<String>, // ← valeur du filtre
) -> Vec<SourceDestination> // ← ex: Vec<EleveAvecCours>
// ── Récupérer les enfants d'un seul parent ──
pub fn get_childrens_of_parent(
parent_single: &impl GetValuePropertyOfKeyParent, // ← un seul élève
jointure_many: &Vec<TJointure>, // ← la table d'inscription
enfants_source: &Vec<TEnfant>, // ← les cours
) -> Vec<TEnfant> // ← les cours de cet élève
```
---
## Helpers de conversion inclus
Pour lire les valeurs depuis une `HashMap` dans tes implémentations de `build()` :
```rust
get_string(&fields, "nom_complet") // → String (chaîne vide si absent)
get_string_opt(&fields, "email") // → Option<String> (None si absent)
get_bool(&fields, "actif") // → bool (false si absent)
get_i64(&fields, "note") // → i64 (0 si absent)
get_f64(&fields, "moyenne") // → f64 (0.0 si absent)
get_time(&fields, "heure_cours") // → Option<NaiveTime>
get_date(&fields, "date_examen") // → Option<NaiveDate>
get_datetime(&fields, "created_at") // → Option<NaiveDateTime>
```
---
## Installation
```toml
[dependencies]
generic_relation_helpers = "0.1.0"
```
---
## Compatibilité testée
| Diesel + PostgreSQL | ✅ Testé |
| Données en mémoire (tests) | ✅ Testé |
| API externe (JSON désérialisé)| ✅ Compatible |
| SQLx | ✅ Compatible |
| Fichiers CSV / JSON | ✅ Compatible |
---
## Licence
MIT