Skip to content

Commit 8540e09

Browse files
authored
Merge pull request #5 from maxholman/feat/v5
feat: enforce type-safe parameters with path param constraints
2 parents 46bc687 + 4d5cf32 commit 8540e09

5 files changed

Lines changed: 61 additions & 12 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expectTypeOf } from 'vitest';
2+
import type { Parameter } from '../lib/parameter.ts';
3+
import type { ValidParameter } from '../lib/types.ts';
4+
5+
// Test that only matching path param names are allowed
6+
type PathParams = ValidParameter<'/users/{userId}'>;
7+
expectTypeOf<Parameter<'userId', 'path'>>().toExtend<PathParams>();
8+
9+
// @ts-expect-error path parameter with wrong name is not allowed
10+
expectTypeOf<Parameter<'notInRoute', 'path'>>().toExtend<PathParams>();
11+
12+
// Test that query parameters can be any string
13+
expectTypeOf<Parameter<'anyName', 'query'>>().toExtend<
14+
ValidParameter<'/users/{userId}'>
15+
>();
16+
17+
// Test that header parameters can be any string
18+
expectTypeOf<Parameter<'X-Custom-Header', 'header'>>().toExtend<
19+
ValidParameter<'/users/{userId}'>
20+
>();
21+
22+
// Test that cookie parameters can be any string
23+
expectTypeOf<Parameter<'sessionId', 'cookie'>>().toExtend<
24+
ValidParameter<'/users/{userId}'>
25+
>();
26+
27+
// Test multi-param routes
28+
type MultiParamRoute = ValidParameter<'/users/{userId}/notes/{noteId}'>;
29+
expectTypeOf<Parameter<'userId', 'path'>>().toExtend<MultiParamRoute>();
30+
expectTypeOf<Parameter<'noteId', 'path'>>().toExtend<MultiParamRoute>();
31+
32+
// @ts-expect-error path parameter not in route
33+
expectTypeOf<Parameter<'otherParam', 'path'>>().toExtend<MultiParamRoute>();
34+
35+
// Test that non-path parameters work for any route
36+
expectTypeOf<Parameter<'limit', 'query'>>().toExtend<MultiParamRoute>();
37+
expectTypeOf<Parameter<'offset', 'query'>>().toExtend<MultiParamRoute>();

lib/operation.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Construct } from 'constructs';
22
import type { oas31 } from 'openapi3-ts';
3-
import type { Parameter } from './parameter.ts';
43
import { RequestBody, type RequestBodyOptions } from './request-body.ts';
54
import type { Response } from './response.ts';
65
import type { SecurityRequirement } from './security-requirement.ts';
76
import type { Tag } from './tag.ts';
8-
import type { ExtractRouteParams } from './types.ts';
7+
import type { ValidParameter } from './types.ts';
98
import { stripUndefined } from './utils.ts';
109
import type { HttpMethod } from './http-method.ts';
1110

@@ -15,7 +14,7 @@ export interface OperationOptions<TPath extends string = '/'> {
1514
description?: string;
1615
tags?: Set<Tag>;
1716
deprecated?: boolean;
18-
parameters?: Parameter<keyof ExtractRouteParams<TPath>>[];
17+
parameters?: ValidParameter<TPath>[];
1918
security?: SecurityRequirement;
2019
responses?: {
2120
[statusCode: string | number]: Response;

lib/parameter.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import type { oas31 } from 'openapi3-ts';
33
import type { Api } from './api.ts';
44
import type { Schema } from './schema.ts';
55

6-
interface ParameterOptionsBase<TName extends string | number | symbol> {
6+
interface ParameterOptionsBase<
7+
TName extends string | number | symbol,
8+
TIn extends 'query' | 'header' | 'path' | 'cookie',
9+
> {
710
name: TName;
8-
in: 'query' | 'header' | 'path' | 'cookie';
11+
in: TIn;
912
required: boolean;
1013
description?: string;
1114
deprecated?: boolean;
@@ -14,8 +17,10 @@ interface ParameterOptionsBase<TName extends string | number | symbol> {
1417
style?: 'simple';
1518
}
1619

17-
interface ParameterOptions<TName extends string | number | symbol>
18-
extends ParameterOptionsBase<TName> {
20+
interface ParameterOptions<
21+
TName extends string | number | symbol,
22+
TIn extends 'query' | 'header' | 'path' | 'cookie',
23+
> extends ParameterOptionsBase<TName, TIn> {
1924
schema: Schema;
2025
}
2126

@@ -26,10 +31,11 @@ interface ParameterOptions<TName extends string | number | symbol>
2631

2732
export class Parameter<
2833
TName extends string | number | symbol = '',
34+
TIn extends 'query' | 'header' | 'path' | 'cookie' = 'query',
2935
> extends Construct {
30-
private options: ParameterOptions<TName>;
36+
private options: ParameterOptions<TName, TIn>;
3137

32-
constructor(scope: Api, id: string, options: ParameterOptions<TName>) {
38+
constructor(scope: Api, id: string, options: ParameterOptions<TName, TIn>) {
3339
super(scope, id);
3440
this.options = options;
3541
}

lib/path.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import type { oas31 } from 'openapi3-ts';
33
import type { Api } from './api.ts';
44
import type { HttpMethod } from './http-method.ts';
55
import { Operation, type OperationOptions } from './operation.ts';
6-
import type { Parameter } from './parameter.ts';
76
import type { Server } from './server.ts';
87
import type { Tag } from './tag.ts';
9-
import type { ExtractRouteParams } from './types.ts';
8+
import type { ValidParameter } from './types.ts';
109

1110
interface PathOptions<TPath extends string> {
1211
path: TPath;
1312
summary?: string;
1413
servers?: Server[];
15-
parameters?: Parameter<keyof ExtractRouteParams<TPath>>[];
14+
parameters?: ValidParameter<TPath>[];
1615
tags?: Set<Tag>;
1716
}
1817

lib/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Parameter } from './parameter.ts';
2+
13
/* eslint-disable @typescript-eslint/no-unused-vars */
24
export type ExtractRouteParams<T> = string extends T
35
? Record<string, string>
@@ -6,3 +8,9 @@ export type ExtractRouteParams<T> = string extends T
68
: T extends `${infer _Start}{${infer Param}}`
79
? { [k in Param]: string }
810
: Record<string, never>;
11+
12+
export type ValidParameter<TPath extends string> =
13+
| Parameter<keyof ExtractRouteParams<TPath>, 'path'>
14+
| Parameter<string, 'query'>
15+
| Parameter<string, 'header'>
16+
| Parameter<string, 'cookie'>;

0 commit comments

Comments
 (0)