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