# RoleMorphic

Motor de conversão polimórfica de valores.

Um valor pode assumir múltiplas formas (variantes) mantendo sua identidade semântica.

## Instalação

```bash
pnpm add @attrx/role-morphic
```

## Os 4 Pilares

Cada role implementa 4 operações fundamentais:

| Pilar | Descrição | Exemplo |
|-------|-----------|---------|
| **Convert** | Transforma entre variantes | `hectare → acre` |
| **Cast** | Normaliza input sujo | `"100 ha" → 100` |
| **Validate** | Verifica regras semânticas | `área ≥ 0` |
| **Format** | Apresenta como string | `2.5 → "2.5 ha"` |

## Uso Rápido

### Import Completo

```typescript
import { areaRole, lengthRole, colorRole, dateRole, currencyRole } from '@attrx/role-morphic';

// Area
areaRole.convert('hectare', 'acre', 1);         // 2.47105
areaRole.cast('hectare', '100 ha');             // 100
areaRole.validate('hectare', 100);              // { valid: true, errors: [] }
areaRole.format('hectare', 2.5);                // "2.5 ha"

// Length
lengthRole.convert('kilometer', 'mile', 100);   // 62.1371
lengthRole.format('meter', 1500);               // "1500 m"

// Color
colorRole.convert('hex', 'rgb_object', '#ff0000');  // { r: 255, g: 0, b: 0, a: 1 }
colorRole.format('hex', '#ff0000', { uppercase: true });  // "#FF0000"

// Date
dateRole.convert('iso', 'timestamp', '2024-12-05T00:00:00.000Z');  // 1733356800000

// Currency (sem convert - só validate/cast/format)
currencyRole.cast('R$ 1.500,50');               // 1500.50
currencyRole.format(1500.50, { currency: 'BRL' }); // "R$1.500,50"
```

### Import por Pilar (Tree-Shaking)

```typescript
// Apenas validate (zero dependências!)
import { validateArea } from '@attrx/role-morphic/area/validate';
import { validateDate } from '@attrx/role-morphic/date/validate';

// Apenas convert
import { convertLength, toBaseLength } from '@attrx/role-morphic/length/convert';

// Apenas cast
import { castCurrency, tryCastCurrency } from '@attrx/role-morphic/currency/cast';

// Apenas format
import { formatColor, formatHex } from '@attrx/role-morphic/color/format';
```

### Via RoleMorphic (Registry)

```typescript
import { RoleMorphic, areaRole, lengthRole } from '@attrx/role-morphic';

const morph = new RoleMorphic();
morph.register('area', areaRole.toSpec());
morph.register('length', lengthRole.toSpec());

// Convert
morph.convert('area:hectare', 'area:acre', 1);        // 2.47105
morph.convert('length:kilometer', 'length:mile', 100); // 62.1371

// Try (Result type)
const result = morph.tryConvert('area:hectare', 'area:acre', 1);
if (result.ok) {
  console.log(result.value);  // 2.47105
}
```

## Estrutura de Arquivos

Cada role segue o padrão:

```
src/roles/{role}/
├── {Role}Role.ts      # Classe + singleton
├── constants.ts       # Tipos e configurações
├── validate.ts        # Pilar validate (standalone, zero deps)
├── convert.ts         # Pilar convert
├── cast.ts            # Pilar cast
├── format.ts          # Pilar format
├── index.ts           # Exports organizados
└── {Role}Role.test.ts # Testes
```

## API

### Funções Standalone (por pilar)

```typescript
// Validate
validateArea(100);                    // { valid: true, errors: [] }
isValidArea(100);                     // true

// Convert
convertArea('hectare', 'acre', 1);    // 2.47105
toBaseArea('hectare', 1);             // 10000 (→ m²)
fromBaseArea('acre', 10000);          // 2.47105

// Cast
castArea('100 ha');                   // 100
castArea('100 ha', 'acre');           // 247.105 (converte)
tryCastArea('invalid');               // { ok: false, error: '...' }

// Format
formatArea('hectare', 2.5);           // "2.5 ha"
formatArea('hectare', 2.5, { verbose: true }); // "2.5 hectares"
```

