# Story 9.2: Unification des Types Duplicats - FluidId

**Epic:** 9 - Coherence Corrections (Post-Audit)  
**Priorité:** P1-CRITIQUE  
**Estimation:** 2h  
**Statut:** done  
**Dépendances:** Aucune

---

## Story

> En tant que développeur Rust,  
> Je veux un type `FluidId` unique avec API cohérente,  
> Afin d'éviter la confusion entre `fluids::FluidId` et `components::port::FluidId`.

---

## Contexte

L'audit de cohérence a révélé que `FluidId` est défini à deux endroits avec des encapsulations différentes :

```rust
// crates/fluids/src/types.rs:35
pub struct FluidId(pub String);  // Champ public, accès direct

// crates/components/src/port.rs:137
pub struct FluidId(String);  // Champ privé, méthode as_str()
```

Cela crée une incohérence d'API qui peut causer des erreurs de compilation.

---

## Problème Actuel

1. **Deux types homonymes avec API différente**
2. **Champ public vs privé** : accès direct vs méthode `as_str()`
3. **Pas de compatibilité** entre les deux
4. **Confusion potentielle** pour les développeurs

---

## Solution Proposée

### Option retenue : Garder `FluidId` dans `entropyk_fluids`

1. **Source unique** : `crates/fluids/src/types.rs`
2. **API complète** : champ public + méthode `as_str()` pour compatibilité
3. **Supprimer** la définition de `port.rs`
4. **Ré-exporter** depuis `entropyk_components`

### Implémentation

```rust
// crates/fluids/src/types.rs

/// Identifiant d'un fluide réfrigérant.
/// 
/// Exemples: "R134a", "R410A", "R744", "Water", "Air"
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FluidId(pub String);

impl FluidId {
    /// Crée un FluidId depuis une chaîne.
    pub fn new(name: impl Into<String>) -> Self {
        Self(name.into())
    }
    
    /// Retourne le nom du fluide comme slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }
    
    /// Consomme le FluidId et retourne le String interne.
    pub fn into_inner(self) -> String {
        self.0
    }
}

impl From<&str> for FluidId {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

impl From<String> for FluidId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl std::fmt::Display for FluidId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl AsRef<str> for FluidId {
    fn as_ref(&self) -> &str {
        &self.0
    }
}
```

---

## Fichiers à Modifier

| Fichier | Action |
|---------|--------|
| `crates/fluids/src/types.rs` | Ajouter méthodes `as_str()`, `into_inner()`, `AsRef<str>` |
| `crates/fluids/src/lib.rs` | Vérifier export `FluidId` |
| `crates/components/src/port.rs` | Supprimer définition locale, ré-importer |
| `crates/components/src/lib.rs` | Ré-exporter `FluidId` depuis `entropyk_fluids` |

---

## Critères d'Acceptation

- [x] Un seul `FluidId` dans la codebase (dans `entropyk_fluids`)
- [x] Champ `0` public pour accès direct
- [x] Méthode `as_str()` disponible
- [x] Méthode `into_inner()` disponible
- [x] `From<&str>` et `From<String>` implémentés
- [x] `AsRef<str>` implémenté
- [x] `Display` implémenté
- [x] `cargo test --workspace` passe
- [x] `cargo clippy -- -D warnings` passe

---

## Tests Requis

```rust
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_new() {
        let id = FluidId::new("R134a");
        assert_eq!(id.0, "R134a");
    }
    
    #[test]
    fn test_from_str() {
        let id: FluidId = "R410A".into();
        assert_eq!(id.0, "R410A");
    }
    
    #[test]
    fn test_from_string() {
        let id: FluidId = String::from("R744").into();
        assert_eq!(id.0, "R744");
    }
    
    #[test]
    fn test_as_str() {
        let id = FluidId::new("Water");
        assert_eq!(id.as_str(), "Water");
    }
    
    #[test]
    fn test_into_inner() {
        let id = FluidId::new("Air");
        let inner = id.into_inner();
        assert_eq!(inner, "Air");
    }
    
    #[test]
    fn test_as_ref() {
        let id = FluidId::new("R1234yf");
        let s: &str = id.as_ref();
        assert_eq!(s, "R1234yf");
    }
    
    #[test]
    fn test_display() {
        let id = FluidId::new("R32");
        assert_eq!(format!("{}", id), "R32");
    }
}
```

---

## Risques et Mitigations

| Risque | Mitigation |
|--------|------------|
| Breaking change pour code existant | Vérifier tous les usages avant suppression |
| Import circulaire | `entropyk_components` dépend déjà de `entropyk_fluids` |

---

## Références

- [Architecture.md - Type System](../planning-artifacts/architecture.md#Type-System)
- [Coherence Audit Report](./coherence-audit-remediation-plan.md)

---

## File List

| File | Action |
|------|--------|
| `crates/fluids/src/types.rs` | Modified - Added `as_str()`, `into_inner()`, `AsRef<str>`, `Display`, and comprehensive tests |
| `crates/components/src/port.rs` | Modified - Removed duplicate `FluidId`, added re-export from `entropyk_fluids` |

---

## Dev Agent Record

### Completion Notes (2026-02-22)

- **Centralized FluidId**: Unified type now lives exclusively in `crates/fluids/src/types.rs`.
- **API Enhanced**: Added `as_str()`, `into_inner()`, `AsRef<str>`, and `Display` to `FluidId`.
- **Derives Added**: Added `PartialOrd`, `Ord`, `Serialize`, `Deserialize` for full functionality.
- **Removed Duplication**: Deleted the local `FluidId` in `crates/components/src/port.rs` and replaced it with a re-export of `entropyk_fluids::FluidId`.
- **Verified**: All workspace tests and doc-tests pass.

### Code Review Fixes (2026-02-22)

- **Added Missing Tests**: Implemented all 7 required tests from story specification:
  - `test_new()`, `test_from_str()`, `test_from_string()`, `test_as_str()`, `test_into_inner()`, `test_as_ref()`, `test_display()`
- **Test Verification**: All 11 tests in `types::tests` module pass.

---

## Change Log

| Date | Change |
|------|--------|
| 2026-02-22 | Initial implementation - FluidId unification complete |
| 2026-02-22 | Code review - Added missing unit tests for FluidId |
