svelte-reflector 2.5.1 → 2.5.3

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.
@@ -22,6 +22,15 @@ export class SchemaClassRenderer {
22
22
  const keys = [];
23
23
  const bundleParams = [];
24
24
  let staticMethod = "";
25
+ // Optional, always-instantiated sub-DTOs (`nome? = $state<T>(new T)`): the
26
+ // runtime gate reads this set to skip an empty optional block instead of
27
+ // flagging its inner `required` fields. Emitted only when non-empty.
28
+ const optionalDtoNames = objectProps
29
+ .map((prop) => prop.optionalGateName())
30
+ .filter((name) => name !== null);
31
+ const optionalDtosDecl = optionalDtoNames.length
32
+ ? `readonly _optionalDtos = new Set<string>([${optionalDtoNames.map((name) => `"${name}"`).join(", ")}])`
33
+ : "";
25
34
  primitiveProps.forEach((prop) => {
26
35
  constructorThis.push(prop.constructorBuild());
27
36
  bundleParams.push(prop.bundleBuild());
@@ -93,7 +102,7 @@ export class SchemaClassRenderer {
93
102
  : responseBundle;
94
103
  const schema = `
95
104
  export class ${name} {
96
- ${keys.join(";")}
105
+ ${[...keys, optionalDtosDecl].filter(Boolean).join(";\n ")}
97
106
 
98
107
  ${constructorCode}
99
108
 
@@ -31,7 +31,7 @@ export class SchemaPropertyClassifier {
31
31
  isNullable: value.nullable,
32
32
  });
33
33
  }
34
- return SchemaPropertyClassifier.classifyObject({ key, value });
34
+ return SchemaPropertyClassifier.classifyObject({ key, value, requireds });
35
35
  }
36
36
  const required = requireds.includes(key);
37
37
  const items = value.items;
@@ -107,10 +107,10 @@ export class SchemaPropertyClassifier {
107
107
  });
108
108
  }
109
109
  static classifyObject(params) {
110
- const { value, key } = params;
110
+ const { value, key, requireds } = params;
111
111
  if ("allOf" in value) {
112
112
  const ref = value.allOf?.[0];
113
- const isRequired = !!value.nullable;
113
+ const isRequired = requireds.includes(key);
114
114
  const nullable = !!value.nullable;
115
115
  if (ref && isReferenceObject(ref)) {
116
116
  return new ObjectProp({ name: key, referenceObject: ref, isRequired, isNullable: nullable });
@@ -118,7 +118,7 @@ export class SchemaPropertyClassifier {
118
118
  return null;
119
119
  }
120
120
  if (isReferenceObject(value)) {
121
- return new ObjectProp({ name: key, referenceObject: value });
121
+ return new ObjectProp({ name: key, referenceObject: value, isRequired: requireds.includes(key) });
122
122
  }
123
123
  return null;
124
124
  }
@@ -14,5 +14,14 @@ export declare class ObjectProp {
14
14
  classBuild(): string;
15
15
  interfaceBuild(): string;
16
16
  bundleBuild(): string;
17
+ /**
18
+ * Field name when this sub-DTO is emitted as optional AND always-instantiated
19
+ * (`nome? = $state<T>(new T)` — `!required && !nullable`), so the client-side
20
+ * gate (`validateForm`) must skip it when empty instead of validating its inner
21
+ * `required` fields as mandatory. `null` for required or nullable sub-DTOs (the
22
+ * latter defaults to `null` and is already skipped at runtime). Mirrors the `?`
23
+ * modifier `classBuild` emits, keeping the runtime gate consistent with the type.
24
+ */
25
+ optionalGateName(): string | null;
17
26
  hydrateBuild(): string;
18
27
  }
@@ -7,7 +7,7 @@ export class ObjectProp {
7
7
  const { referenceObject, name, isRequired, isNullable } = params;
8
8
  this.name = name;
9
9
  this.type = referenceObject.$ref.split("/").at(-1) ?? "";
10
- this.required = isRequired ?? true; // tem que ver isso daí
10
+ this.required = isRequired ?? false;
11
11
  this.isNullable = !!isNullable;
12
12
  }
13
13
  constructorBuild() {
@@ -31,6 +31,17 @@ export class ObjectProp {
31
31
  const nullable = this.isNullable ? "?? null" : "";
32
32
  return `${this.name}: this.${this.name}?.bundle() ${nullable}`;
33
33
  }
34
+ /**
35
+ * Field name when this sub-DTO is emitted as optional AND always-instantiated
36
+ * (`nome? = $state<T>(new T)` — `!required && !nullable`), so the client-side
37
+ * gate (`validateForm`) must skip it when empty instead of validating its inner
38
+ * `required` fields as mandatory. `null` for required or nullable sub-DTOs (the
39
+ * latter defaults to `null` and is already skipped at runtime). Mirrors the `?`
40
+ * modifier `classBuild` emits, keeping the runtime gate consistent with the type.
41
+ */
42
+ optionalGateName() {
43
+ return !this.required && !this.isNullable ? this.name : null;
44
+ }
34
45
  hydrateBuild() {
35
46
  if (this.isNullable) {
36
47
  return `if (data.${this.name} !== undefined) {
@@ -74,6 +74,7 @@ export class BuildedInput<T> {
74
74
  private _value = $state<T>(null as any);
75
75
  private serverErrorMessage = $state<string | null>(null);
76
76
  private serverErrorValue = $state.raw<T | null>(null);
77
+ showError = $state(false);
77
78
  sanitizer?: Sanitizer;
78
79
  required: boolean;
79
80
  nullable: boolean;
@@ -180,6 +181,14 @@ export class BuildedInput<T> {
180
181
  return this.validator(this.value);
181
182
  }
182
183
 
184
+ touch(): void {
185
+ this.showError = true;
186
+ }
187
+
188
+ untouch(): void {
189
+ this.showError = false;
190
+ }
191
+
183
192
  setServerError(message: string) {
184
193
  this.serverErrorMessage = message;
185
194
  this.serverErrorValue = this.value;
@@ -241,6 +250,11 @@ export function build<T>(params: {
241
250
  return new BuildedInput(params);
242
251
  }
243
252
 
253
+ /**
254
+ * @deprecated Use `validateForm` — `isFormValid` faz `throw` no primeiro campo
255
+ * inválido (nunca retorna `false`), ignora o flag `required` e muta o schema
256
+ * (`delete schema.bundle`). Remoção real fica para o próximo major.
257
+ */
244
258
  export function isFormValid<T>(schema: PartialBuildedInput<T>): boolean {
245
259
  delete (schema as { bundle?: unknown }).bundle;
246
260
 
@@ -266,6 +280,93 @@ export function isFormValid<T>(schema: PartialBuildedInput<T>): boolean {
266
280
  return isValid;
267
281
  }
268
282
 
283
+ function isNestedDto(v: unknown): v is Record<string, unknown> {
284
+ return v != null && typeof v === "object" && typeof (v as { bundle?: unknown }).bundle === "function";
285
+ }
286
+
287
+ function isInputEmpty(input: BuildedInput<unknown>): boolean {
288
+ return input.value === "" || input.value === null || input.value === undefined;
289
+ }
290
+
291
+ /**
292
+ * Visita os `BuildedInput` do schema, recursando em DTO aninhado (campo com
293
+ * `.bundle()`) e em array de DTOs — simétrico ao `bundleInputs`/`bundle`, que
294
+ * também recursam. Sem short-circuit: aplica `fn` em cada campo.
295
+ *
296
+ * `skipOptionalEmpty`: pula um sub-DTO listado no `_optionalDtos` do pai cujos
297
+ * campos estão TODOS vazios. O codegen marca os sub-DTOs **opcionais
298
+ * always-instantiated** (`nome? = $state<T>(new T)`); sem isso o gate validaria
299
+ * o `required` interno de um bloco opcional em branco e bloquearia o submit.
300
+ */
301
+ function forEachBuildedInput(
302
+ schema: object,
303
+ fn: (input: BuildedInput<unknown>) => void,
304
+ skipOptionalEmpty: boolean,
305
+ ): void {
306
+ const optional = skipOptionalEmpty
307
+ ? (schema as { _optionalDtos?: Set<string> })._optionalDtos
308
+ : undefined;
309
+ for (const [key, v] of Object.entries(schema)) {
310
+ if (isBuildedInput(v)) {
311
+ fn(v);
312
+ } else if (Array.isArray(v)) {
313
+ for (const item of v) {
314
+ if (isBuildedInput(item)) fn(item);
315
+ else if (isNestedDto(item)) forEachBuildedInput(item, fn, skipOptionalEmpty);
316
+ }
317
+ } else if (isNestedDto(v)) {
318
+ if (optional?.has(key) && isEmptyDto(v)) continue;
319
+ forEachBuildedInput(v, fn, skipOptionalEmpty);
320
+ }
321
+ }
322
+ }
323
+
324
+ function isEmptyDto(dto: object): boolean {
325
+ let empty = true;
326
+ forEachBuildedInput(
327
+ dto,
328
+ (input) => {
329
+ if (!isInputEmpty(input)) empty = false;
330
+ },
331
+ false,
332
+ );
333
+ return empty;
334
+ }
335
+
336
+ /**
337
+ * Gate de validação client-side honesto: percorre os campos `BuildedInput` do
338
+ * schema (recursando em DTO aninhado e array de DTOs), chama `touch()` em cada um
339
+ * (acende o erro por-campo no submit) e retorna `true` só se todos forem válidos.
340
+ * Campo vazio é inválido quando `required`; caso contrário consulta o `validator`.
341
+ * Sub-DTO opcional inteiramente vazio é pulado (não bloqueia bloco em branco).
342
+ * Sem `throw`, sem `toast`, sem mutar o schema. Tocar todos é de propósito (não
343
+ * `.every`, que short-circuita e deixaria campos sem acender).
344
+ */
345
+ export function validateForm(schema: object): boolean {
346
+ let valid = true;
347
+ forEachBuildedInput(
348
+ schema,
349
+ (input) => {
350
+ input.touch();
351
+ const fieldValid = isInputEmpty(input) ? !input.required : input.validate() === null;
352
+ if (!fieldValid) valid = false;
353
+ },
354
+ true,
355
+ );
356
+ return valid;
357
+ }
358
+
359
+ /**
360
+ * Simétrico do `validateForm`: apaga `showError` de todos os campos do schema
361
+ * (incl. aninhados, sem pular opcional). Usar no `onClose`/reset de modal que
362
+ * reusa a instância do form. (Quando o endpoint faz `reset()` → `new Dto()`,
363
+ * `showError` já volta `false` de graça; o untouch só é necessário pra instância
364
+ * reutilizada.)
365
+ */
366
+ export function untouchForm(schema: object): void {
367
+ forEachBuildedInput(schema, (input) => input.untouch(), false);
368
+ }
369
+
269
370
  export function genericArrayBundler(data: string[]): string[];
270
371
  export function genericArrayBundler<T extends { bundle: () => BundleResult<T> }>(
271
372
  data: T[],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-reflector",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Reflects types from openAPI schemas",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",