typespec-typescript-emitter 0.2.0 → 0.3.1

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.
@@ -0,0 +1,137 @@
1
+ import { Model, Type } from "@typespec/compiler";
2
+ import { resolveScalar } from "./emit_types_resolve.js";
3
+
4
+ export const getTypeguardModel = (
5
+ m: Model,
6
+ accessor: string,
7
+ nestingLevel = 1,
8
+ knownGuards?: Array<{ filename: string; name: string }>,
9
+ ): [string, string[]] => {
10
+ const imports: string[] = [];
11
+ return [
12
+ Array.from(m.properties)
13
+ .map((property) => {
14
+ const guard = getTypeguard(
15
+ property[1].type,
16
+ `${accessor}['${property[1].name}']`,
17
+ nestingLevel + 1,
18
+ knownGuards,
19
+ );
20
+ imports.push(...guard[1]);
21
+ let ret = " ".repeat(nestingLevel);
22
+ ret += property[1].optional
23
+ ? `${accessor}['${property[1].name}'] === undefined || `
24
+ : `${accessor}['${property[1].name}'] !== undefined && `;
25
+ ret += `(${guard[0]})`;
26
+ return ret;
27
+ })
28
+ .filter((x) => !!x)
29
+ .join(" &&\n"),
30
+ imports,
31
+ ];
32
+ };
33
+
34
+ /**
35
+ * Creates the function body for a typeguard
36
+ * @param t Type to create guards for
37
+ * @param accessor String by which the type-to-test can be accessed by the code
38
+ * @param nestingLevel
39
+ * @param knownGuards Array of names of known typeguards; if type is found in those, no new typeguard will be created and instead a reference to the existing one is produced
40
+ * @returns Tuple: [function body of the typeguard, array of import filenames (not unique!)]
41
+ */
42
+ export const getTypeguard = (
43
+ t: Type,
44
+ accessor: string,
45
+ nestingLevel = 1,
46
+ knownGuards?: Array<{ filename: string; name: string }>,
47
+ ): [string, string[]] => {
48
+ switch (t.kind) {
49
+ case "Model":
50
+ if (t.name === "Array") {
51
+ const guard = getTypeguard(
52
+ t.indexer!.value,
53
+ "v",
54
+ nestingLevel,
55
+ knownGuards,
56
+ );
57
+ if (guard[0].endsWith("\n"))
58
+ guard[0] = guard[0].substring(0, guard[0].length - 1);
59
+ return [
60
+ `Array.isArray(${accessor}) && ${accessor}.every((v) => ${guard[0]})`,
61
+ guard[1],
62
+ ];
63
+ } else if (knownGuards && knownGuards.some((x) => x.name === t.name)) {
64
+ return [
65
+ `is${t.name}(${accessor})`,
66
+ [
67
+ `import {is${t.name}} from './${knownGuards.find((x) => x.name === t.name)!.filename}';`,
68
+ ],
69
+ ];
70
+ } else if (t.name && !knownGuards && t.namespace?.models.has(t.name)) {
71
+ return [`is${t.name}(${accessor})`, []];
72
+ } else {
73
+ const guard = getTypeguardModel(t, accessor, nestingLevel, knownGuards);
74
+ return [
75
+ `(\n${guard[0]}\n${" ".repeat(Math.max(nestingLevel - 1, 0))})`,
76
+ guard[1],
77
+ ];
78
+ }
79
+ case "Boolean":
80
+ return [`typeof ${accessor} === 'boolean'`, []];
81
+ case "Intrinsic":
82
+ return [`${accessor} === ${t.name}`, []];
83
+ case "Number":
84
+ return [`typeof ${accessor} === 'number'`, []];
85
+ case "Scalar":
86
+ if (
87
+ // TODO: figure out how to support all the varieties of dates
88
+ // TODO: support byte arrays
89
+ resolveScalar(t) !== "Date" &&
90
+ resolveScalar(t) !== "Uint8Array"
91
+ )
92
+ return [`typeof ${accessor} === '${resolveScalar(t)}'`, []];
93
+ break;
94
+ case "String":
95
+ return [`typeof ${accessor} === 'string'`, []];
96
+ case "Tuple": {
97
+ // TODO: ['string1', 'string2'] gets resolved as [string, string] instead of literals. Why?
98
+ const imports: string[] = [];
99
+ return [
100
+ t.values
101
+ .map((v, i) => {
102
+ const guard = getTypeguard(
103
+ v,
104
+ `${accessor}[${i}]`,
105
+ nestingLevel,
106
+ knownGuards,
107
+ );
108
+ imports.push(...guard[1]);
109
+ return `(${guard[0]})`;
110
+ })
111
+ .join(" && "),
112
+ imports,
113
+ ];
114
+ }
115
+ case "Union": {
116
+ const imports: string[] = [];
117
+ return [
118
+ Array.from(t.variants)
119
+ .map((v) => {
120
+ const guard = getTypeguard(
121
+ v[1].type,
122
+ `${accessor}`,
123
+ nestingLevel,
124
+ knownGuards,
125
+ );
126
+ imports.push(...guard[1]);
127
+ return `(${guard[0]})`;
128
+ })
129
+ .join(" || "),
130
+ imports,
131
+ ];
132
+ }
133
+ default:
134
+ console.warn("Could not resolve type:", t.kind);
135
+ }
136
+ return ["true", []]; // fallback to not break everything in case of errors
137
+ };
package/src/emitter.ts CHANGED
@@ -9,20 +9,46 @@ import emitRoutes from "./emit_routes.js";
9
9
  import emitTypes from "./emit_types.js";
