generic_relation_helpers 0.1.0

Traits et helpers génériques pour jointures parent/enfant
Documentation
  • Coverage
  • 2.56%
    1 out of 39 items documented1 out of 32 items with examples
  • Size
  • Source code size: 54.38 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 2.29 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 29s Average build duration of successful builds.
  • all releases: 31s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • Dev-Skortana

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 :

// 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" }
  ]
}
-- 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

[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.

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 :

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.

// 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.

// ── 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
);
// ── 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

// 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

// 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) :

[
  {
    "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

// ── 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

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 standard)
Fichiers CSV / JSON ✅ Compatible

Licence

MIT