@attrx/role-morphic 0.1.0

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 ADDED
@@ -0,0 +1,301 @@
1
+ # RoleMorphic
2
+
3
+ Motor de conversão polimórfica de valores.
4
+
5
+ Um valor pode assumir múltiplas formas (variantes) mantendo sua identidade semântica.
6
+
7
+ ## Instalação
8
+
9
+ ```bash
10
+ pnpm add @attrx/role-morphic
11
+ ```
12
+
13
+ ## Os 4 Pilares
14
+
15
+ Cada role implementa 4 operações fundamentais:
16
+
17
+ | Pilar | Descrição | Exemplo |
18
+ |-------|-----------|---------|
19
+ | **Convert** | Transforma entre variantes | `hectare → acre` |
20
+ | **Cast** | Normaliza input sujo | `"100 ha" → 100` |
21
+ | **Validate** | Verifica regras semânticas | `área ≥ 0` |
22
+ | **Format** | Apresenta como string | `2.5 → "2.5 ha"` |
23
+
24
+ ## Uso Rápido
25
+
26
+ ### Direto (Recomendado)
27
+
28
+ ```typescript
29
+ import { areaRole, lengthRole, colorRole, dateRole } from '@attrx/role-morphic';
30
+
31
+ // Area
32
+ areaRole.convert('hectare', 'acre', 1); // 2.47105
33
+ areaRole.cast('hectare', '100 ha'); // 100
34
+ areaRole.validate('hectare', 100); // { valid: true, errors: [] }
35
+ areaRole.format('hectare', 2.5); // "2.5 ha"
36
+
37
+ // Length
38
+ lengthRole.convert('kilometer', 'mile', 100); // 62.1371
39
+ lengthRole.format('meter', 1500); // "1500 m"
40
+
41
+ // Color
42
+ colorRole.convert('hex', 'rgb_object', '#ff0000'); // { r: 255, g: 0, b: 0, a: 1 }
43
+ colorRole.format('hex', '#ff0000', { uppercase: true }); // "#FF0000"
44
+
45
+ // Date
46
+ dateRole.convert('iso', 'timestamp', '2024-12-05T00:00:00.000Z'); // 1733356800000
47
+ dateRole.format('iso', '2024-12-05T00:00:00.000Z', { dateOnly: true });
48
+ ```
49
+
50
+ ### Via RoleMorphic (Registry)
51
+
52
+ ```typescript
53
+ import { RoleMorphic, areaRole, lengthRole } from '@attrx/role-morphic';
54
+
55
+ const morph = new RoleMorphic();
56
+ morph.register('area', areaRole.toSpec());
57
+ morph.register('length', lengthRole.toSpec());
58
+
59
+ // Convert
60
+ morph.convert('area:hectare', 'area:acre', 1); // 2.47105
61
+ morph.convert('length:kilometer', 'length:mile', 100); // 62.1371
62
+
63
+ // Try (Result type)
64
+ const result = morph.tryConvert('area:hectare', 'area:acre', 1);
65
+ if (result.ok) {
66
+ console.log(result.value); // 2.47105
67
+ }
68
+ ```
69
+
70
+ ## API
71
+
72
+ ### Role Instance Methods
73
+
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 |
100
+
101
+ ## Roles Disponíveis
102
+
103
+ ### SimpleRoles (Numéricas)
104
+
105
+ | Role | Base | Variantes | Exemplo |
106
+ |------|------|-----------|---------|
107
+ | **Area** | square_meter | 12 | hectare, acre, km² |
108
+ | **Length** | meter | 17 | km, mile, foot, inch |
109
+ | **Mass** | kilogram | 16 | gram, pound, ounce |
110
+ | **Temperature** | celsius | 4 | fahrenheit, kelvin |
111
+ | **Volume** | liter | 18 | ml, gallon, cup |
112
+ | **Speed** | meter_per_second | 7 | km/h, mph, knot |
113
+ | **Time** | second | 15 | minute, hour, day |
114
+ | **Energy** | joule | 12 | calorie, kwh, btu |
115
+ | **Power** | watt | 11 | kw, hp, btu/h |
116
+ | **Pressure** | pascal | 12 | bar, psi, atm |
117
+ | **Frequency** | hertz | 9 | khz, mhz, rpm |
118
+ | **Angle** | radian | 7 | degree, turn, grad |
119
+ | **Digital** | byte | 12 | kb, mb, gb, gib |
120
+
121
+ ### ComplexRoles (Heterogêneas)
122
+
123
+ | Role | Base | Variantes |
124
+ |------|------|-----------|
125
+ | **Color** | rgb_object | hex, rgb_string, hsl_object, hsl_string |
126
+ | **Date** | timestamp | iso, epoch |
127
+
128
+ ## Exemplos por Role
129
+
130
+ ### Area
131
+
132
+ ```typescript
133
+ import { areaRole } from '@attrx/role-morphic';
134
+
135
+ 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
148
+ ```
149
+
150
+ ### Temperature
151
+
152
+ ```typescript
153
+ import { temperatureRole } from '@attrx/role-morphic';
154
+
155
+ temperatureRole.convert('celsius', 'fahrenheit', 0); // 32
156
+ temperatureRole.convert('celsius', 'kelvin', 0); // 273.15
157
+ temperatureRole.format('celsius', 25); // "25 °C"
158
+ ```
159
+
160
+ ### Color
161
+
162
+ ```typescript
163
+ import { colorRole } from '@attrx/role-morphic';
164
+
165
+ // 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 });
176
+ ```
177
+
178
+ ### Date
179
+
180
+ ```typescript
181
+ import { dateRole } from '@attrx/role-morphic';
182
+
183
+ // ISO → Timestamp
184
+ dateRole.convert('iso', 'timestamp', '2024-12-05T00:00:00.000Z');
185
+ // 1733356800000
186
+
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', {
193
+ dateStyle: 'long',
194
+ timeStyle: 'short',
195
+ locale: 'pt-BR',
196
+ });
197
+ ```
198
+
199
+ ## FormatOptions
200
+
201
+ ### Base (todas as roles)
202
+
203
+ ```typescript
204
+ type BaseFormatOptions = {
205
+ decimals?: number; // Casas decimais
206
+ locale?: string; // 'pt-BR', 'en-US'
207
+ notation?: 'standard' | 'scientific' | 'compact';
208
+ verbose?: boolean; // Nome completo vs símbolo
209
+ };
210
+ ```
211
+
212
+ ### Color
213
+
214
+ ```typescript
215
+ type ColorFormatOptions = BaseFormatOptions & {
216
+ uppercase?: boolean; // #FF0000 vs #ff0000
217
+ includeAlpha?: boolean; // Incluir alpha mesmo quando 1
218
+ compact?: boolean; // rgb(255,0,0) vs rgb(255, 0, 0)
219
+ };
220
+ ```
221
+
222
+ ### Date
223
+
224
+ ```typescript
225
+ type DateFormatOptions = BaseFormatOptions & {
226
+ dateStyle?: 'full' | 'long' | 'medium' | 'short';
227
+ timeStyle?: 'full' | 'long' | 'medium' | 'short';
228
+ timeZone?: string; // 'America/Sao_Paulo', 'UTC'
229
+ dateOnly?: boolean;
230
+ timeOnly?: boolean;
231
+ };
232
+ ```
233
+
234
+ ## Arquitetura
235
+
236
+ - **Hub-and-Spoke**: Toda conversão passa pela variante base (2N vs N² funções)
237
+ - **SimpleRole**: Para roles numéricas - usa `factors` para conversão linear
238
+ - **ComplexRole**: Para roles heterogêneas - cada variante implementa `IVariant`
239
+
240
+ Ver [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) para detalhes.
241
+
242
+ ## Criando Roles Custom
243
+
244
+ ### SimpleRole
245
+
246
+ ```typescript
247
+ import { SimpleRole, SimpleUnitConfig } from '@attrx/role-morphic';
248
+
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: '€' },
253
+ };
254
+
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' };
260
+ }
261
+
262
+ const currencyRole = new CurrencyRole();
263
+ currencyRole.convert('brl', 'usd', 100); // 20
264
+ ```
265
+
266
+ ### Via RoleSpec
267
+
268
+ ```typescript
269
+ import { RoleMorphic, RoleSpec } from '@attrx/role-morphic';
270
+
271
+ const currencySpec: RoleSpec<number> = {
272
+ base: 'cents',
273
+ variants: {
274
+ cents: {
275
+ type: 'number',
276
+ toBase: (v) => v,
277
+ fromBase: (v) => v,
278
+ },
279
+ dollars: {
280
+ type: 'number',
281
+ toBase: (d) => Math.round(d * 100),
282
+ fromBase: (c) => c / 100,
283
+ },
284
+ },
285
+ };
286
+
287
+ const morph = new RoleMorphic();
288
+ morph.register('currency', currencySpec);
289
+ ```
290
+
291
+ ## Testes
292
+
293
+ ```bash
294
+ pnpm test
295
+ ```
296
+
297
+ **Status:** 15 roles, 1324 testes
298
+
299
+ ## Licença
300
+
301
+ MIT