Skip to content

Commit 6d4efbb

Browse files
committed
🧹 chore: test case
1 parent 9d4138f commit 6d4efbb

File tree

10 files changed

+2088
-242
lines changed

10 files changed

+2088
-242
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# 1.4.10 - 22 Sep 2025
2+
Improvement:
3+
- populate params from path when no schema is provided
4+
- accept operationId from detail
5+
- type gen: accept number as path segment
6+
- add test case for type gen, and OpenAPI schema
7+
28
Bug fix:
39
- [#226](https://github.com/elysiajs/elysia-openapi/issues/266) accept operationId
410

example/c.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { declarationToJSONSchema } from '../src/gen'
2+
3+
console.log(
4+
declarationToJSONSchema(`{
5+
"hello-world": {
6+
2: {
7+
get: {
8+
params: { }
9+
query: { }
10+
headers: { }
11+
body: { }
12+
response: {
13+
200: {
14+
name: string
15+
}
16+
}
17+
}
18+
}
19+
}
20+
}`)
21+
)

example/gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const app = new Elysia()
4848
}
4949
}
5050
)
51+
.get('/hello/2', () => 'hello')
5152
.post(
5253
'/json',
5354
({ body, status }) => (Math.random() > 0.5 ? status(418) : body),

example/index.ts

Lines changed: 11 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -18,158 +18,17 @@ const user = t.Object({
1818
})
1919
})
2020

