@attrx/role-morphic 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,10 +23,10 @@ Cada role implementa 4 operações fundamentais:
23
23
 
24
24
  ## Uso Rápido
25
25
 
26
- ### Direto (Recomendado)
26
+ ### Import Completo
27
27
 
28
28
  ```typescript
29
- import { areaRole, lengthRole, colorRole, dateRole } from '@attrx/role-morphic';
29
+ import { areaRole, lengthRole, colorRole, dateRole, currencyRole } from '@attrx/role-morphic';
30
30
 
31
31
  // Area
32
32
  areaRole.convert('hectare', 'acre', 1); // 2.47105
@@ -44,7 +44,27 @@ colorRole.format('hex', '#ff0000', { uppercase: true }); // "#FF0000"
44
44
 
45
45
  // Date
46
46
  dateRole.convert('iso', 'timestamp', '2024-12-05T00:00:00.000Z'); // 1733356800000
47
- dateRole.format('iso', '2024-12-05T00:00:00.000Z', { dateOnly: true });
47
+
48
+ // Currency (sem convert - só validate/cast/format)
49
+ currencyRole.cast('R$ 1.500,50'); // 1500.50
50
+ currencyRole.format(1500.50, { currency: 'BRL' }); // "R$1.500,50"
51
+ ```
52
+
53
+ ### Import por Pilar (Tree-Shaking)
54
+
55
+ ```typescript
56
+ // Apenas validate (zero dependências!)
57
+ import { validateArea } from '@attrx/role-morphic/area/validate';
58
+ import { validateDate } from '@attrx/role-morphic/date/validate';
59
+
60
+ // Apenas convert
61
+ import { convertLength, toBaseLength } from '@attrx/role-morphic/length/convert';
62
+
63
+ // Apenas cast
64
+ import { castCurrency, tryCastCurrency } from '@attrx/role-morphic/currency/cast';
65
+
66
+ // Apenas format
67
+ import { formatColor, formatHex } from '@attrx/role-morphic/color/format';
48
68
  ```
49
69
 
50
70
  ### Via RoleMorphic (Registry)
@@ -67,40 +87,63 @@ if (result.ok) {
67
87
  }
