@attrx/role-morphic
v0.2.1
Published
Polymorphic value conversion engine - transform values between multiple representations while preserving semantic identity
Maintainers
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
