JSPM

@attrx/role-morphic

0.2.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1
  • Score
    100M100P100Q37551F
  • License MIT

Polymorphic value conversion engine - transform values between multiple representations while preserving semantic identity

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-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

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 # Testes

API

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 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

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 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 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.3066

Via 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.5

Testes

pnpm test

Status: 16 roles, 1407 testes

Licença

MIT