### Role Instance Methods

```typescript
import { areaRole } from '@attrx/role-morphic';

areaRole.convert('hectare', 'acre', 1);
areaRole.cast('hectare', '100 ha');
areaRole.validate('hectare', 100);
areaRole.format('hectare', 2.5);
areaRole.getVariants();               // ['square_meter', 'hectare', ...]
areaRole.hasVariant('hectare');       // true
areaRole.toSpec();                    // RoleSpec para RoleMorphic
```

## Roles Disponíveis

### SimpleRoles (Numéricas - 4 pilares)

| Role | Base | Variantes | Exemplo |
|------|------|-----------|---------|
| **Area** | square_meter | 12 | hectare, acre, km² |
| **Length** | meter | 17 | km, mile, foot, inch |
| **Mass** | kilogram | 16 | gram, pound, ounce |
| **Temperature** | celsius | 4 | fahrenheit, kelvin |
| **Volume** | liter | 18 | ml, gallon, cup |
| **Speed** | meter_per_second | 7 | km/h, mph, knot |
| **Time** | second | 15 | minute, hour, day |
| **Energy** | joule | 12 | calorie, kwh, btu |
| **Power** | watt | 11 | kw, hp, btu/h |
| **Pressure** | pascal | 12 | bar, psi, atm |
| **Frequency** | hertz | 9 | khz, mhz, rpm |
| **Angle** | degree | 7 | radian, turn, grad |
| **Digital** | byte | 12 | kb, mb, gb, gib |

### ComplexRoles (Heterogêneas - 4 pilares)

| Role | Base | Variantes |
|------|------|-----------|
| **Color** | rgb_object | hex, rgb_string, hsl_object, hsl_string |
| **Date** | timestamp | iso, epoch |

### MetadataRoles (3 pilares - sem convert)

| Role | Pilares | Moedas |
|------|---------|--------|
| **Currency** | validate, cast, format | BRL, USD, EUR, GBP, JPY, +15 |

## Exemplos por Role

### Area

```typescript
import { areaRole, convertArea, formatArea } from '@attrx/role-morphic';

areaRole.convert('hectare', 'acre', 1);           // 2.47105
convertArea('square_meter', 'hectare', 10000);    // 1
formatArea('hectare', 2.5, { verbose: true });    // "2.5 hectares"
```

### Temperature

```typescript
import { temperatureRole } from '@attrx/role-morphic';

temperatureRole.convert('celsius', 'fahrenheit', 0);   // 32
temperatureRole.convert('celsius', 'kelvin', 0);       // 273.15
temperatureRole.format('celsius', 25);                 // "25 °C"
```

### Color

```typescript
import { colorRole, convertColor, hexToRgb } from '@attrx/role-morphic';

// Hex → RGB
convertColor('hex', 'rgb_object', '#ff0000');
// { r: 255, g: 0, b: 0 }

// Convenience functions
hexToRgb('#ff0000');      // { r: 255, g: 0, b: 0 }
hexToHsl('#ff0000');      // { h: 0, s: 100, l: 50 }
isValidColor('#ff0000');  // true
isValidColor('red');      // true (named colors)
```

### Date

```typescript
import { dateRole, convertDate, formatDate } from '@attrx/role-morphic';

// ISO → Timestamp
convertDate('iso', 'timestamp', '2024-12-05T00:00:00.000Z');
// 1733356800000

// Format with locale
formatDate('iso', '2024-12-05T10:30:00.000Z', {
  dateStyle: 'long',
  locale: 'pt-BR',
});
```

### Currency

