Package Exports
- @attrx/role-morphic
- @attrx/role-morphic/cast
- @attrx/role-morphic/convert
- @attrx/role-morphic/format
- @attrx/role-morphic/validate
Readme
RoleMorphic
Motor de conversão polimórfica de valores.
Um valor pode assumir múltiplas formas (variantes) mantendo sua identidade semântica.
Instalação
pnpm add @attrx/role-morphicOs 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
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)
// 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)
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 # TestesAPI
Funções Standalone (por pilar)
// 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
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 RoleMorphicRoles 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
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
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
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
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
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)
type BaseFormatOptions = {
decimals?: number; // Casas decimais
locale?: string; // 'pt-BR', 'en-US'
notation?: 'standard' | 'scientific' | 'compact';
verbose?: boolean; // Nome completo vs símbolo
};Color
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
type DateFormatOptions = BaseFormatOptions & {
dateStyle?: 'full' | 'long' | 'medium' | 'short';
timeStyle?: 'full' | 'long' | 'medium' | 'short';
timeZone?: string; // 'America/Sao_Paulo', 'UTC'
dateOnly?: boolean;
timeOnly?: boolean;
};Currency
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
factorspara 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 para detalhes.
Criando Roles Custom
SimpleRole
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.3066Via RoleSpec
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.5Testes
pnpm testStatus: 16 roles, 1407 testes
Licença
MIT