Skip to content

Commit 51613bc

Browse files
committed
feat: add collected parameter tracking
1 parent 7f2c5e7 commit 51613bc

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

src/analysis/collector.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { ParameterLocation } from "../config-schema.js";
2+
import { isValueType } from "../types.js";
3+
import type {
4+
CollectedParameter,
5+
RouteRegistration,
6+
ValueType,
7+
} from "../types.js";
8+
9+
export class ParameterCollector {
10+
readonly #path = new Map<string, CollectedParameter>();
11+
readonly #query = new Map<string, CollectedParameter>();
12+
readonly #body = new Map<string, CollectedParameter>();
13+
14+
constructor(route: RouteRegistration) {
15+
for (const parameter of collectPathParams(route)) {
16+
this.#path.set(parameter.canonicalPath, parameter);
17+
}
18+
}
19+
20+
add(parameter: CollectedParameter): void {
21+
if (parameter.location === "path") {
22+
this.#path.set(
23+
parameter.canonicalPath,
24+
mergeCollectedParameter(
25+
this.#path.get(parameter.canonicalPath),
26+
parameter,
27+
),
28+
);
29+
return;
30+
}
31+
if (parameter.location === "query") {
32+
this.#query.set(
33+
parameter.canonicalPath,
34+
mergeCollectedParameter(
35+
this.#query.get(parameter.canonicalPath),
36+
parameter,
37+
),
38+
);
39+
return;
40+
}
41+
if (parameter.location === "body") {
42+
this.#body.set(
43+
parameter.canonicalPath,
44+
mergeCollectedParameter(
45+
this.#body.get(parameter.canonicalPath),
46+
parameter,
47+
),
48+
);
49+
}
50+
}
51+
52+
snapshot(location: ParameterLocation): readonly CollectedParameter[] {
53+
switch (location) {
54+
case "path":
55+
return [...this.#path.values()];
56+
case "query":
57+
return [...this.#query.values()];
58+
case "body":
59+
return [...this.#body.values()];
60+
default: {
61+
const _exhaustiveCheck: never = location;
62+
// eslint-disable-next-line
63+
throw new Error(`Unhandled case: ${_exhaustiveCheck}`);
64+
}
65+
}
66+
}
67+
}
68+
69+
function mergeCollectedParameter(
70+
existing: CollectedParameter | undefined,
71+
incoming: CollectedParameter,
72+
): CollectedParameter {
73+
if (existing === undefined) {
74+
return incoming;
75+
}
76+
77+
return {
78+
...incoming,
79+
valueType: mergeValueTypes(existing.valueType, incoming.valueType),
80+
};
81+
}
82+
83+
function mergeValueTypes(
84+
left: ValueType | undefined,
85+
right: ValueType | undefined,
86+
): ValueType | undefined {
87+
if (left === undefined) {
88+
return right;
89+
}
90+
if (right === undefined || left === right) {
91+
return left;
92+
}
93+
if (left === "array" && right.endsWith("[]")) {
94+
return right;
95+
}
96+
if (right === "array" && left.endsWith("[]")) {
97+
return left;
98+
}
99+
100+
const variants = new Set([...left.split(" | "), ...right.split(" | ")]);
101+
const merged = [...variants].sort((a, b) => a.localeCompare(b)).join(" | ");
102+
if (isValueType(merged)) {
103+
return merged;
104+
}
105+
return left;
106+
}
107+
108+
function collectPathParams(
109+
route: RouteRegistration,
110+
): readonly CollectedParameter[] {
111+
return route.pathTemplate
112+
.split("/")
113+
.filter((part) => part.startsWith(":"))
114+
.map((part) => part.slice(1))
115+
.filter((name) => name.length > 0)
116+
.map(
117+
(name): CollectedParameter => ({
118+
location: "path",
119+
canonicalPath: name,
120+
valueType: "string",
121+
source: route.source,
122+
}),
123+
);
124+
}

0 commit comments

Comments
 (0)