```typescript
import { currencyRole, castCurrency, formatCurrency } from '@attrx/role-morphic';

// Cast (parseia string → número)
castCurrency('R$ 1.500,50');     // 1500.50
castCurrency('$1,000.00');       // 1000
castCurrency('€ 99,99');         // 99.99

// Format (número → string com moeda)
formatCurrency(1500.50, { currency: 'BRL' }); // "R$1.500,50"
formatCurrency(1000, { currency: 'USD' });    // "$1,000.00"

// Validate
currencyRole.validate(100.50);                // { valid: true }
currencyRole.validate(-50);                   // { valid: false } (por padrão)
currencyRole.validate(-50, { allowNegative: true }); // { valid: true }
```

## FormatOptions

### Base (todas as roles)

```typescript
type BaseFormatOptions = {
  decimals?: number;      // Casas decimais
  locale?: string;        // 'pt-BR', 'en-US'
  notation?: 'standard' | 'scientific' | 'compact';
  verbose?: boolean;      // Nome completo vs símbolo
};
```

### Color

```typescript
type ColorFormatOptions = BaseFormatOptions & {
  uppercase?: boolean;    // #FF0000 vs #ff0000
  includeAlpha?: boolean; // Incluir alpha mesmo quando 1
  compact?: boolean;      // rgb(255,0,0) vs rgb(255, 0, 0)
};
```

### Date

```typescript
type DateFormatOptions = BaseFormatOptions & {
  dateStyle?: 'full' | 'long' | 'medium' | 'short';
  timeStyle?: 'full' | 'long' | 'medium' | 'short';
  timeZone?: string;      // 'America/Sao_Paulo', 'UTC'
  dateOnly?: boolean;
  timeOnly?: boolean;
};
```

### Currency

```typescript
type CurrencyFormatOptions = {
  currency: CurrencyCode; // 'BRL', 'USD', 'EUR', etc.
  locale?: string;
  decimals?: number;
  verbose?: boolean;      // "1.500,50 Brazilian reais"
  hideSymbol?: boolean;
  showPositiveSign?: boolean;
};
```

## Arquitetura

- **Hub-and-Spoke**: Toda conversão passa pela variante base (2N vs N² funções)
- **SimpleRole**: Para roles numéricas - usa `factors` para conversão linear
- **ComplexRole**: Para roles heterogêneas - cada variante implementa `IVariant`
- **MetadataRole**: Para roles onde o "tipo" é metadata (ex: currency code)

Ver [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) para detalhes.

## Criando Roles Custom

### SimpleRole

```typescript
import { SimpleRole, SimpleUnitConfig } from '@attrx/role-morphic';

const DISTANCE_UNITS: Record<string, SimpleUnitConfig> = {
  meter: { factor: 1, symbol: 'm' },
  lightyear: { factor: 9.461e15, symbol: 'ly' },
  parsec: { factor: 3.086e16, symbol: 'pc' },
};

class AstronomicalRole extends SimpleRole {
  readonly name = 'astronomical';
  readonly base = 'meter';
  readonly units = DISTANCE_UNITS;
  readonly aliases = { ly: 'lightyear' };
}

const astroRole = new AstronomicalRole();
astroRole.convert('lightyear', 'parsec', 1);  // 0.3066
```

### Via RoleSpec

```typescript
import { RoleMorphic, RoleSpec } from '@attrx/role-morphic';

const percentSpec: RoleSpec<number> = {
  base: 'decimal',
  variants: {
    decimal: {
      type: 'number',
      toBase: (v) => v,
      fromBase: (v) => v,
    },
    percent: {
      type: 'number',
      toBase: (p) => p / 100,
      fromBase: (d) => d * 100,
    },
  },
};

const morph = new RoleMorphic();
morph.register('percent', percentSpec);
morph.convert('percent:percent', 'percent:decimal', 50);  // 0.5
```

## Testes

```bash
pnpm test
```

**Status:** 16 roles, 1407 testes

## Licença

MIT
