q-type 0.1.2
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 +312 -0
- package/dist/index.cjs +602 -0
- package/dist/index.d.cts +612 -0
- package/dist/index.d.ts +612 -0
- package/dist/index.js +559 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Qtype
|
|
2
|
+
|
|
3
|
+
> Librería para parsear y serializar protocolos de mensajería basados en delimitadores (pipes, punto y coma, clave:valor). Inferencia de tipos TypeScript completa desde la definición del esquema.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Descripción
|
|
8
|
+
|
|
9
|
+
Qtype es una librería de schemas runtime que permite definir la estructura de un mensaje posicional (como los usados en protocolos bancarios, fiscales o de middleware) y obtener automáticamente:
|
|
10
|
+
|
|
11
|
+
- **Parseo tipado**: de string crudo a objetos TypeScript con tipos inferidos.
|
|
12
|
+
- **Serialización**: de objetos tipados de vuelta al formato wire.
|
|
13
|
+
- **Errores descriptivos**: con ruta, clave y valor crudo para depuración.
|
|
14
|
+
|
|
15
|
+
Está pensada para mensajes delimitados por `|` (pipes) donde cada campo ocupa una posición fija, con soporte para sub-estructuras dentro de un mismo segmento (arreglos, objetos clave:valor, tuplas).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Tecnologías
|
|
20
|
+
|
|
21
|
+
| Tecnología | Versión | Propósito |
|
|
22
|
+
| ---------- | ------- | ----------------------------------- |
|
|
23
|
+
| TypeScript | 7.x | Lenguaje, inferencia de tipos |
|
|
24
|
+
| Bun | 1.3+ | Runtime, test runner y bundler |
|
|
25
|
+
| bunup | 0.16 | Empaquetado ESM/CJS + declaraciones |
|
|
26
|
+
|
|
27
|
+
La librería **no tiene dependencias de runtime**. Solo requiere TypeScript como peerDependency para la inferencia de tipos.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Instalación
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bun add qtype
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
O con npm/pnpm:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install qtype
|
|
41
|
+
pnpm add qtype
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Uso rápido
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { q, type Static } from "qtype";
|
|
50
|
+
|
|
51
|
+
const Mensaje = q.Schema({
|
|
52
|
+
tipo: q.String(),
|
|
53
|
+
subTipo: q.String(),
|
|
54
|
+
status: q.Integer(),
|
|
55
|
+
cuenta: q.String(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
type Mensaje = Static<typeof Mensaje>;
|
|
59
|
+
// { tipo: string; subTipo: string; status: number; cuenta: string }
|
|
60
|
+
|
|
61
|
+
const parsed = Mensaje.parse("105|00|10|412345678912345678");
|
|
62
|
+
// { tipo: "105", subTipo: "00", status: 10, cuenta: "412345678912345678" }
|
|
63
|
+
|
|
64
|
+
Mensaje.serialize(parsed);
|
|
65
|
+
// "105|00|10|412345678912345678"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## API de referencia
|
|
71
|
+
|
|
72
|
+
Toda la API pública se accede mediante el namespace `q`. Cada factory retorna una instancia de `QType<T>` que expone los métodos `parse`, `safeParse` y `serialize`.
|
|
73
|
+
|
|
74
|
+
### Primitivos
|
|
75
|
+
|
|
76
|
+
| Factory | Tipo TS | Descripción |
|
|
77
|
+
| -------------------- | ---------------- | ---------------------------------------------------------------------------------- |
|
|
78
|
+
| `q.String()` | `string` | String requerido. Vacío lanza error. |
|
|
79
|
+
| `q.Number()` | `number` | Número de punto flotante. |
|
|
80
|
+
| `q.Integer(radix?)` | `number` | Entero en base `radix` (por defecto 10). |
|
|
81
|
+
| `q.Boolean(truthy?)` | `boolean` | `true` si el valor está en la lista truthy. Por defecto `["1", "true", "Y", "y"]`. |
|
|
82
|
+
| `q.Date(format?)` | `Date` | Fecha en formato `"iso"` (por defecto) o `"yyyyMMdd"`. |
|
|
83
|
+
| `q.Literal(value)` | `typeof value` | Solo acepta el valor literal exacto. |
|
|
84
|
+
| `q.Enum(values)` | `values[number]` | Restringe a un conjunto finito de strings. |
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
q.String(); // string
|
|
88
|
+
q.Number(); // number
|
|
89
|
+
q.Integer(16); // entero hexadecimal
|
|
90
|
+
q.Boolean(["S", "s"]); // true para "S" o "s"
|
|
91
|
+
q.Date("yyyyMMdd"); // formato compacto de 8 dígitos
|
|
92
|
+
q.Literal("00"); // solo "00"
|
|
93
|
+
q.Enum(["alta", "baja"]); // "alta" | "baja"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Combinadores
|
|
97
|
+
|
|
98
|
+
| Factory | Tipo TS | Descripción |
|
|
99
|
+
| ------------------------------------- | ---------------- | ------------------------------------------- |
|
|
100
|
+
| `q.Optional(schema)` | `T \| undefined` | Vacío produce `undefined`. |
|
|
101
|
+
| `q.Nullable(schema)` | `T \| null` | Vacío produce `null`. |
|
|
102
|
+
| `q.Default(schema, value)` | `T` | Vacío produce `value`. No ensancha el tipo. |
|
|
103
|
+
| `q.Transform(schema, decode, encode)` | `B` | Transforma wire ↔ app bidireccionalmente. |
|
|
104
|
+
| `q.at(index, schema)` | `T` | Asigna posición explícita en un `q.Schema`. |
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
q.Optional(q.String()); // string | undefined
|
|
108
|
+
q.Nullable(q.Integer()); // number | null
|
|
109
|
+
q.Default(q.String(), "N/A"); // string (nunca undefined)
|
|
110
|
+
q.Transform(
|
|
111
|
+
q.Number(),
|
|
112
|
+
(cents) => cents / 100, // wire → app
|
|
113
|
+
(dollars) => dollars * 100, // app → wire
|
|
114
|
+
);
|
|
115
|
+
q.at(3, q.String()); // ocupa la posición 3
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Compuestos
|
|
119
|
+
|
|
120
|
+
Estos esquemas operan sobre un **mismo segmento** usando sub-delimitadores.
|
|
121
|
+
|
|
122
|
+
| Factory | Tipo TS | Descripción |
|
|
123
|
+
| ------------------------ | --------------- | --------------------------------------------------------- |
|
|
124
|
+
| `q.Array(schema, opts?)` | `T[]` | Lista homogénea. Separador por defecto `";"`. |
|
|
125
|
+
| `q.Object(props, opts?)` | `{ [K]: T[K] }` | Pares clave:valor. Separadores por defecto `";"` y `":"`. |
|
|
126
|
+
| `q.Tuple(items, opts?)` | `[T0, T1, ...]` | Lista posicional de largo fijo con tipos heterogéneos. |
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
q.Array(q.String(), { sep: ";" });
|
|
130
|
+
// "a;b;c" → ["a", "b", "c"]
|
|
131
|
+
|
|
132
|
+
q.Object({ name: q.String(), age: q.Integer() }, { sep: ";", kvSep: ":" });
|
|
133
|
+
// "name:John;age:30" → { name: "John", age: 30 }
|
|
134
|
+
|
|
135
|
+
q.Tuple([q.String(), q.Integer(), q.String()], { sep: ";" });
|
|
136
|
+
// "John;30;NYC" → ["John", 30, "NYC"]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Schema (mensaje completo)
|
|
140
|
+
|
|
141
|
+
`q.Schema(props, opts?)` es el tipo principal. Define un mensaje completo donde cada campo ocupa una posición fija separada por `delimiter` (por defecto `"|"`).
|
|
142
|
+
|
|
143
|
+
**Posiciones automáticas** — se asignan según el orden de las claves:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const Msg = q.Schema({
|
|
147
|
+
tipo: q.String(), // posición 0
|
|
148
|
+
monto: q.Number(), // posición 1
|
|
149
|
+
ref: q.String(), // posición 2
|
|
150
|
+
});
|
|
151
|
+
Msg.parse("105|150.00|REF-001");
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Posiciones explícitas con `q.at`** — layouts sparse (solo declaras los campos que te interesan):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const Sparse = q.Schema({
|
|
158
|
+
tipo: q.at(0, q.String()),
|
|
159
|
+
monto: q.at(45, q.Number()), // salta 44 posiciones
|
|
160
|
+
uuid: q.at(199, q.String()), // salta otras 153
|
|
161
|
+
});
|
|
162
|
+
// Solo 3 campos declarados, TypeScript solo infiere esos 3
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Anidamiento** — los compuestos pueden anidarse dentro de un Schema:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const Msg = q.Schema({
|
|
169
|
+
tipo: q.String(),
|
|
170
|
+
items: q.Array(q.String(), { sep: ";" }),
|
|
171
|
+
persona: q.Object(
|
|
172
|
+
{ nombre: q.String(), edad: q.Integer() },
|
|
173
|
+
{ sep: ",", kvSep: ":" },
|
|
174
|
+
),
|
|
175
|
+
});
|
|
176
|
+
Msg.parse("105|a;b;c|nombre:Juan,edad:30");
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Uniones
|
|
180
|
+
|
|
181
|
+
`q.Union(variants, opts?)` prueba cada variante en orden y retorna la primera que decodifique correctamente.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const Msg = q.Union([
|
|
185
|
+
q.Schema({ tipo: q.Literal("00"), id: q.String() }),
|
|
186
|
+
q.Schema({ tipo: q.Literal("01"), id: q.String(), status: q.String() }),
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
Msg.parse("00|CPN-1"); // variante 00
|
|
190
|
+
Msg.parse("01|CPN-2|ok"); // variante 01
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Unión discriminada con `match()`**:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const Msg = q.Union([...], { discriminator: "tipo" });
|
|
197
|
+
|
|
198
|
+
const variant = Msg.match("00"); // QSchema | undefined
|
|
199
|
+
if (variant) {
|
|
200
|
+
variant.parse("00|CPN-1|a;b;c");
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Manejo de errores
|
|
207
|
+
|
|
208
|
+
### `parse` — lanza excepción
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
try {
|
|
212
|
+
schema.parse(raw);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (err instanceof QtypeParseError) {
|
|
215
|
+
console.log(err.message); // "[Qtype] $.status: required segment is empty (got \"\")"
|
|
216
|
+
console.log(err.path); // "status"
|
|
217
|
+
console.log(err.rawValue); // ""
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `safeParse` — no lanza
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const result = schema.safeParse(raw);
|
|
226
|
+
if (result.success) {
|
|
227
|
+
console.log(result.data);
|
|
228
|
+
} else {
|
|
229
|
+
console.log(result.error.message);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Scripts
|
|
236
|
+
|
|
237
|
+
| Script | Descripción |
|
|
238
|
+
| ------------------------- | ----------------------------------- |
|
|
239
|
+
| `bun run build` | Compila ESM + CJS + tipos a `dist/` |
|
|
240
|
+
| `bun test` | Ejecuta los tests |
|
|
241
|
+
| `bun run src/examples.ts` | Corre los 12 ejemplos de patrones |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Estructura del proyecto
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
src/
|
|
249
|
+
├── types.ts # QType<T>, DecodeContext, EncodeOptions, Static<T>, SchemaProps
|
|
250
|
+
├── errors.ts # QtypeParseError, SafeResult<T>
|
|
251
|
+
├── primitives.ts # QString, QNumber, QInteger, QBoolean, QDate, QLiteral, QEnum
|
|
252
|
+
├── combinators.ts # QOptional, QNullable, QDefault, QTransform, QPositioned
|
|
253
|
+
├── compound.ts # QArray, QObject, QTuple, QSchema
|
|
254
|
+
├── union.ts # QUnion (discriminada y no discriminada)
|
|
255
|
+
├── index.ts # API pública: exports + namespace q
|
|
256
|
+
└── examples.ts # 12 ejemplos ejecutables que cubren todos los patrones
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Patrones comunes
|
|
262
|
+
|
|
263
|
+
### Layout sparse — declarar solo los campos necesarios
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const Sparse = q.Schema({
|
|
267
|
+
tipo: q.at(0, q.String()),
|
|
268
|
+
cuenta: q.at(12, q.String()),
|
|
269
|
+
monto: q.at(45, q.Number()),
|
|
270
|
+
fecha: q.at(67, q.Date("yyyyMMdd")),
|
|
271
|
+
uuid: q.at(199, q.String()),
|
|
272
|
+
});
|
|
273
|
+
// Infiere solo { tipo, cuenta, monto, fecha, uuid }
|
|
274
|
+
// Las 194 posiciones restantes se ignoran
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Discriminación por código de mensaje
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const registry = {
|
|
281
|
+
"00": q.Schema({ code: q.at(0, q.Literal("00")), id: q.at(1, q.String()) }),
|
|
282
|
+
"01": q.Schema({
|
|
283
|
+
code: q.at(0, q.Literal("01")),
|
|
284
|
+
id: q.at(1, q.String()),
|
|
285
|
+
status: q.at(2, q.String()),
|
|
286
|
+
}),
|
|
287
|
+
} as const;
|
|
288
|
+
|
|
289
|
+
function dispatch(code: keyof typeof registry, raw: string) {
|
|
290
|
+
return registry[code].parse(raw);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Transformación wire ↔ app
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const Amount = q.Transform(
|
|
298
|
+
q.Number(),
|
|
299
|
+
(cents) => cents / 100, // wire "150000" → app 1500.00
|
|
300
|
+
(dollars) => dollars * 100, // app 1500.00 → wire "150000"
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Objetos anidados con separadores personalizados
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const Persona = q.Object(
|
|
308
|
+
{ nombre: q.String(), edad: q.Integer(), ciudad: q.Optional(q.String()) },
|
|
309
|
+
{ sep: ",", kvSep: ":" },
|
|
310
|
+
);
|
|
311
|
+
// "nombre:Ana,edad:28,ciudad:Bogotá" → { nombre: "Ana", edad: 28, ciudad: "Bogotá" }
|
|
312
|
+
```
|