# Migration Guide: From String Keys to domain-key

This guide will help you migrate from string-based keys to domain-key's type-safe key system. We'll cover common patterns, migration strategies, and best practices for a smooth transition.

## Table of Contents

1. [Why Migrate?](#why-migrate)
2. [Migration Strategy](#migration-strategy)
3. [Common Patterns](#common-patterns)
4. [Step-by-Step Migration](#step-by-step-migration)
5. [Compatibility Layer](#compatibility-layer)
6. [Testing Migration](#testing-migration)
7. [Performance Considerations](#performance-considerations)
8. [Troubleshooting](#troubleshooting)

## Why Migrate?

### Problems with String Keys

```rust
// String-based approach (error-prone)
fn get_user_order(user_id: String, order_id: String) -> Option<Order> {
    // Oops! Arguments swapped - runtime bug!
    database.get_order(order_id, user_id)
}

// No type safety
let user_id = "user_123".to_string();
let order_id = "order_456".to_string();
let product_id = "product_789".to_string();

// All these comparisons compile but may be meaningless
if user_id == order_id { /* bug! */ }
if order_id == product_id { /* bug! */ }

// Easy to mix up in collections
let mut cache: HashMap<String, String> = HashMap::new();
cache.insert(user_id, "user data");
cache.insert(order_id, "order data");
// Later: which key was which?
```

### Benefits of domain-key

```rust
// Type-safe approach
fn get_user_order(user_id: UserKey, order_id: OrderKey) -> Option<Order> {
    // Clear, type-safe interface
    database.get_order(user_id, order_id)
}

// Strong typing prevents errors
let user_id = UserKey::new("user_123")?;
let order_id = OrderKey::new("order_456")?;
let product_id = ProductKey::new("product_789")?;

// These won't compile - caught at compile time!
// if user_id == order_id { /* Compile error! */ }
// if order_id == product_id { /* Compile error! */ }

// Type-safe collections
let mut user_cache: HashMap<UserKey, UserData> = HashMap::new();
let mut order_cache: HashMap<OrderKey, OrderData> = HashMap::new();
user_cache.insert(user_id, user_data);
order_cache.insert(order_id, order_data);
// No confusion possible!
```

Key benefits at a glance:

- **Type safety**: Compiler prevents mixing keys from different domains
- **Zero-cost abstractions**: No runtime overhead for domain separation
- **Validation**: Keys are validated on creation, not scattered across your codebase
- **Ergonomic collections**: `HashMap<UserKey, V>::get("str")` works via `Borrow<str>`
- **Compile-time key validation**: `static_key!` and `const fn is_valid_key` catch invalid literals at build time, not at runtime

### Beyond String Keys

For numeric database IDs, consider `Id<D>` instead of `Key<D>`:

```rust
use domain_key::prelude::*;

define_id!(UserIdDomain => UserId);
let id = UserId::new(42).unwrap(); // NonZeroU64, 8 bytes, Copy
```

For UUID-based identifiers, use `Uuid<D>` (requires `uuid` feature):

```rust
define_uuid!(OrderUuidDomain => OrderUuid);
let uuid = OrderUuid::new();
```

## Migration Strategy

### 1. Gradual Migration (Recommended)

Migrate module by module to minimize risk:

```rust
// Phase 1: Create domain definitions alongside existing code
mod keys {
    use domain_key::{Key, Domain, KeyDomain};

    #[derive(Debug)]
    pub struct UserDomain;

    impl Domain for UserDomain {
        const DOMAIN_NAME: &'static str = "user";
    }

    impl KeyDomain for UserDomain {
        const MAX_LENGTH: usize = 32;
        const TYPICALLY_SHORT: bool = true; // Optimization hint
    }

    // Or use macros for less boilerplate:
    // use domain_key::define_domain;
    // define_domain!(UserDomain, "user");

    pub type UserKey = Key<UserDomain>;

    // Add conversion helpers
    impl UserKey {
        pub fn from_string_unchecked(s: String) -> Self {
            Self::new(s).expect("Invalid user key")
        }

        pub fn to_string(&self) -> String {
            self.as_str().to_string()
        }
    }
}

// Phase 2: Use new keys in new code
use keys::UserKey;

fn new_function(user_id: UserKey) -> Result<UserData, Error> {
    // New code uses domain keys
    Ok(UserData::load(user_id)?)
}

// Phase 3: Gradually convert existing functions
fn existing_function(user_id: String) -> Result<UserData, Error> {
    // Convert at boundary
    let user_key = UserKey::new(user_id)?;
    new_function(user_key)
}
```

### 2. Big Bang Migration

For smaller codebases, you might prefer complete migration:

```rust
// Before: String everywhere
struct UserService {
    cache: HashMap<String, UserData>,
}

impl UserService {
    fn get_user(&self, id: String) -> Option<&UserData> {
        self.cache.get(&id)
    }

    fn store_user(&mut self, id: String, data: UserData) {
        self.cache.insert(id, data);
    }
}

// After: domain-key everywhere
struct UserService {
    cache: HashMap<UserKey, UserData>,
}

impl UserService {
    fn get_user(&self, id: UserKey) -> Option<&UserData> {
        self.cache.get(&id)
    }

    fn store_user(&mut self, id: UserKey, data: UserData) {
        self.cache.insert(id, data);
    }
}
```

## Common Patterns

### Pattern 1: Simple String Replacement

**Before:**
```rust
struct User {
    id: String,
    name: String,
}

fn find_user(id: &str) -> Option<User> {
    // Database lookup
}
```

**After:**
```rust
struct User {
    id: UserKey,
    name: String,
}

fn find_user(id: UserKey) -> Option<User> {
    // Database lookup
}
```

### Pattern 2: Collections

**Before:**
```rust
let mut users: HashMap<String, User> = HashMap::new();
let mut orders: HashMap<String, Order> = HashMap::new();

// Easy to mix up keys
users.insert("123".to_string(), user);
orders.insert("123".to_string(), order); // Different "123"!
```

**After:**
```rust
let mut users: HashMap<UserKey, User> = HashMap::new();
let mut orders: HashMap<OrderKey, Order> = HashMap::new();

// Type safety prevents mixing
users.insert(UserKey::new("123")?, user);
orders.insert(OrderKey::new("123")?, order); // Different types!
```

### Pattern 3: Function Parameters

**Before:**
```rust
fn create_order(user_id: String, product_id: String, quantity: u32) -> Order {
    // Easy to swap parameters
    Order::new(user_id, product_id, quantity)
}

// Dangerous - parameters swapped!
let order = create_order(product_id, user_id, 1);
```

**After:**
```rust
fn create_order(user_id: UserKey, product_id: ProductKey, quantity: u32) -> Order {
    Order::new(user_id, product_id, quantity)
}

// This won't compile if parameters are swapped
let order = create_order(user_id, product_id, 1);
```

### Pattern 4: API Boundaries

**Before:**
```rust
#[derive(Serialize, Deserialize)]
struct ApiRequest {
    user_id: String,
    session_id: String,
}

// No validation at deserialization
```

**After:**
```rust
#[derive(Serialize, Deserialize)]
struct ApiRequest {
    user_id: UserKey,
    session_id: SessionKey,
}

// Automatic validation during deserialization!
```

## Step-by-Step Migration

### Step 1: Define Your Domains

Identify the different types of keys in your application:

```rust
// domains.rs
use domain_key::{Key, Domain, KeyDomain, KeyParseError};
use std::borrow::Cow;

// User domain
#[derive(Debug)]
pub struct UserDomain;

impl Domain for UserDomain {
    const DOMAIN_NAME: &'static str = "user";
}

impl KeyDomain for UserDomain {
    const MAX_LENGTH: usize = 32;
    const TYPICALLY_SHORT: bool = true;
    const FREQUENTLY_COMPARED: bool = true; // Often used in hash maps
}

// Or use macros for less boilerplate:
// use domain_key::define_domain;
// define_domain!(UserDomain, "user");

// Session domain
#[derive(Debug)]
pub struct SessionDomain;

impl Domain for SessionDomain {
    const DOMAIN_NAME: &'static str = "session";
}

impl KeyDomain for SessionDomain {
    const MAX_LENGTH: usize = 64;
    const HAS_CUSTOM_VALIDATION: bool = true;

    fn validate_domain_rules(key: &str) -> Result<(), KeyParseError> {
        if !key.chars().all(|c| c.is_ascii_alphanumeric()) {
            return Err(KeyParseError::domain_error(
                Self::DOMAIN_NAME,
                "Session keys must be alphanumeric"
            ));
        }
        Ok(())
    }
}

// Product domain with normalization
#[derive(Debug)]
pub struct ProductDomain;

impl Domain for ProductDomain {
    const DOMAIN_NAME: &'static str = "product";
}

impl KeyDomain for ProductDomain {
    const MAX_LENGTH: usize = 48;
    const HAS_CUSTOM_NORMALIZATION: bool = true;

    fn normalize_domain(key: Cow<'_, str>) -> Cow<'_, str> {
        // Normalize product keys to lowercase with underscores
        if key.chars().any(|c| c.is_ascii_uppercase() || c == '-' || c == ' ') {
            let normalized = key.to_ascii_lowercase().replace(['-', ' '], "_");
            Cow::Owned(normalized)
        } else {
            key
        }
    }
}

// Type aliases for easy use
pub type UserKey = Key<UserDomain>;
pub type SessionKey = Key<SessionDomain>;
pub type ProductKey = Key<ProductDomain>;

// v0.4.2+: document invariants with zero-cost compile-time assertions.
// If MAX_LENGTH changes or a rule is tightened, these break the build immediately.
const _: () = assert!(UserDomain::is_valid_key("alice"));
const _: () = assert!(SessionDomain::is_valid_key("sess_abc123"));
```

### Step 2: Create Conversion Helpers

Build bridges between old and new systems:

```rust
// conversion.rs
use crate::domains::*;

pub trait StringKeyConversion<T> {
    fn from_string_key(s: String) -> Result<T, domain_key::KeyParseError>;
    fn to_string_key(&self) -> String;
}

impl StringKeyConversion<UserKey> for UserKey {
    fn from_string_key(s: String) -> Result<UserKey, domain_key::KeyParseError> {
        UserKey::from_string(s)
    }

    fn to_string_key(&self) -> String {
        self.as_str().to_string()
    }
}

impl StringKeyConversion<SessionKey> for SessionKey {
    fn from_string_key(s: String) -> Result<SessionKey, domain_key::KeyParseError> {
        SessionKey::from_string(s)
    }

    fn to_string_key(&self) -> String {
        self.as_str().to_string()
    }
}

// Macro for easy conversion
macro_rules! convert_or_return {
    ($string_key:expr, $key_type:ty) => {
        match <$key_type>::from_string_key($string_key) {
            Ok(key) => key,
            Err(e) => return Err(e.into()),
        }
    };
}
```

### Step 3: Migrate Data Structures

Update your structs gradually:

```rust
// Before
#[derive(Debug, Clone)]
pub struct User {
    pub id: String,
    pub email: String,
    pub session_id: Option<String>,
}

// During migration - support both
#[derive(Debug, Clone)]
pub struct User {
    pub id: UserKey,           // New field
    pub email: String,
    pub session_id: Option<SessionKey>, // New field

    // Deprecated - remove in next version
    #[deprecated]
    pub legacy_id: Option<String>,
    #[deprecated]
    pub legacy_session_id: Option<String>,
}

impl User {
    // Migration constructor
    pub fn from_legacy(
        id: String,
        email: String,
        session_id: Option<String>,
    ) -> Result<Self, domain_key::KeyParseError> {
        Ok(User {
            id: UserKey::from_string(id.clone())?,
            email,
            session_id: session_id.as_ref()
                .map(|s| SessionKey::new(s))
                .transpose()?,
            legacy_id: Some(id),
            legacy_session_id: session_id,
        })
    }

    // New constructor
    pub fn new(
        id: UserKey,
        email: String,
        session_id: Option<SessionKey>,
    ) -> Self {
        User {
            id,
            email,
            session_id,
            legacy_id: None,
            legacy_session_id: None,
        }
    }
}

// After migration - clean version
#[derive(Debug, Clone)]
pub struct User {
    pub id: UserKey,
    pub email: String,
    pub session_id: Option<SessionKey>,
}
```

### Step 4: Migrate Functions

Update function signatures progressively:

```rust
// Legacy function (keep for compatibility)
pub fn get_user_legacy(id: &str) -> Result<Option<User>, Error> {
    let user_key = UserKey::new(id)?;
    get_user(user_key)
}

// New function (preferred)
pub fn get_user(id: UserKey) -> Result<Option<User>, Error> {
    // Implementation using domain keys
    database.find_user(id)
}

// Transition helper
#[deprecated(note = "Use get_user with UserKey instead")]
pub fn get_user_string(id: String) -> Result<Option<User>, Error> {
    get_user_legacy(&id)
}
```

#### Static / constant keys

Before (runtime panic if invalid):
```rust
fn anonymous_key() -> UserKey {
    UserKey::new("anonymous").expect("valid")
}
```

After (compile error if invalid, v0.4.2+):
```rust
fn anonymous_key() -> UserKey {
    static_key!(UserKey, "anonymous")
}
```

### Step 5: Update APIs

Migrate your API layer:

```rust
// API handlers
use axum::{Json, extract::Path};

// Before
async fn get_user_handler(
    Path(user_id): Path<String>
) -> Result<Json<User>, ApiError> {
    let user = get_user_legacy(&user_id)?;
    Ok(Json(user))
}

// After  
async fn get_user_handler(
    Path(user_id): Path<UserKey>
) -> Result<Json<User>, ApiError> {
    // UserKey is automatically validated during deserialization!
    let user = get_user(user_id)?;
    Ok(Json(user))
}
```

## Compatibility Layer

Create a compatibility layer for gradual migration:

```rust
// compat.rs
use crate::domains::*;

pub struct CompatUserService {
    inner: UserService, // New service using domain keys
}

impl CompatUserService {
    // Legacy methods that convert to new types
    pub fn get_user_by_string(&self, id: String) -> Result<Option<User>, Error> {
        let user_key = UserKey::from_string(id)?;
        self.inner.get_user(user_key)
    }

    pub fn create_user_from_string(
        &mut self,
        id: String,
        email: String
    ) -> Result<User, Error> {
        let user_key = UserKey::from_string(id)?;
        self.inner.create_user(user_key, email)
    }

    // Forward to new methods
    pub fn get_user(&self, id: UserKey) -> Result<Option<User>, Error> {
        self.inner.get_user(id)
    }
}
```

## Testing Migration

### Unit Tests

Test both old and new interfaces during migration:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_user_key_creation() {
        let key = UserKey::new("user_123").unwrap();
        assert_eq!(key.as_str(), "user_123");
        assert_eq!(key.domain(), "user");
    }

    #[test]
    fn test_legacy_compatibility() {
        let user_id = "user_123".to_string();
        let user_key = UserKey::from_string(user_id.clone()).unwrap();

        // Both should work the same
        let result1 = get_user_legacy(&user_id).unwrap();
        let result2 = get_user(user_key).unwrap();

        assert_eq!(result1, result2);
    }

    #[test]
    fn test_conversion_helpers() {
        let original = "user_456".to_string();
        let key = UserKey::from_string_key(original.clone()).unwrap();
        let converted_back = key.to_string_key();

        assert_eq!(original, converted_back);
    }

    #[test]
    fn test_migration_validation() {
        // Test that new validation rules work correctly
        let valid_session = SessionKey::new("abcd1234").unwrap();
        assert_eq!(valid_session.as_str(), "abcd1234");

        // Invalid session (contains special characters)
        let invalid_session = SessionKey::new("abc-123");
        assert!(invalid_session.is_err());
    }
}
```

### Integration Tests

Test the entire migration path:

```rust
#[tokio::test]
async fn test_api_migration() {
    let app = create_test_app();

    // Test legacy endpoint
    let response = app
        .get("/api/users/user_123")
        .send()
        .await;
    assert_eq!(response.status(), 200);

    // Test new endpoint (same result)
    let response = app
        .get("/api/v2/users/user_123")
        .send()
        .await;
    assert_eq!(response.status(), 200);
}

#[test]
fn test_database_migration() {
    // Test that keys work correctly with database operations
    let user_key = UserKey::new("test_user").unwrap();

    // Store user with new key system
    let user = User::new(user_key.clone(), "test@example.com".to_string(), None);
    database.store_user(&user).unwrap();

    // Retrieve using both old and new methods
    let retrieved = database.get_user(user_key).unwrap();
    assert!(retrieved.is_some());
}
```

## Performance Considerations

### Memory Usage

domain-key can be more memory-efficient than String:

```rust
use std::mem::size_of;

// String: typically 24 bytes (ptr + len + capacity)
println!("String size: {}", size_of::<String>());

// UserKey: optimized size (SmartString + cached data)
println!("UserKey size: {}", size_of::<UserKey>());

// For short keys (<=23 chars), SmartString uses stack allocation
let short_key = UserKey::new("user123").unwrap();  // Stack allocated
let long_key = UserKey::new("very_long_user_identifier_name").unwrap(); // Heap allocated
```

### Hash Performance

domain-key implements `Borrow<str>`, so you can look up entries by `&str`
without constructing a temporary key (and without the allocation + validation
that entails):

```rust
use std::collections::HashMap;

// Build a typed map
let mut users: HashMap<UserKey, UserData> = HashMap::new();
let key = UserKey::new("alice")?;
users.insert(key, alice_data);

// Lookup by &str — zero-alloc, no temporary Key needed
let found = users.get("alice");

// The pre-computed hash is still available for custom data structures:
let key = UserKey::new("alice")?;
println!("feature-selected hash: {}", key.hash());
```

> **Note:** The `Hash` trait implementation delegates to `str`'s hash so that
> the `Borrow<str>` contract is satisfied (`hash(key) == hash(key.borrow())`).
> The pre-computed hash accessible via `.hash() -> u64` uses the feature-selected
> algorithm (gxhash / ahash / blake3 / fnv-1a) and is intended for your own
> data structures or protocols, not for `HashMap` lookups.

### Validation Performance

Move validation to creation time:

```rust
// Before: Validate on every use
fn process_user(id: &str) -> Result<(), Error> {
    validate_user_id(id)?; // Validation every time
    // ... process
}

// After: Validate once at creation
fn process_user(id: UserKey) -> Result<(), Error> {
    // id is already validated!
    // ... process
}
```

### Optimized Domain Configuration

Configure domains for your performance profile:

```rust
#[derive(Debug)]
struct HighPerformanceDomain;

impl Domain for HighPerformanceDomain {
    const DOMAIN_NAME: &'static str = "fast";
}

impl KeyDomain for HighPerformanceDomain {
    const MAX_LENGTH: usize = 32;
    const EXPECTED_LENGTH: usize = 16;     // Pre-allocation hint
    const TYPICALLY_SHORT: bool = true;    // Stack allocation
    const FREQUENTLY_COMPARED: bool = true; // Hash optimizations
    const FREQUENTLY_SPLIT: bool = false;  // Disable split caching
}
```

## Troubleshooting

### Common Migration Issues

**Issue**: Compilation errors when mixing key types
```rust
let user_key = UserKey::new("123")?;
let order_key = OrderKey::new("456")?;
// if user_key == order_key { } // Won't compile!
```

**Solution**: This is intentional! Use string comparison if needed:
```rust
if user_key.as_str() == order_key.as_str() {
// Explicit string comparison
}
```

**Issue**: Serde serialization format changes
```rust
// Before: "user_123"
// After: "user_123" (same!)
```

**Solution**: domain-key serializes as strings by default, so JSON/etc. format is unchanged.

**Issue**: Database integration
```rust
// Before
diesel::insert_into(users)
.values(NewUser { id: user_id_string })
.execute(conn)?;

// After - need to convert
diesel::insert_into(users)
.values(NewUser { id: user_key.as_str() })
.execute(conn)?;
```

**Solution**: Create helper traits for database integration:
```rust
trait ToDbString {
    fn to_db_string(&self) -> &str;
}

impl ToDbString for UserKey {
    fn to_db_string(&self) -> &str {
        self.as_str()
    }
}
```

**Issue**: Performance regression during migration
```rust
// Mixed usage can hurt performance
fn mixed_usage() {
    let string_key = "user_123".to_string();
    let domain_key = UserKey::new(&string_key).unwrap(); // Extra allocation
    // Use domain_key...
}
```

**Solution**: Prefer creating domain keys early:
```rust
fn optimal_usage() {
    let domain_key = UserKey::new("user_123").unwrap(); // Direct creation
    // Use domain_key everywhere...
}
```

### Migration Checklist

- [ ] Identify all key types in your application
- [ ] Define corresponding domains with appropriate optimization hints
- [ ] Create conversion helpers for gradual migration
- [ ] Start with leaf functions (no dependencies)
- [ ] Work backwards to API boundaries
- [ ] Update database interactions
- [ ] Test thoroughly at each step
- [ ] Monitor performance during migration
- [ ] Remove compatibility layer after migration
- [ ] Update documentation
- [ ] Replace `.expect("valid")` on known-good string literals with `static_key!` or `const _: () = assert!(MyDomain::is_valid_key("…"))` to turn hidden panic paths into compile-time guarantees

### Performance Monitoring

Monitor key metrics during migration:

```rust
// Add performance monitoring
struct MigrationMetrics {
    key_creation_time: std::time::Duration,
    hash_operations: usize,
    validation_failures: usize,
}

impl MigrationMetrics {
    fn time_key_creation<F, R>(&mut self, f: F) -> R
    where F: FnOnce() -> R
    {
        let start = std::time::Instant::now();
        let result = f();
        self.key_creation_time += start.elapsed();
        result
    }
}
```

### Rolling Back

If you need to roll back migration:

```rust
// Emergency rollback helper
impl UserKey {
    pub fn emergency_to_string(&self) -> String {
        self.as_str().to_string()
    }
}

// Quickly convert back if needed
let string_id = user_key.emergency_to_string();
```

---

Happy migrating! The type safety and performance benefits are worth the effort.

## Next Steps

- Read the [Performance Guide](performance.md) for optimization strategies
- Check out the [examples/](../examples/) directory for real-world usage patterns
- Browse the [API documentation](https://docs.rs/domain-key) for complete reference
- Join our community discussions for migration support
