svelte-reflector 2.5.0 → 2.5.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/dist/core/schema/DiscriminatedInterfaceRenderer.d.ts +35 -0
- package/dist/core/schema/DiscriminatedInterfaceRenderer.js +70 -0
- package/dist/core/schema/ReflectorInterface.d.ts +2 -0
- package/dist/core/schema/ReflectorInterface.js +4 -1
- package/dist/core/schema/Schema.d.ts +2 -0
- package/dist/core/schema/Schema.js +8 -0
- package/dist/core/schema/SchemaClassRenderer.d.ts +2 -0
- package/dist/core/schema/SchemaClassRenderer.js +53 -12
- package/dist/core/schema/SchemaDependencyCollector.d.ts +2 -0
- package/dist/core/schema/SchemaDependencyCollector.js +6 -1
- package/dist/core/schema/SchemaPropertyClassifier.d.ts +2 -1
- package/dist/core/schema/SchemaPropertyClassifier.js +14 -4
- package/dist/props/object.property.d.ts +9 -0
- package/dist/props/object.property.js +12 -1
- package/dist/props/union.property.d.ts +37 -0
- package/dist/props/union.property.js +62 -0
- package/dist/runtime/reflector.svelte.ts +101 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ArrayProp } from "../../props/array.property.js";
|
|
2
|
+
import type { EnumProp } from "../../props/enum.property.js";
|
|
3
|
+
import type { ObjectProp } from "../../props/object.property.js";
|
|
4
|
+
import type { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import type { UnionProp } from "../../props/union.property.js";
|
|
6
|
+
/**
|
|
7
|
+
* Renders a schema's `Interface` as a *discriminated union* type alias when one
|
|
8
|
+
* of its properties is a `oneOf` carrying a `discriminator` whose `propertyName`
|
|
9
|
+
* points at a sibling enum property (e.g. `NotificationData.actionMeta` keyed by
|
|
10
|
+
* `action`). Each `discriminator.mapping` entry (action value → variant schema)
|
|
11
|
+
* becomes a union member that pins `action` to its literal(s) and `actionMeta`
|
|
12
|
+
* to the matching variant `Interface`:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export type NotificationDataInterface =
|
|
16
|
+
* | { ...commons; action: "STOCK_LOW" | "STOCK_OUT"; actionMeta: ProductStockMetaInterface }
|
|
17
|
+
* | { ...commons; action: "ANNOUNCEMENT"; actionMeta: UrlMetaInterface };
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* This is what makes `if (n.action === "STOCK_LOW") n.actionMeta.productId`
|
|
21
|
+
* narrow on plain `Interface`-typed values (raw response data, `.bundle()`,
|
|
22
|
+
* `.discriminated()`). It never narrows on the live `$state` class instance —
|
|
23
|
+
* TS can't correlate two independent mutable fields.
|
|
24
|
+
*/
|
|
25
|
+
export declare class DiscriminatedInterfaceRenderer {
|
|
26
|
+
static render(params: {
|
|
27
|
+
name: string;
|
|
28
|
+
union: UnionProp;
|
|
29
|
+
primitiveProps: PrimitiveProp[];
|
|
30
|
+
arrayProps: ArrayProp[];
|
|
31
|
+
objectProps: ObjectProp[];
|
|
32
|
+
enumProps: EnumProp[];
|
|
33
|
+
otherUnionProps: UnionProp[];
|
|
34
|
+
}): string;
|
|
35
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a schema's `Interface` as a *discriminated union* type alias when one
|
|
3
|
+
* of its properties is a `oneOf` carrying a `discriminator` whose `propertyName`
|
|
4
|
+
* points at a sibling enum property (e.g. `NotificationData.actionMeta` keyed by
|
|
5
|
+
* `action`). Each `discriminator.mapping` entry (action value → variant schema)
|
|
6
|
+
* becomes a union member that pins `action` to its literal(s) and `actionMeta`
|
|
7
|
+
* to the matching variant `Interface`:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* export type NotificationDataInterface =
|
|
11
|
+
* | { ...commons; action: "STOCK_LOW" | "STOCK_OUT"; actionMeta: ProductStockMetaInterface }
|
|
12
|
+
* | { ...commons; action: "ANNOUNCEMENT"; actionMeta: UrlMetaInterface };
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* This is what makes `if (n.action === "STOCK_LOW") n.actionMeta.productId`
|
|
16
|
+
* narrow on plain `Interface`-typed values (raw response data, `.bundle()`,
|
|
17
|
+
* `.discriminated()`). It never narrows on the live `$state` class instance —
|
|
18
|
+
* TS can't correlate two independent mutable fields.
|
|
19
|
+
*/
|
|
20
|
+
export class DiscriminatedInterfaceRenderer {
|
|
21
|
+
static render(params) {
|
|
22
|
+
const { name, union, primitiveProps, arrayProps, objectProps, enumProps, otherUnionProps } = params;
|
|
23
|
+
const discProp = union.discriminator.propertyName;
|
|
24
|
+
const mapping = union.discriminator.mapping ?? {};
|
|
25
|
+
// Commons = every property except the discriminant and the discriminated union itself.
|
|
26
|
+
const allProps = [
|
|
27
|
+
...primitiveProps,
|
|
28
|
+
...arrayProps,
|
|
29
|
+
...objectProps,
|
|
30
|
+
...enumProps,
|
|
31
|
+
...otherUnionProps,
|
|
32
|
+
];
|
|
33
|
+
const commons = allProps
|
|
34
|
+
.filter((p) => p.name !== discProp && p.name !== union.name)
|
|
35
|
+
.map((p) => p.interfaceBuild());
|
|
36
|
+
// Invert the mapping: variant schema name → the action literals that select it,
|
|
37
|
+
// preserving first-seen order so output stays deterministic.
|
|
38
|
+
const byVariant = new Map();
|
|
39
|
+
for (const [actionValue, ref] of Object.entries(mapping)) {
|
|
40
|
+
const variant = ref.split("/").at(-1) ?? "";
|
|
41
|
+
if (!variant)
|
|
42
|
+
continue;
|
|
43
|
+
const list = byVariant.get(variant) ?? [];
|
|
44
|
+
list.push(actionValue);
|
|
45
|
+
byVariant.set(variant, list);
|
|
46
|
+
}
|
|
47
|
+
// The discriminated part: only the discriminant + the union field vary per variant.
|
|
48
|
+
// Commons are factored into a single object type and intersected with the union —
|
|
49
|
+
// narrowing on `action` still works through the intersection.
|
|
50
|
+
const members = [...byVariant.entries()].map(([variant, actions]) => {
|
|
51
|
+
const literals = actions.map((a) => `"${a}"`).join(" | ");
|
|
52
|
+
return `{ ${discProp}: ${literals}; ${union.name}: ${variant}Interface }`;
|
|
53
|
+
});
|
|
54
|
+
const variantUnion = members.map((m) => `| ${m}`).join("\n");
|
|
55
|
+
// No commons → emit the bare union (avoid a pointless `{} & (...)`).
|
|
56
|
+
if (commons.length === 0) {
|
|
57
|
+
return `
|
|
58
|
+
export type ${name}Interface =
|
|
59
|
+
${variantUnion};
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
return `
|
|
63
|
+
export type ${name}Interface = {
|
|
64
|
+
${commons.join(";\n")}
|
|
65
|
+
} & (
|
|
66
|
+
${variantUnion}
|
|
67
|
+
);
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import type { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import type { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import type { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import type { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
export declare class ReflectorInterface {
|
|
6
7
|
builded: string;
|
|
7
8
|
constructor(params: {
|
|
@@ -10,5 +11,6 @@ export declare class ReflectorInterface {
|
|
|
10
11
|
name: string;
|
|
11
12
|
objectProps: ObjectProp[];
|
|
12
13
|
enumProps: EnumProp[];
|
|
14
|
+
unionProps: UnionProp[];
|
|
13
15
|
});
|
|
14
16
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export class ReflectorInterface {
|
|
2
2
|
builded;
|
|
3
3
|
constructor(params) {
|
|
4
|
-
const { name, arrayProps, primitiveProps, objectProps, enumProps } = params;
|
|
4
|
+
const { name, arrayProps, primitiveProps, objectProps, enumProps, unionProps } = params;
|
|
5
5
|
const buildedProps = [];
|
|
6
6
|
primitiveProps.forEach((prop) => {
|
|
7
7
|
buildedProps.push(prop.interfaceBuild());
|
|
@@ -15,6 +15,9 @@ export class ReflectorInterface {
|
|
|
15
15
|
enumProps.forEach((prop) => {
|
|
16
16
|
buildedProps.push(prop.interfaceBuild());
|
|
17
17
|
});
|
|
18
|
+
unionProps.forEach((prop) => {
|
|
19
|
+
buildedProps.push(prop.interfaceBuild());
|
|
20
|
+
});
|
|
18
21
|
this.builded = `
|
|
19
22
|
export interface ${name}Interface {
|
|
20
23
|
${buildedProps}
|
|
@@ -2,6 +2,7 @@ import { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
import type { SchemaObject, ReferenceObject } from "../../types/open-api-spec.interface.js";
|
|
6
7
|
import type { FieldConfigs } from "../../types/types.js";
|
|
7
8
|
import type { CodegenContext } from "../CodegenContext.js";
|
|
@@ -11,6 +12,7 @@ export declare class Schema {
|
|
|
11
12
|
arrayProps: ArrayProp[];
|
|
12
13
|
objectProps: ObjectProp[];
|
|
13
14
|
enumProps: EnumProp[];
|
|
15
|
+
unionProps: UnionProp[];
|
|
14
16
|
/** Other schema class names this schema depends on (via ObjectProp/$ref arrays) */
|
|
15
17
|
readonly schemaDeps: string[];
|
|
16
18
|
/** Enum type names used by this schema */
|
|
@@ -2,6 +2,7 @@ import { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
import { isReferenceObject } from "../../helpers/helpers.js";
|
|
6
7
|
import { SchemaPropertyClassifier } from "./SchemaPropertyClassifier.js";
|
|
7
8
|
import { SchemaDependencyCollector } from "./SchemaDependencyCollector.js";
|
|
@@ -13,6 +14,7 @@ export class Schema {
|
|
|
13
14
|
arrayProps = [];
|
|
14
15
|
objectProps = [];
|
|
15
16
|
enumProps = [];
|
|
17
|
+
unionProps = [];
|
|
16
18
|
/** Other schema class names this schema depends on (via ObjectProp/$ref arrays) */
|
|
17
19
|
schemaDeps;
|
|
18
20
|
/** Enum type names used by this schema */
|
|
@@ -45,6 +47,7 @@ export class Schema {
|
|
|
45
47
|
schema.arrayProps = [];
|
|
46
48
|
schema.objectProps = [];
|
|
47
49
|
schema.enumProps = [];
|
|
50
|
+
schema.unionProps = [];
|
|
48
51
|
schema.schemaDeps = element.kind === "ref" ? [element.type] : [];
|
|
49
52
|
schema.enumDeps = element.kind === "enum" ? [element.type] : [];
|
|
50
53
|
schema.customTypeDeps = [];
|
|
@@ -110,12 +113,15 @@ export class Schema {
|
|
|
110
113
|
this.objectProps.push(prop);
|
|
111
114
|
else if (prop instanceof EnumProp)
|
|
112
115
|
this.enumProps.push(prop);
|
|
116
|
+
else if (prop instanceof UnionProp)
|
|
117
|
+
this.unionProps.push(prop);
|
|
113
118
|
}
|
|
114
119
|
const deps = SchemaDependencyCollector.collect({
|
|
115
120
|
primitiveProps: this.primitiveProps,
|
|
116
121
|
arrayProps: this.arrayProps,
|
|
117
122
|
objectProps: this.objectProps,
|
|
118
123
|
enumProps: this.enumProps,
|
|
124
|
+
unionProps: this.unionProps,
|
|
119
125
|
});
|
|
120
126
|
this.schemaDeps = deps.schemaDeps;
|
|
121
127
|
this.enumDeps = deps.enumDeps;
|
|
@@ -127,6 +133,7 @@ export class Schema {
|
|
|
127
133
|
arrayProps: this.arrayProps,
|
|
128
134
|
objectProps: this.objectProps,
|
|
129
135
|
enumProps: this.enumProps,
|
|
136
|
+
unionProps: this.unionProps,
|
|
130
137
|
});
|
|
131
138
|
this.interface = rendered.interface;
|
|
132
139
|
this.schema = rendered.schema;
|
|
@@ -148,6 +155,7 @@ export class Schema {
|
|
|
148
155
|
arrayProps: this.arrayProps,
|
|
149
156
|
objectProps: this.objectProps,
|
|
150
157
|
enumProps: this.enumProps,
|
|
158
|
+
unionProps: this.unionProps,
|
|
151
159
|
mode: "request",
|
|
152
160
|
});
|
|
153
161
|
this.schema = rendered.schema;
|
|
@@ -2,6 +2,7 @@ import type { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import type { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import type { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import type { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import type { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
export declare class SchemaClassRenderer {
|
|
6
7
|
static render(params: {
|
|
7
8
|
name: string;
|
|
@@ -9,6 +10,7 @@ export declare class SchemaClassRenderer {
|
|
|
9
10
|
arrayProps: ArrayProp[];
|
|
10
11
|
objectProps: ObjectProp[];
|
|
11
12
|
enumProps: EnumProp[];
|
|
13
|
+
unionProps: UnionProp[];
|
|
12
14
|
mode?: "request" | "response";
|
|
13
15
|
}): {
|
|
14
16
|
interface: string;
|
|
@@ -1,19 +1,36 @@
|
|
|
1
|
+
import { DiscriminatedInterfaceRenderer } from "./DiscriminatedInterfaceRenderer.js";
|
|
1
2
|
import { ReflectorInterface } from "./ReflectorInterface.js";
|
|
2
3
|
export class SchemaClassRenderer {
|
|
3
4
|
static render(params) {
|
|
4
|
-
const { name, primitiveProps, arrayProps, objectProps, enumProps } = params;
|
|
5
|
+
const { name, primitiveProps, arrayProps, objectProps, enumProps, unionProps } = params;
|
|
5
6
|
const mode = params.mode ?? "response";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// A `oneOf` with a usable discriminator turns the Interface into a discriminated
|
|
8
|
+
// union type alias; the matching `discriminated()` accessor is emitted below.
|
|
9
|
+
const discriminatedUnion = unionProps.find((u) => u.hasDiscriminator);
|
|
10
|
+
const interfaceStr = discriminatedUnion
|
|
11
|
+
? DiscriminatedInterfaceRenderer.render({
|
|
12
|
+
name,
|
|
13
|
+
union: discriminatedUnion,
|
|
14
|
+
primitiveProps,
|
|
15
|
+
arrayProps,
|
|
16
|
+
objectProps,
|
|
17
|
+
enumProps,
|
|
18
|
+
otherUnionProps: unionProps.filter((u) => u !== discriminatedUnion),
|
|
19
|
+
})
|
|
20
|
+
: new ReflectorInterface({ name, arrayProps, primitiveProps, objectProps, enumProps, unionProps }).builded;
|
|
13
21
|
const constructorThis = [];
|
|
14
22
|
const keys = [];
|
|
15
23
|
const bundleParams = [];
|
|
16
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
|
+
: "";
|
|
17
34
|
primitiveProps.forEach((prop) => {
|
|
18
35
|
constructorThis.push(prop.constructorBuild());
|
|
19
36
|
bundleParams.push(prop.bundleBuild());
|
|
@@ -35,6 +52,11 @@ export class SchemaClassRenderer {
|
|
|
35
52
|
keys.push(prop.classBuild());
|
|
36
53
|
bundleParams.push(prop.bundleBuild());
|
|
37
54
|
});
|
|
55
|
+
unionProps.forEach((prop) => {
|
|
56
|
+
constructorThis.push(prop.constructorBuild());
|
|
57
|
+
keys.push(prop.classBuild());
|
|
58
|
+
bundleParams.push(prop.bundleBuild());
|
|
59
|
+
});
|
|
38
60
|
const constructorCode = `constructor(params?: { data?: ${name}Interface | undefined, empty?: boolean }) {
|
|
39
61
|
${constructorThis.join(";\n")}
|
|
40
62
|
}`;
|
|
@@ -43,6 +65,7 @@ export class SchemaClassRenderer {
|
|
|
43
65
|
arrayProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
44
66
|
objectProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
45
67
|
enumProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
68
|
+
unionProps.forEach((p) => hydrateLines.push(p.hydrateBuild()));
|
|
46
69
|
const hydrateCode = `
|
|
47
70
|
hydrate(data: Partial<${name}Interface>): void {
|
|
48
71
|
${hydrateLines.join(";\n")}
|
|
@@ -52,18 +75,34 @@ export class SchemaClassRenderer {
|
|
|
52
75
|
this.hydrate(new ${name}({ empty: true }).bundle() as Partial<${name}Interface>);
|
|
53
76
|
}
|
|
54
77
|
`;
|
|
78
|
+
// Live class instances can't narrow `actionMeta` by `action` (independent
|
|
79
|
+
// `$state` fields), so expose a plain discriminated snapshot for consumers
|
|
80
|
+
// that want the narrowing. `bundle()` already returns the discriminated type
|
|
81
|
+
// (see below), so this is just an ergonomic, cast-free alias.
|
|
82
|
+
const discriminatedAccessor = discriminatedUnion
|
|
83
|
+
? `discriminated(): ${name}Interface { return this.bundle(); }`
|
|
84
|
+
: "";
|
|
55
85
|
// Request DTO serializa a partir das instâncias `BuildedInput` (carregam os flags
|
|
56
86
|
// required/nullable), não do `.value` pré-extraído — bundleInputs faz a coerção
|
|
57
87
|
// nullable ''→null e cobre array/aninhado. Response fica em bundleStrict (inalterado).
|
|
88
|
+
//
|
|
89
|
+
// Para um schema com união discriminada, `bundleStrict` infere a forma plana (action
|
|
90
|
+
// = enum inteiro), que NÃO é atribuível a nenhum membro da união — o array-root wrapper
|
|
91
|
+
// (`item.bundle()` tipado como `Interface[]` discriminada) quebraria. O único `as` do
|
|
92
|
+
// feature mora aqui, fazendo `bundle()` devolver a própria união; `discriminated()` e
|
|
93
|
+
// `reset()` ficam cast-free por consequência.
|
|
58
94
|
const bundleHelper = mode === "request" ? "inputs" : "strict";
|
|
95
|
+
const responseBundle = discriminatedUnion
|
|
96
|
+
? `return bundleStrict({ ${bundleParams.join(",")} }) as ${name}Interface`
|
|
97
|
+
: `return bundleStrict({ ${bundleParams.join(",")} })`;
|
|
59
98
|
const bundleBody = mode === "request"
|
|
60
|
-
? `return bundleInputs({ ${[...primitiveProps, ...arrayProps, ...objectProps, ...enumProps]
|
|
99
|
+
? `return bundleInputs({ ${[...primitiveProps, ...arrayProps, ...objectProps, ...enumProps, ...unionProps]
|
|
61
100
|
.map((p) => `${p.name}: this.${p.name}`)
|
|
62
101
|
.join(",")} })`
|
|
63
|
-
:
|
|
102
|
+
: responseBundle;
|
|
64
103
|
const schema = `
|
|
65
104
|
export class ${name} {
|
|
66
|
-
${keys.join("
|
|
105
|
+
${[...keys, optionalDtosDecl].filter(Boolean).join(";\n ")}
|
|
67
106
|
|
|
68
107
|
${constructorCode}
|
|
69
108
|
|
|
@@ -74,7 +113,9 @@ export class SchemaClassRenderer {
|
|
|
74
113
|
bundle(){
|
|
75
114
|
${bundleBody}
|
|
76
115
|
}
|
|
116
|
+
|
|
117
|
+
${discriminatedAccessor}
|
|
77
118
|
};`;
|
|
78
|
-
return { interface:
|
|
119
|
+
return { interface: interfaceStr, schema, bundleHelper };
|
|
79
120
|
}
|
|
80
121
|
}
|
|
@@ -2,6 +2,7 @@ import type { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import type { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import type { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import type { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import type { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
export interface SchemaDependencies {
|
|
6
7
|
schemaDeps: string[];
|
|
7
8
|
enumDeps: string[];
|
|
@@ -14,5 +15,6 @@ export declare class SchemaDependencyCollector {
|
|
|
14
15
|
arrayProps: ArrayProp[];
|
|
15
16
|
objectProps: ObjectProp[];
|
|
16
17
|
enumProps: EnumProp[];
|
|
18
|
+
unionProps: UnionProp[];
|
|
17
19
|
}): SchemaDependencies;
|
|
18
20
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export class SchemaDependencyCollector {
|
|
2
2
|
static collect(props) {
|
|
3
|
-
const { primitiveProps, arrayProps, objectProps, enumProps } = props;
|
|
3
|
+
const { primitiveProps, arrayProps, objectProps, enumProps, unionProps } = props;
|
|
4
4
|
const schemaDepsSet = new Set();
|
|
5
5
|
for (const prop of objectProps) {
|
|
6
6
|
schemaDepsSet.add(prop.type);
|
|
@@ -10,6 +10,11 @@ export class SchemaDependencyCollector {
|
|
|
10
10
|
schemaDepsSet.add(prop.type);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
for (const prop of unionProps) {
|
|
14
|
+
for (const variant of prop.variantTypes) {
|
|
15
|
+
schemaDepsSet.add(variant);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
13
18
|
const enumDepsSet = new Set();
|
|
14
19
|
for (const prop of enumProps) {
|
|
15
20
|
if (prop.enumName)
|
|
@@ -2,10 +2,11 @@ import { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
import type { SchemaObject, ReferenceObject } from "../../types/open-api-spec.interface.js";
|
|
6
7
|
import type { FieldConfigs } from "../../types/types.js";
|
|
7
8
|
import type { CodegenContext } from "../CodegenContext.js";
|
|
8
|
-
export type ClassifiedProp = PrimitiveProp | ArrayProp | ObjectProp | EnumProp;
|
|
9
|
+
export type ClassifiedProp = PrimitiveProp | ArrayProp | ObjectProp | EnumProp | UnionProp;
|
|
9
10
|
export declare class SchemaPropertyClassifier {
|
|
10
11
|
static classify(params: {
|
|
11
12
|
key: string;
|
|
@@ -2,11 +2,21 @@ import { ArrayProp } from "../../props/array.property.js";
|
|
|
2
2
|
import { EnumProp } from "../../props/enum.property.js";
|
|
3
3
|
import { ObjectProp } from "../../props/object.property.js";
|
|
4
4
|
import { PrimitiveProp } from "../../props/primitive.property.js";
|
|
5
|
+
import { UnionProp } from "../../props/union.property.js";
|
|
5
6
|
import { isReferenceObject, isPrimitiveEnumValues } from "../../helpers/helpers.js";
|
|
6
7
|
export class SchemaPropertyClassifier {
|
|
7
8
|
static classify(params) {
|
|
8
9
|
const { key, value, requireds, fieldConfigs, schemaName, context } = params;
|
|
9
10
|
if (isReferenceObject(value) || !value?.type) {
|
|
11
|
+
if (!isReferenceObject(value) && value.oneOf) {
|
|
12
|
+
return new UnionProp({
|
|
13
|
+
name: key,
|
|
14
|
+
oneOf: value.oneOf,
|
|
15
|
+
discriminator: value.discriminator,
|
|
16
|
+
isRequired: requireds.includes(key),
|
|
17
|
+
isNullable: value.nullable,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
10
20
|
if (!isReferenceObject(value) && value.additionalProperties) {
|
|
11
21
|
const fakeStringSchema = { ...value, type: "string" };
|
|
12
22
|
const config = fieldConfigs.get(key);
|
|
@@ -21,7 +31,7 @@ export class SchemaPropertyClassifier {
|
|
|
21
31
|
isNullable: value.nullable,
|
|
22
32
|
});
|
|
23
33
|
}
|
|
24
|
-
return SchemaPropertyClassifier.classifyObject({ key, value });
|
|
34
|
+
return SchemaPropertyClassifier.classifyObject({ key, value, requireds });
|
|
25
35
|
}
|
|
26
36
|
const required = requireds.includes(key);
|
|
27
37
|
const items = value.items;
|
|
@@ -97,10 +107,10 @@ export class SchemaPropertyClassifier {
|
|
|
97
107
|
});
|
|
98
108
|
}
|
|
99
109
|
static classifyObject(params) {
|
|
100
|
-
const { value, key } = params;
|
|
110
|
+
const { value, key, requireds } = params;
|
|
101
111
|
if ("allOf" in value) {
|
|
102
112
|
const ref = value.allOf?.[0];
|
|
103
|
-
const isRequired =
|
|
113
|
+
const isRequired = requireds.includes(key);
|
|
104
114
|
const nullable = !!value.nullable;
|
|
105
115
|
if (ref && isReferenceObject(ref)) {
|
|
106
116
|
return new ObjectProp({ name: key, referenceObject: ref, isRequired, isNullable: nullable });
|
|
@@ -108,7 +118,7 @@ export class SchemaPropertyClassifier {
|
|
|
108
118
|
return null;
|
|
109
119
|
}
|
|
110
120
|
if (isReferenceObject(value)) {
|
|
111
|
-
return new ObjectProp({ name: key, referenceObject: value });
|
|
121
|
+
return new ObjectProp({ name: key, referenceObject: value, isRequired: requireds.includes(key) });
|
|
112
122
|
}
|
|
113
123
|
return null;
|
|
114
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 ??
|
|
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) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DiscriminatorObject, ReferenceObject, SchemaObject } from "../types/open-api-spec.interface.js";
|
|
2
|
+
/**
|
|
3
|
+
* Property whose OpenAPI schema is a `oneOf` — a union of `$ref`ed component
|
|
4
|
+
* schemas, optionally carrying a `discriminator`. Only `$ref` variants are
|
|
5
|
+
* supported (inline `oneOf` members aren't promoted today).
|
|
6
|
+
*
|
|
7
|
+
* Runtime model is intentionally flat/passthrough: the class field stores the
|
|
8
|
+
* raw incoming object typed as the union of the variants' `Interface`s — there
|
|
9
|
+
* is no `new` (you can't instantiate a union) and no `.bundle()` on it
|
|
10
|
+
* (`bundleStrict`/`bundleInputs` pass plain objects through unchanged). The
|
|
11
|
+
* elegant discriminated narrowing lives in the schema's `Interface` type alias
|
|
12
|
+
* (see `DiscriminatedInterfaceRenderer`), not on the live class instance.
|
|
13
|
+
*/
|
|
14
|
+
export declare class UnionProp {
|
|
15
|
+
name: string;
|
|
16
|
+
/** Variant component names extracted from each `oneOf` `$ref` (deduped, order-preserving). */
|
|
17
|
+
readonly variantTypes: string[];
|
|
18
|
+
readonly discriminator: DiscriminatorObject | undefined;
|
|
19
|
+
private readonly required;
|
|
20
|
+
private readonly isNullable;
|
|
21
|
+
constructor(params: {
|
|
22
|
+
name: string;
|
|
23
|
+
oneOf: (SchemaObject | ReferenceObject)[];
|
|
24
|
+
discriminator?: DiscriminatorObject | undefined;
|
|
25
|
+
isRequired?: boolean;
|
|
26
|
+
isNullable?: boolean | undefined;
|
|
27
|
+
});
|
|
28
|
+
/** `AInterface | BInterface | ...` — the flat union used for the class field type. */
|
|
29
|
+
flatUnion(): string;
|
|
30
|
+
/** True when this union can render as a discriminated `Interface` (needs a mapping). */
|
|
31
|
+
get hasDiscriminator(): boolean;
|
|
32
|
+
classBuild(): string;
|
|
33
|
+
constructorBuild(): string;
|
|
34
|
+
bundleBuild(): string;
|
|
35
|
+
interfaceBuild(): string;
|
|
36
|
+
hydrateBuild(): string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isReferenceObject } from "../helpers/helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Property whose OpenAPI schema is a `oneOf` — a union of `$ref`ed component
|
|
4
|
+
* schemas, optionally carrying a `discriminator`. Only `$ref` variants are
|
|
5
|
+
* supported (inline `oneOf` members aren't promoted today).
|
|
6
|
+
*
|
|
7
|
+
* Runtime model is intentionally flat/passthrough: the class field stores the
|
|
8
|
+
* raw incoming object typed as the union of the variants' `Interface`s — there
|
|
9
|
+
* is no `new` (you can't instantiate a union) and no `.bundle()` on it
|
|
10
|
+
* (`bundleStrict`/`bundleInputs` pass plain objects through unchanged). The
|
|
11
|
+
* elegant discriminated narrowing lives in the schema's `Interface` type alias
|
|
12
|
+
* (see `DiscriminatedInterfaceRenderer`), not on the live class instance.
|
|
13
|
+
*/
|
|
14
|
+
export class UnionProp {
|
|
15
|
+
name;
|
|
16
|
+
/** Variant component names extracted from each `oneOf` `$ref` (deduped, order-preserving). */
|
|
17
|
+
variantTypes;
|
|
18
|
+
discriminator;
|
|
19
|
+
required;
|
|
20
|
+
isNullable;
|
|
21
|
+
constructor(params) {
|
|
22
|
+
const { name, oneOf, discriminator, isRequired, isNullable } = params;
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.discriminator = discriminator;
|
|
25
|
+
this.required = isRequired ?? true;
|
|
26
|
+
this.isNullable = !!isNullable;
|
|
27
|
+
const refs = oneOf
|
|
28
|
+
.filter(isReferenceObject)
|
|
29
|
+
.map((ref) => ref.$ref.split("/").at(-1) ?? "")
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
this.variantTypes = [...new Set(refs)];
|
|
32
|
+
}
|
|
33
|
+
/** `AInterface | BInterface | ...` — the flat union used for the class field type. */
|
|
34
|
+
flatUnion() {
|
|
35
|
+
return this.variantTypes.map((t) => `${t}Interface`).join(" | ");
|
|
36
|
+
}
|
|
37
|
+
/** True when this union can render as a discriminated `Interface` (needs a mapping). */
|
|
38
|
+
get hasDiscriminator() {
|
|
39
|
+
return !!this.discriminator?.mapping && Object.keys(this.discriminator.mapping).length > 0;
|
|
40
|
+
}
|
|
41
|
+
classBuild() {
|
|
42
|
+
const req = this.required ? "" : "?";
|
|
43
|
+
const nullable = this.isNullable ? " | null" : "";
|
|
44
|
+
// No `new` for a union — `$state<T>()` (no arg) yields `T | undefined`,
|
|
45
|
+
// populated by the constructor from the incoming data.
|
|
46
|
+
return `${this.name}${req} = $state<${this.flatUnion()}${nullable}>()`;
|
|
47
|
+
}
|
|
48
|
+
constructorBuild() {
|
|
49
|
+
return `this.${this.name} = params?.data?.${this.name}`;
|
|
50
|
+
}
|
|
51
|
+
bundleBuild() {
|
|
52
|
+
return `${this.name}: this.${this.name}`;
|
|
53
|
+
}
|
|
54
|
+
interfaceBuild() {
|
|
55
|
+
const req = this.required ? "" : "?";
|
|
56
|
+
const nullable = this.isNullable ? " | null" : "";
|
|
57
|
+
return `${this.name}${req}: ${this.flatUnion()}${nullable}`;
|
|
58
|
+
}
|
|
59
|
+
hydrateBuild() {
|
|
60
|
+
return `if (data.${this.name} !== undefined) this.${this.name} = data.${this.name}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -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: Record<string, unknown>,
|
|
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: Record<string, unknown>): 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: Record<string, unknown>): 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: Record<string, unknown>): 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[],
|