Package Exports
- @attrx/role-morphic
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
Direto (Recomendado)
import { areaRole, lengthRole, colorRole, dateRole } 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
dateRole.format('iso', '2024-12-05T00:00:00.000Z', { dateOnly: true });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
}API
Role Instance Methods
Cada role exporta uma instância singleton (ex: areaRole, lengthRole):
| Método | Retorno | Throws |
|---|---|---|
convert(from, to, value) |
T |
Sim |
tryConvert(from, to, value) |
Result<T> |
Não |
cast(variant, input) |
T | null |
Não |
tryCast(variant, input) |
Result<T> |
Não |
validate(variant, value) |
ValidationResult |
Não |
isValid(variant, value) |
boolean |
Não |
format(variant, value, options?) |
string |
Não |
getVariants() |
string[] |
Não |
hasVariant(name) |
boolean |
Não |
toSpec() |
RoleSpec |
Não |
RoleMorphic Methods
| Método | Retorno | Throws |
|---|---|---|
register(id, spec) |
void |
Sim (duplicado) |
convert(from, to, value) |
T |
Sim |
tryConvert(from, to, value) |
Result<T> |
Não |
hasRole(id) |
boolean |
Não |
hasVariant(fullId) |
boolean |
Não |
listRoles() |
string[] |
Não |
listVariants(roleId) |
string[] |
Não |
Roles Disponíveis
SimpleRoles (Numéricas)
| 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 | radian | 7 | degree, turn, grad |
| Digital | byte | 12 | kb, mb, gb, gib |
ComplexRoles (Heterogêneas)
| Role | Base | Variantes |
|---|---|---|
| Color | rgb_object | hex, rgb_string, hsl_object, hsl_string |
| Date | timestamp | iso, epoch |
Exemplos por Role
Area
import { areaRole } from '@attrx/role-morphic';
areaRole.convert('hectare', 'acre', 1); // 2.47105
areaRole.convert('square_meter', 'hectare', 10000); // 1
areaRole.format('hectare', 2.5, { verbose: true }); // "2.5 hectares"Length
import { lengthRole } from '@attrx/role-morphic';
lengthRole.convert('mile', 'kilometer', 1); // 1.609344
lengthRole.convert('inch', 'centimeter', 1); // 2.54
lengthRole.cast('meter', '100 m'); // 100Temperature
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 } from '@attrx/role-morphic';
// Hex → RGB
colorRole.convert('hex', 'rgb_object', '#ff0000');
// { r: 255, g: 0, b: 0, a: 1 }
// RGB → HSL
colorRole.convert('rgb_object', 'hsl_object', { r: 255, g: 0, b: 0, a: 1 });
// { h: 0, s: 100, l: 50, a: 1 }
// Format options
colorRole.format('hex', '#ff0000', { uppercase: true }); // "#FF0000"
colorRole.format('rgb_string', { r: 255, g: 0, b: 0, a: 1 }, { compact: true });Date
import { dateRole } from '@attrx/role-morphic';
// ISO → Timestamp
dateRole.convert('iso', 'timestamp', '2024-12-05T00:00:00.000Z');
// 1733356800000
// Timestamp → ISO
dateRole.convert('timestamp', 'iso', 1733356800000);
// "2024-12-05T00:00:00.000Z"
// Format options
dateRole.format('iso', '2024-12-05T10:30:00.000Z', {
dateStyle: 'long',
timeStyle: 'short',
locale: 'pt-BR',
});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;
};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
Ver docs/ARCHITECTURE.md para detalhes.
Criando Roles Custom
SimpleRole
import { SimpleRole, SimpleUnitConfig } from '@attrx/role-morphic';
const CURRENCY_UNITS: Record<string, SimpleUnitConfig> = {
brl: { factor: 1, symbol: 'R$' },
usd: { factor: 0.20, symbol: '$' }, // 1 BRL = 0.20 USD
eur: { factor: 0.18, symbol: '€' },
};
class CurrencyRole extends SimpleRole {
readonly name = 'currency';
readonly base = 'brl';
readonly units = CURRENCY_UNITS;
readonly aliases = { real: 'brl', dollar: 'usd', euro: 'eur' };
}
const currencyRole = new CurrencyRole();
currencyRole.convert('brl', 'usd', 100); // 20Via RoleSpec
import { RoleMorphic, RoleSpec } from '@attrx/role-morphic';
const currencySpec: RoleSpec<number> = {
base: 'cents',
variants: {
cents: {
type: 'number',
toBase: (v) => v,
fromBase: (v) => v,
},
dollars: {
type: 'number',
toBase: (d) => Math.round(d * 100),
fromBase: (c) => c / 100,
},
},
};
const morph = new RoleMorphic();
morph.register('currency', currencySpec);Testes
pnpm testStatus: 15 roles, 1324 testes
Licença
MIT