68
88
  ```
69
89
 
90
+ ## Estrutura de Arquivos
91
+
92
+ Cada role segue o padrão:
93
+
94
+ ```
95
+ src/roles/{role}/
96
+ ├── {Role}Role.ts # Classe + singleton
97
+ ├── constants.ts # Tipos e configurações
98
+ ├── validate.ts # Pilar validate (standalone, zero deps)
99
+ ├── convert.ts # Pilar convert
100
+ ├── cast.ts # Pilar cast
101
+ ├── format.ts # Pilar format
102
+ ├── index.ts # Exports organizados
103
+ └── {Role}Role.test.ts # Testes
104
+ ```
105
+
70
106
  ## API
71
107
 
108
+ ### Funções Standalone (por pilar)
109
+
110
+ ```typescript
111
+ // Validate
112
+ validateArea(100); // { valid: true, errors: [] }
113
+ isValidArea(100); // true
114
+
115
+ // Convert
116
+ convertArea('hectare', 'acre', 1); // 2.47105
117
+ toBaseArea('hectare', 1); // 10000 (→ m²)
118
+ fromBaseArea('acre', 10000); // 2.47105
119
+
120
+ // Cast
121
+ castArea('100 ha'); // 100
122
+ castArea('100 ha', 'acre'); // 247.105 (converte)
123
+ tryCastArea('invalid'); // { ok: false, error: '...' }
124
+
125
+ // Format
126
+ formatArea('hectare', 2.5); // "2.5 ha"
127
+ formatArea('hectare', 2.5, { verbose: true }); // "2.5 hectares"
128
+ ```
129
+
72
130
  ### Role Instance Methods
73
131
 
74
- Cada role exporta uma instância singleton (ex: `areaRole`, `lengthRole`):
75
-
76
- | Método | Retorno | Throws |
77
- |--------|---------|--------|
78
- | `convert(from, to, value)` | `T` | Sim |
79
- | `tryConvert(from, to, value)` | `Result<T>` | Não |
80
- | `cast(variant, input)` | `T \| null` | Não |
81
- | `tryCast(variant, input)` | `Result<T>` | Não |
82
- | `validate(variant, value)` | `ValidationResult` | Não |
83
- | `isValid(variant, value)` | `boolean` | Não |
84
- | `format(variant, value, options?)` | `string` | Não |
85
- | `getVariants()` | `string[]` | Não |
86
- | `hasVariant(name)` | `boolean` | Não |
87
- | `toSpec()` | `RoleSpec` | Não |
88
-
89
- ### RoleMorphic Methods
90
-
91
- | Método | Retorno | Throws |
92
- |--------|---------|--------|
93
- | `register(id, spec)` | `void` | Sim (duplicado) |
94
- | `convert(from, to, value)` | `T` | Sim |
95
- | `tryConvert(from, to, value)` | `Result<T>` | Não |
96
- | `hasRole(id)` | `boolean` | Não |
97
- | `hasVariant(fullId)` | `boolean` | Não |
98
- | `listRoles()` | `string[]` | Não |
99
- | `listVariants(roleId)` | `string[]` | Não |
132
+ ```typescript
133
+ import { areaRole } from '@attrx/role-morphic';
134
+
135
+ areaRole.convert('hectare', 'acre', 1);
136
+ areaRole.cast('hectare', '100 ha');
137
+ areaRole.validate('hectare', 100);
138
+ areaRole.format('hectare', 2.5);
139
+ areaRole.getVariants(); // ['square_meter', 'hectare', ...]
140
+ areaRole.hasVariant('hectare'); // true
141
+ areaRole.toSpec(); // RoleSpec para RoleMorphic
142
+ ```
100
143
 
101
144
  ## Roles Disponíveis
102
145
 
103
- ### SimpleRoles (Numéricas)
146
+ ### SimpleRoles (Numéricas - 4 pilares)
104
147
 
105
148
  | Role | Base | Variantes | Exemplo |
106
149
  |------|------|-----------|---------|
@@ -115,36 +158,32 @@ Cada role exporta uma instância singleton (ex: `areaRole`, `lengthRole`):
115
158
  | **Power** | watt | 11 | kw, hp, btu/h |
116
159
  | **Pressure** | pascal | 12 | bar, psi, atm |
117
160
  | **Frequency** | hertz | 9 | khz, mhz, rpm |
118
- | **Angle** | radian | 7 | degree, turn, grad |
161
+ | **Angle** | degree | 7 | radian, turn, grad |
119
162
  | **Digital** | byte | 12 | kb, mb, gb, gib |
120
163
 
121
- ### ComplexRoles (Heterogêneas)
164
+ ### ComplexRoles (Heterogêneas - 4 pilares)
122
165
 
123
166
  | Role | Base | Variantes |
124
167
  |------|------|-----------|
125
168
  | **Color** | rgb_object | hex, rgb_string, hsl_object, hsl_string |
126
169
  | **Date** | timestamp | iso, epoch |
127
170
 
171
+ ### MetadataRoles (3 pilares - sem convert)
172
+
173
+ | Role | Pilares | Moedas |
174
+ |------|---------|--------|
175
+ | **Currency** | validate, cast, format | BRL, USD, EUR, GBP, JPY, +15 |
176
+
128
177
  ## Exemplos por Role
129
178
 
130
179
  ### Area
131
180
 
132
181
  ```typescript
133
- import { areaRole } from '@attrx/role-morphic';
182
+ import { areaRole, convertArea, formatArea } from '@attrx/role-morphic';
134
183
 
135
184
  areaRole.convert('hectare', 'acre', 1); // 2.47105
136
- areaRole.convert('square_meter', 'hectare', 10000); // 1
137
- areaRole.format('hectare', 2.5, { verbose: true }); // "2.5 hectares"
138
- ```
139
-
140
- ### Length
141
-
142
- ```typescript
143
- import { lengthRole } from '@attrx/role-morphic';
144
-
145
- lengthRole.convert('mile', 'kilometer', 1); // 1.609344
146
- lengthRole.convert('inch', 'centimeter', 1); // 2.54
147
- lengthRole.cast('meter', '100 m'); // 100
185
+ convertArea('square_meter', 'hectare', 10000); // 1
186
+ formatArea('hectare', 2.5, { verbose: true }); // "2.5 hectares"
148
187
  ```
149
188
 
150
189
  ### Temperature
@@ -160,42 +199,55 @@ temperatureRole.format('celsius', 25); // "25 °C"
160
199
  ### Color
161
200
 
162
201
  ```typescript