10
10
  import { EmitterOptions } from "./lib.js";
11
11
 
12
+ declare global {
13
+ interface String {
14
+ addLine(str: string, tabs?: number, continued?: boolean): string;
15
+ }
16
+ }
17
+
18
+ String.prototype.addLine = function (
19
+ this: string,
20
+ str: string,
21
+ tabs?: number,
22
+ continued?: boolean,
23
+ ): string {
24
+ return `${this}${" ".repeat(tabs ?? 0)}${str}${continued ? "" : "\n"}`;
25
+ };
26
+
12
27
  export async function $onEmit(context: EmitContext) {
13
28
  if (!context.program.compilerOptions.noEmit) {
14
29
  const options: EmitterOptions = {
15
30
  "root-namespace": context.options["root-namespace"],
16
31
  "out-dir": context.options["out-dir"] ?? context.emitterOutputDir,
17
32
  "enable-types": context.options["enable-types"] ?? true,
33
+ "enable-typeguards":
34
+ (context.options["enable-types"] ?? true) &&
35
+ (context.options["enable-typeguards"] ?? false),
18
36
  "enable-routes": context.options["enable-routes"] ?? false,
37
+ "typeguards-in-routes":
38
+ (context.options["enable-types"] ?? true) &&
39
+ (context.options["enable-typeguards"] ?? false) &&
40
+ (context.options["enable-routes"] ?? false) &&
41
+ (context.options["typeguards-in-routes"] ?? false),
19
42
  };
20
43
 
21
44
  console.log(`Writing routes to ${options["out-dir"]}`);
22
45
 
23
46
  let targetNamespaceFound = false;
24
47
  let routesObject = "";
25
- let typeFiles: ReturnType<typeof emitTypes> = {};
48
+ let typeFiles: ReturnType<typeof emitTypes> = {
49
+ files: {},
50
+ typeguardedNames: [],
51
+ };
26
52
  navigateProgram(context.program, {
27
53
  namespace(n) {
28
54
  if (
@@ -30,15 +56,18 @@ export async function $onEmit(context: EmitContext) {
30
56
  n.name === context.options["root-namespace"]
31
57
  ) {
32
58
  targetNamespaceFound = true;
59
+ if (options["enable-types"] || options["enable-typeguards"])
60
+ typeFiles = emitTypes(context, n, options);
33
61
  if (options["enable-routes"]) {
34
62
  const servers = getServers(context.program, n);
35
63
  routesObject = emitRoutes(
36
64
  context,
37
65
  n,
38
66
  servers && servers[0] ? servers[0].url : "",
67
+ options,
68
+ typeFiles.typeguardedNames,
39
69
  );
40
70
  }
41
- if (options["enable-types"]) typeFiles = emitTypes(context, n);
42
71
  }
43
72
  },
44
73
  });
@@ -59,13 +88,14 @@ export async function $onEmit(context: EmitContext) {
59
88
  }
60
89
 
61
90
  // type files
62
- if (options["enable-types"]) {
63
- const typeFileArr = Object.entries(typeFiles);
91
+ if (options["enable-types"] || options["enable-typeguards"]) {
92
+ const typeFileArr = Object.entries(typeFiles.files);
64
93
  for (let i = 0; i < typeFileArr.length; i++) {
65
- await emitFile(context.program, {
66
- path: resolvePath(options["out-dir"], `${typeFileArr[i][0]}.ts`),
67
- content: typeFileArr[i][1],
68
- });
94
+ if (typeFileArr[i][1])
95
+ await emitFile(context.program, {
96
+ path: resolvePath(options["out-dir"], `${typeFileArr[i][0]}.ts`),
97
+ content: typeFileArr[i][1],
98
+ });
69
99
  }
70
100
  }
71
101
  }
package/src/lib.ts CHANGED
@@ -4,7 +4,9 @@ export interface EmitterOptions {
4
4
  "root-namespace": string;
5
5
  "out-dir": string;
6
6
  "enable-types": boolean;
7
+ "enable-typeguards": boolean;
7
8
  "enable-routes": boolean;
9
+ "typeguards-in-routes": boolean;
8
10
  }
9
11
 
10
12
  const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
@@ -14,7 +16,9 @@ const EmitterOptionsSchema: JSONSchemaType<EmitterOptions> = {
14
16
  "root-namespace": { type: "string" },
15
17
  "out-dir": { type: "string", format: "absolute-path" },
16
18
  "enable-types": { type: "boolean" },
19
+ "enable-typeguards": { type: "boolean" },
17
20
  "enable-routes": { type: "boolean" },
21
+ "typeguards-in-routes": { type: "boolean" },
18
22
  },
19
23
  required: ["root-namespace"],
20
24
  };