21-
export const app = new Elysia()
22-
.use(
23-
openapi({
24-
provider: 'scalar',
25-
mapJsonSchema: {
26-
zod: z.toJSONSchema,
27-
effect: JSONSchema.make
28-
},
29-
documentation: {
30-
info: {
31-
title: 'Elysia Scalar',
32-
version: '1.3.1a'
33-
},
34-
tags: [
35-
{
36-
name: 'Test',
37-
description: 'Hello'
38-
}
39-
],
40-
components: {
41-
securitySchemes: {
42-
bearer: {
43-
type: 'http',
44-
scheme: 'bearer'
45-
},
46-
cookie: {
47-
type: 'apiKey',
48-
in: 'cookie',
49-
name: 'session_id'
50-
}
51-
}
52-
}
53-
}
54-
})
55-
)
56-
.model({ schema, schema2, user })
57-
.model({
58-
idParam: t.Object({
59-
id: t.Union([
60-
t.String({ format: 'uuid' }),
61-
t.Number({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER })
62-
]),
63-
id2: t.String()
64-
}),
65-
response200: t.Object({
66-
message: t.String(),
67-
content: t.Array(t.Object({
68-
id: t.Union([t.String(), t.Number()])
69-
}))
70-
})
21+
const model = new Elysia().model(
22+
'body',
23+
t.Object({
24+
name: t.Literal('Lilith')
7125
})
72-
.get('/test/:id/:id2', ({ params: { id } }) => ({
73-
message: 'ok',
74-
content: [{ id }]
75-
}), {
76-
params: 'idParam',
77-
response: 'response200'
26+
)
27+
28+
const app = new Elysia()
29+
.use(openapi())
30+
.use(model)
31+
.post('/user', () => 'hello', {
32+
body: 'body'
7833
})
79-
.get(
80-
'/',
81-
{ test: 'hello' as const },
82-
{
83-
response: {
84-
200: t.Object({
85-
test: t.Literal('hello')
86-
}),
87-
204: withHeaders(
88-
t.Void({
89-
title: 'Thing',
90-
description: 'Void response'
91-
}),
92-
{
93-
'X-Custom-Header': t.Literal('Elysia')
94-
}
95-
)
96-
}
97-
}
98-
)
99-
.post(
100-
'/json',
101-
({ body }) => ({
102-
test: 'hello'
103-
}),
104-
{
105-
parse: ['json', 'formdata'],
106-
body: 'schema',
107-
response: {
108-
200: t.Object({
109-
test: t.Literal('hello')
110-
}),
111-
400: z.object({
112-
a: z.string(),
113-
b: z.literal('a')
114-
}),
115-
401: Schema.standardSchemaV1(
116-
Schema.Struct({
117-
a: Schema.Literal('hi')
118-
})
119-
)
120-
}
121-
}
122-
)
123-
.post(
124-
'/json/:id',
125-
({ body, params: { id }, query: { name, email, birthday } }) => ({
126-
...body,
127-
id,
128-
name,
129-
email,
130-
birthday
131-
}),
132-
{
133-
params: t.Object({
134-
id: t.String()
135-
}),
136-
query: t.Object({
137-
name: t.String(),
138-
email: t.String({
139-
description: 'sample email description',
140-
format: 'email'
141-
}),
142-
birthday: t.String({
143-
description: 'sample birthday description',
144-
pattern: '\\d{4}-\\d{2}-\\d{2}',
145-
minLength: 10,
146-
maxLength: 10
147-
})
148-
}),
149-
body: t.Object({
150-
username: t.String(),
151-
password: t.String()
152-
}),
153-
response: t.Object(
154-
{
155-
username: t.String(),
156-
password: t.String(),
157-
id: t.String(),
158-
name: t.String(),
159-
email: t.String({
160-
description: 'sample email description',
161-
format: 'email'
162-
}),
163-
birthday: t.String({
164-
description: 'sample birthday description',
165-
pattern: '\\d{4}-\\d{2}-\\d{2}',
166-
minLength: 10,
167-
maxLength: 10
168-
})
169-
},
170-
{ description: 'sample description 3' }
171-
)
172-
}
173-
)
174-
.get('/id/:id/name/:name', () => {})
17534
.listen(3000)

src/gen/index.ts

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@ import { TypeBox } from '@sinclair/typemap'
1212
import { tmpdir } from 'os'
1313
import { join } from 'path'
1414
import { spawnSync } from 'child_process'
15-
import { AdditionalReference, AdditionalReferences } from '../types'
16-
import { Kind, TObject } from '@sinclair/typebox/type'
17-
import { readdir } from 'fs/promises'
15+
import { AdditionalReference } from '../types'
1816

1917
const matchRoute = /: Elysia<(.*)>/gs
20-
const matchStatus = /(\d{3}):/g
21-
const wrapStatusInQuote = (value: string) => value.replace(matchStatus, '"$1":')
18+
const numberKey = /(\d+):/g
2219

2320
interface OpenAPIGeneratorOptions {
2421
/**
@@ -90,7 +87,6 @@ function extractRootObjects(code: string) {
9087
const colonIdx = code.indexOf(':', i)
9188
if (colonIdx === -1) break
9289

93-
// --- find key ---
9490
// walk backwards from colon to find start of key
9591
let keyEnd = colonIdx - 1
9692
while (keyEnd >= 0 && /\s/.test(code[keyEnd])) keyEnd--
@@ -127,6 +123,52 @@ function extractRootObjects(code: string) {
127123
return results
128124
}
129125

126+
export function declarationToJSONSchema(declaration: string) {
127+
const routes: AdditionalReference = {}
128+
129+
// Treaty is a collection of { ... } & { ... } & { ... }
130+
for (const route of extractRootObjects(
131+
declaration.replace(numberKey, '"$1":')
132+
)) {
133+
let schema = TypeBox(route.replaceAll(/readonly/g, ''))
134+
if (schema.type !== 'object') continue
135+
136+
const paths = []
137+
138+
while (true) {
139+
const keys = Object.keys(schema.properties)
140+
if (keys.length !== 1) break
141+
142+
paths.push(keys[0])
143+
144+
schema = schema.properties[keys[0]] as any
145+
if (!schema?.properties) break
146+
}
147+
148+
const method = paths.pop()!
149+
// For whatever reason, if failed to infer route correctly
150+
if (!method) continue
151+
152+
const path = '/' + paths.join('/')
153+
schema = schema.properties
154+
155+
if (schema?.response?.type === 'object') {
156+
const responseSchema: Record<string, any> = {}
157+
158+
for (const key in schema.response.properties)
159+
responseSchema[key] = schema.response.properties[key]
160+
161+
schema.response = responseSchema
162+
}
163+
164+
if (!routes[path]) routes[path] = {}
165+
// @ts-ignore
166+
routes[path][method.toLowerCase()] = schema
167+
}
168+
169+
return routes
170+
}
171+
130172
/**
131173
* Auto generate OpenAPI schema from Elysia instance
132174
*
@@ -155,6 +197,10 @@ export const fromTypes =
155197
) =>
156198
() => {
157199
try {
200+
// targetFilePath is an actual dts reference
201+
if (targetFilePath.trim().startsWith('{'))
202+
return declarationToJSONSchema(targetFilePath)
203+
158204
if (
159205
!targetFilePath.endsWith('.ts') &&
160206
!targetFilePath.endsWith('.tsx')
@@ -326,49 +372,7 @@ export const fromTypes =
326372
)
327373
)
328374

329-
const routes: AdditionalReference = {}
330-
331-
// Treaty is a collection of { ... } & { ... } & { ... }
332-
for (const route of extractRootObjects(
333-
instance.slice(2).replace(matchStatus, '"$1":')
334-
)) {
335-
let schema = TypeBox(route.replaceAll(/readonly/g, ''))
336-
if (schema.type !== 'object') continue
337-
338-
const paths = []
339-
340-
while (true) {
341-
const keys = Object.keys(schema.properties)
342-
if (keys.length !== 1) break
343-
344-
paths.push(keys[0])
345-
346-
schema = schema.properties[keys[0]] as any
347-
if (!schema?.properties) break
348-
}
349-
350-
const method = paths.pop()!
351-
// For whatever reason, if failed to infer route correctly
352-
if (!method) continue
353-
354-
const path = '/' + paths.join('/')
355-
schema = schema.properties
356-
357-
if (schema?.response?.type === 'object') {
358-
const responseSchema: Record<string, any> = {}
359-
360-
for (const key in schema.response.properties)
361-
responseSchema[key] = schema.response.properties[key]
362-
363-
schema.response = responseSchema
364-
}
365-
366-
if (!routes[path]) routes[path] = {}
367-
// @ts-ignore
368-
routes[path][method.toLowerCase()] = schema
369-
}
370-
371-
return routes
375+
return declarationToJSONSchema(instance.slice(2))
372376
} catch (error) {
373377
console.warn(
374378
'[@elysiajs/openapi/gen] Failed to generate OpenAPI schema'

0 commit comments

Comments
 (0)