163
- import { colorRole } from '@attrx/role-morphic';
202
+ import { colorRole, convertColor, hexToRgb } from '@attrx/role-morphic';
164
203
 
165
204
  // Hex → RGB
166
- colorRole.convert('hex', 'rgb_object', '#ff0000');
167
- // { r: 255, g: 0, b: 0, a: 1 }
168
-
169
- // RGB → HSL
170
- colorRole.convert('rgb_object', 'hsl_object', { r: 255, g: 0, b: 0, a: 1 });
171
- // { h: 0, s: 100, l: 50, a: 1 }
172
-
173
- // Format options
174
- colorRole.format('hex', '#ff0000', { uppercase: true }); // "#FF0000"
175
- colorRole.format('rgb_string', { r: 255, g: 0, b: 0, a: 1 }, { compact: true });
205
+ convertColor('hex', 'rgb_object', '#ff0000');
206
+ // { r: 255, g: 0, b: 0 }
207
+
208
+ // Convenience functions
209
+ hexToRgb('#ff0000'); // { r: 255, g: 0, b: 0 }
210
+ hexToHsl('#ff0000'); // { h: 0, s: 100, l: 50 }
211
+ isValidColor('#ff0000'); // true
212
+ isValidColor('red'); // true (named colors)
176
213
  ```
177
214
 
178
215
  ### Date
179
216
 
180
217
  ```typescript
181
- import { dateRole } from '@attrx/role-morphic';
218
+ import { dateRole, convertDate, formatDate } from '@attrx/role-morphic';
182
219
 
183
220
  // ISO → Timestamp
184
- dateRole.convert('iso', 'timestamp', '2024-12-05T00:00:00.000Z');
221
+ convertDate('iso', 'timestamp', '2024-12-05T00:00:00.000Z');
185
222
  // 1733356800000
186
223
 
187
- // Timestamp ISO
188
- dateRole.convert('timestamp', 'iso', 1733356800000);
189
- // "2024-12-05T00:00:00.000Z"
190
-
191
- // Format options
192
- dateRole.format('iso', '2024-12-05T10:30:00.000Z', {
224
+ // Format with locale
225
+ formatDate('iso', '2024-12-05T10:30:00.000Z', {
193
226
  dateStyle: 'long',
194
- timeStyle: 'short',
195
227
  locale: 'pt-BR',
196
228
  });
197
229
  ```
198
230
 
231
+ ### Currency
232
+
233
+ ```typescript
234
+ import { currencyRole, castCurrency, formatCurrency } from '@attrx/role-morphic';
235
+
236
+ // Cast (parseia string → número)
237
+ castCurrency('R$ 1.500,50'); // 1500.50
238
+ castCurrency('$1,000.00'); // 1000
239
+ castCurrency('€ 99,99'); // 99.99
240
+
241
+ // Format (número → string com moeda)
242
+ formatCurrency(1500.50, { currency: 'BRL' }); // "R$1.500,50"
243
+ formatCurrency(1000, { currency: 'USD' }); // "$1,000.00"
244
+
245
+ // Validate
246
+ currencyRole.validate(100.50); // { valid: true }
247
+ currencyRole.validate(-50); // { valid: false } (por padrão)
248
+ currencyRole.validate(-50, { allowNegative: true }); // { valid: true }
249
+ ```
250
+
199
251
  ## FormatOptions
200
252
 
201
253
  ### Base (todas as roles)
@@ -231,11 +283,25 @@ type DateFormatOptions = BaseFormatOptions & {
231
283
  };
232
284
  ```
233
285
 
286
+ ### Currency
287
+
288
+ ```typescript
289
+ type CurrencyFormatOptions = {
290
+ currency: CurrencyCode; // 'BRL', 'USD', 'EUR', etc.
291
+ locale?: string;
292
+ decimals?: number;
293
+ verbose?: boolean; // "1.500,50 Brazilian reais"
294
+ hideSymbol?: boolean;
295
+ showPositiveSign?: boolean;
296
+ };
297
+ ```
298
+
234
299
  ## Arquitetura
235
300
 
236
301
  - **Hub-and-Spoke**: Toda conversão passa pela variante base (2N vs N² funções)
237
302
  - **SimpleRole**: Para roles numéricas - usa `factors` para conversão linear
238
303
  - **ComplexRole**: Para roles heterogêneas - cada variante implementa `IVariant`
304
+ - **MetadataRole**: Para roles onde o "tipo" é metadata (ex: currency code)
239
305
 
240
306
  Ver [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) para detalhes.
241
307
 
@@ -246,21 +312,21 @@ Ver [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) para detalhes.
246
312
  ```typescript
247
313
  import { SimpleRole, SimpleUnitConfig } from '@attrx/role-morphic';
248
314
 
249
- const CURRENCY_UNITS: Record<string, SimpleUnitConfig> = {
250
- brl: { factor: 1, symbol: 'R$' },
251
- usd: { factor: 0.20, symbol: '$' }, // 1 BRL = 0.20 USD
252
- eur: { factor: 0.18, symbol: '' },
315
+ const DISTANCE_UNITS: Record<string, SimpleUnitConfig> = {
316
+ meter: { factor: 1, symbol: 'm' },
317
+ lightyear: { factor: 9.461e15, symbol: 'ly' },
318
+ parsec: { factor: 3.086e16, symbol: 'pc' },
253
319
  };
254
320
 
255
- class CurrencyRole extends SimpleRole {
256
- readonly name = 'currency';
257
- readonly base = 'brl';
258
- readonly units = CURRENCY_UNITS;
259
- readonly aliases = { real: 'brl', dollar: 'usd', euro: 'eur' };
321
+ class AstronomicalRole extends SimpleRole {
322
+ readonly name = 'astronomical';
323
+ readonly base = 'meter';
324
+ readonly units = DISTANCE_UNITS;
325
+ readonly aliases = { ly: 'lightyear' };
260
326
  }
261
327
 
262
- const currencyRole = new CurrencyRole();
263
- currencyRole.convert('brl', 'usd', 100); // 20
328
+ const astroRole = new AstronomicalRole();
329
+ astroRole.convert('lightyear', 'parsec', 1); // 0.3066
264
330
  ```
265
331
 
266
332
  ### Via RoleSpec
@@ -268,24 +334,25 @@ currencyRole.convert('brl', 'usd', 100); // 20
268
334
  ```typescript
269
335
  import { RoleMorphic, RoleSpec } from '@attrx/role-morphic';
270
336
 
271
- const currencySpec: RoleSpec<number> = {
272
- base: 'cents',
337
+ const percentSpec: RoleSpec<number> = {
338
+ base: 'decimal',
273
339
  variants: {
274
- cents: {
340
+ decimal: {
275
341
  type: 'number',
276
342
  toBase: (v) => v,
277
343
  fromBase: (v) => v,
278
344
  },
279
- dollars: {
345
+ percent: {
280
346
  type: 'number',
281
- toBase: (d) => Math.round(d * 100),
282
- fromBase: (c) => c / 100,
347
+ toBase: (p) => p / 100,
348
+ fromBase: (d) => d * 100,
283
349
  },
284
350
  },
285
351
  };
286
352
 
287
353
  const morph = new RoleMorphic();
288
- morph.register('currency', currencySpec);
354
+ morph.register('percent', percentSpec);
355
+ morph.convert('percent:percent', 'percent:decimal', 50); // 0.5
289
356
  ```
290
357
 
291
358
  ## Testes
@@ -294,7 +361,7 @@ morph.register('currency', currencySpec);
294
361
  pnpm test
295
362
  ```
296
363
 
297
- **Status:** 15 roles, 1324 testes
364
+ **Status:** 16 roles, 1407 testes
298
365
 
299
366
  ## Licença
300
367