camelmind 0.1.1 → 0.2.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.
- package/package.json +2 -1
- package/template/app/[...slug]/page.tsx +27 -5
- package/template/app/api/feedback/route.ts +40 -0
- package/template/app/api-reference/[[...slug]]/page.tsx +165 -0
- package/template/app/home/page.tsx +6 -0
- package/template/components/ApiReference/ApiOverview.tsx +63 -0
- package/template/components/ApiReference/ApiSidebar.tsx +69 -0
- package/template/components/ApiReference/CodeSamples.tsx +186 -0
- package/template/components/ApiReference/EndpointPage.tsx +92 -0
- package/template/components/ApiReference/ExpandableCode.tsx +132 -0
- package/template/components/ApiReference/MethodBadge.tsx +29 -0
- package/template/components/ApiReference/ParamsTable.tsx +70 -0
- package/template/components/ApiReference/ResponseContext.tsx +32 -0
- package/template/components/ApiReference/ResponseDocs.tsx +135 -0
- package/template/components/ApiReference/ResponseExample.tsx +98 -0
- package/template/components/ApiReference/SchemaViewer.tsx +205 -0
- package/template/components/DocFeedback/DocFeedback.tsx +155 -0
- package/template/components/LastUpdated/LastUpdated.tsx +23 -0
- package/template/components/Nav/TopNav.tsx +14 -1
- package/template/components/PageNav/PageNav.tsx +2 -2
- package/template/lib/api-reference.ts +441 -0
- package/template/lib/api-types.ts +102 -0
- package/template/lib/api-utils.ts +62 -0
- package/template/lib/config-types.ts +16 -0
- package/template/lib/config.ts +12 -0
- package/template/lib/mdx.ts +27 -1
- package/template/lib/versions.ts +26 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import yaml from "js-yaml"
|
|
4
|
+
import { resolveRef } from "./api-utils"
|
|
5
|
+
import type {
|
|
6
|
+
ParsedSpec,
|
|
7
|
+
ParsedOperation,
|
|
8
|
+
ParsedTag,
|
|
9
|
+
ParsedParameter,
|
|
10
|
+
ParsedRequestBody,
|
|
11
|
+
ParsedResponse,
|
|
12
|
+
CodeSample,
|
|
13
|
+
SchemaObject,
|
|
14
|
+
SecuritySchemeObject,
|
|
15
|
+
HttpMethod,
|
|
16
|
+
} from "./api-types"
|
|
17
|
+
|
|
18
|
+
let _cache: ParsedSpec | null = null
|
|
19
|
+
let _cachedLanguages: string | null = null
|
|
20
|
+
|
|
21
|
+
export function loadApiSpec(specPath: string, languages?: string[]): ParsedSpec {
|
|
22
|
+
const langKey = JSON.stringify(languages ?? null)
|
|
23
|
+
if (_cache && _cachedLanguages === langKey) return _cache
|
|
24
|
+
const raw = fs.readFileSync(path.join(process.cwd(), specPath), "utf-8")
|
|
25
|
+
const doc = yaml.load(raw) as Record<string, unknown>
|
|
26
|
+
_cache = parseOpenApi(doc, languages)
|
|
27
|
+
_cachedLanguages = langKey
|
|
28
|
+
return _cache
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function clearApiCache() {
|
|
32
|
+
_cache = null
|
|
33
|
+
_cachedLanguages = null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function slugify(str: string): string {
|
|
37
|
+
return str.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Generate a single example value from a schema (one level, no deep recursion)
|
|
41
|
+
function examplePrimitive(schema: SchemaObject): unknown {
|
|
42
|
+
if (schema.example !== undefined) return schema.example
|
|
43
|
+
if (schema.default !== undefined) return schema.default
|
|
44
|
+
if (schema.enum?.length) return schema.enum[0]
|
|
45
|
+
switch (schema.type) {
|
|
46
|
+
case "string":
|
|
47
|
+
if (schema.format === "date-time") return "2024-01-15T10:30:00Z"
|
|
48
|
+
if (schema.format === "date") return "2024-01-15"
|
|
49
|
+
if (schema.format === "uuid") return "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
50
|
+
if (schema.format === "email") return "user@example.com"
|
|
51
|
+
if (schema.format === "uri") return "https://example.com"
|
|
52
|
+
return "string"
|
|
53
|
+
case "integer":
|
|
54
|
+
return schema.minimum ?? 1
|
|
55
|
+
case "number":
|
|
56
|
+
return schema.minimum ?? 0
|
|
57
|
+
case "boolean":
|
|
58
|
+
return true
|
|
59
|
+
default:
|
|
60
|
+
return "string"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function generateExampleValue(
|
|
65
|
+
schema: SchemaObject,
|
|
66
|
+
schemas: Record<string, SchemaObject>,
|
|
67
|
+
depth = 0
|
|
68
|
+
): unknown {
|
|
69
|
+
if (depth > 4) return {}
|
|
70
|
+
|
|
71
|
+
if (schema.$ref) {
|
|
72
|
+
const resolved = resolveRef(schema.$ref, schemas)
|
|
73
|
+
if (!resolved) return {}
|
|
74
|
+
return generateExampleValue(resolved, schemas, depth + 1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (schema.allOf?.length) {
|
|
78
|
+
const merged: Record<string, unknown> = {}
|
|
79
|
+
for (const sub of schema.allOf) {
|
|
80
|
+
const val = generateExampleValue(sub, schemas, depth + 1)
|
|
81
|
+
if (typeof val === "object" && val !== null) Object.assign(merged, val)
|
|
82
|
+
}
|
|
83
|
+
return merged
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (schema.type === "object" || schema.properties) {
|
|
87
|
+
const obj: Record<string, unknown> = {}
|
|
88
|
+
for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
|
|
89
|
+
obj[key] = generateExampleValue(propSchema, schemas, depth + 1)
|
|
90
|
+
}
|
|
91
|
+
return obj
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (schema.type === "array") {
|
|
95
|
+
const item = schema.items
|
|
96
|
+
? generateExampleValue(schema.items, schemas, depth + 1)
|
|
97
|
+
: "string"
|
|
98
|
+
return [item]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return examplePrimitive(schema)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- Code generators ----
|
|
105
|
+
|
|
106
|
+
function interpolatePath(urlPath: string, parameters: ParsedParameter[]): string {
|
|
107
|
+
const pathParams = parameters.filter((p) => p.in === "path")
|
|
108
|
+
let result = urlPath
|
|
109
|
+
for (const p of pathParams) {
|
|
110
|
+
const val = p.example ?? examplePrimitive(p.schema)
|
|
111
|
+
result = result.replace(`{${p.name}}`, String(val))
|
|
112
|
+
}
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildQueryString(parameters: ParsedParameter[]): string {
|
|
117
|
+
const qp = parameters.filter((p) => p.in === "query" && p.required)
|
|
118
|
+
if (!qp.length) return ""
|
|
119
|
+
const pairs = qp.map((p) => {
|
|
120
|
+
const val = p.example ?? examplePrimitive(p.schema)
|
|
121
|
+
return `${p.name}=${encodeURIComponent(String(val))}`
|
|
122
|
+
})
|
|
123
|
+
return "?" + pairs.join("&")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function detectAuthHeader(securitySchemes: Record<string, SecuritySchemeObject>, schemeName: string): string | null {
|
|
127
|
+
const scheme = securitySchemes[schemeName]
|
|
128
|
+
if (!scheme) return null
|
|
129
|
+
if (scheme.type === "http" && scheme.scheme === "bearer") return "Bearer <your-token>"
|
|
130
|
+
if (scheme.type === "apiKey" && scheme.in === "header") return null // custom header
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getAuthHeader(op: ParsedOperation, securitySchemes: Record<string, SecuritySchemeObject>): string | null {
|
|
135
|
+
for (const name of op.securitySchemes) {
|
|
136
|
+
const header = detectAuthHeader(securitySchemes, name)
|
|
137
|
+
if (header) return header
|
|
138
|
+
}
|
|
139
|
+
return op.securitySchemes.length ? "Bearer <your-token>" : null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generateCurl(
|
|
143
|
+
op: ParsedOperation,
|
|
144
|
+
baseUrl: string,
|
|
145
|
+
schemas: Record<string, SchemaObject>,
|
|
146
|
+
securitySchemes: Record<string, SecuritySchemeObject>
|
|
147
|
+
): string {
|
|
148
|
+
const fullPath = interpolatePath(op.path, op.parameters)
|
|
149
|
+
const qs = buildQueryString(op.parameters)
|
|
150
|
+
const url = `${baseUrl}${fullPath}${qs}`
|
|
151
|
+
const method = op.method.toUpperCase()
|
|
152
|
+
const auth = getAuthHeader(op, securitySchemes)
|
|
153
|
+
|
|
154
|
+
const lines: string[] = [`curl -X ${method} '${url}'`]
|
|
155
|
+
if (auth) lines.push(` -H 'Authorization: ${auth}'`)
|
|
156
|
+
if (op.requestBody) {
|
|
157
|
+
lines.push(` -H 'Content-Type: ${op.requestBody.contentType}'`)
|
|
158
|
+
const body = generateExampleValue(op.requestBody.schema, schemas)
|
|
159
|
+
lines.push(` -d '${JSON.stringify(body, null, 2).replace(/'/g, "\\'")}'`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return lines.join(" \\\n")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function generatePython(
|
|
166
|
+
op: ParsedOperation,
|
|
167
|
+
baseUrl: string,
|
|
168
|
+
schemas: Record<string, SchemaObject>,
|
|
169
|
+
securitySchemes: Record<string, SecuritySchemeObject>
|
|
170
|
+
): string {
|
|
171
|
+
const fullPath = interpolatePath(op.path, op.parameters)
|
|
172
|
+
const qs = buildQueryString(op.parameters)
|
|
173
|
+
const url = `${baseUrl}${fullPath}${qs}`
|
|
174
|
+
const method = op.method.toLowerCase()
|
|
175
|
+
const auth = getAuthHeader(op, securitySchemes)
|
|
176
|
+
|
|
177
|
+
const headers: Record<string, string> = {}
|
|
178
|
+
if (auth) headers["Authorization"] = auth
|
|
179
|
+
if (op.requestBody) headers["Content-Type"] = op.requestBody.contentType
|
|
180
|
+
|
|
181
|
+
const headersStr = JSON.stringify(headers, null, 4).replace(/"([^"]+)":/g, '"$1":')
|
|
182
|
+
const lines: string[] = ["import requests", ""]
|
|
183
|
+
|
|
184
|
+
const args = [`\n "${url}",`, ` headers=${headersStr},`]
|
|
185
|
+
if (op.requestBody) {
|
|
186
|
+
const body = generateExampleValue(op.requestBody.schema, schemas)
|
|
187
|
+
args.push(` json=${JSON.stringify(body, null, 4)},`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lines.push(`response = requests.${method}(${args.join("\n")}\n)`)
|
|
191
|
+
lines.push("print(response.json())")
|
|
192
|
+
|
|
193
|
+
return lines.join("\n")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function generateJavaScript(
|
|
197
|
+
op: ParsedOperation,
|
|
198
|
+
baseUrl: string,
|
|
199
|
+
schemas: Record<string, SchemaObject>,
|
|
200
|
+
securitySchemes: Record<string, SecuritySchemeObject>
|
|
201
|
+
): string {
|
|
202
|
+
const fullPath = interpolatePath(op.path, op.parameters)
|
|
203
|
+
const qs = buildQueryString(op.parameters)
|
|
204
|
+
const url = `${baseUrl}${fullPath}${qs}`
|
|
205
|
+
const method = op.method.toUpperCase()
|
|
206
|
+
const auth = getAuthHeader(op, securitySchemes)
|
|
207
|
+
|
|
208
|
+
const headers: Record<string, string> = {}
|
|
209
|
+
if (auth) headers["Authorization"] = auth
|
|
210
|
+
if (op.requestBody) headers["Content-Type"] = op.requestBody.contentType
|
|
211
|
+
|
|
212
|
+
const opts: string[] = [` method: "${method}",`]
|
|
213
|
+
if (Object.keys(headers).length) {
|
|
214
|
+
opts.push(` headers: ${JSON.stringify(headers, null, 4).split("\n").map((l, i) => (i === 0 ? l : " " + l)).join("\n")},`)
|
|
215
|
+
}
|
|
216
|
+
if (op.requestBody) {
|
|
217
|
+
const body = generateExampleValue(op.requestBody.schema, schemas)
|
|
218
|
+
opts.push(` body: JSON.stringify(${JSON.stringify(body, null, 4).split("\n").map((l, i) => (i === 0 ? l : " " + l)).join("\n")}),`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return [
|
|
222
|
+
`const response = await fetch("${url}", {`,
|
|
223
|
+
opts.join("\n"),
|
|
224
|
+
"});",
|
|
225
|
+
"const data = await response.json();",
|
|
226
|
+
"console.log(data);",
|
|
227
|
+
].join("\n")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function generateGo(
|
|
231
|
+
op: ParsedOperation,
|
|
232
|
+
baseUrl: string,
|
|
233
|
+
schemas: Record<string, SchemaObject>,
|
|
234
|
+
securitySchemes: Record<string, SecuritySchemeObject>
|
|
235
|
+
): string {
|
|
236
|
+
const fullPath = interpolatePath(op.path, op.parameters)
|
|
237
|
+
const qs = buildQueryString(op.parameters)
|
|
238
|
+
const url = `${baseUrl}${fullPath}${qs}`
|
|
239
|
+
const method = op.method.toUpperCase()
|
|
240
|
+
const auth = getAuthHeader(op, securitySchemes)
|
|
241
|
+
const hasBody = !!op.requestBody
|
|
242
|
+
|
|
243
|
+
const imports = ["fmt", "io", "net/http"]
|
|
244
|
+
if (hasBody) imports.splice(1, 0, "bytes", "encoding/json")
|
|
245
|
+
|
|
246
|
+
const lines: string[] = [
|
|
247
|
+
"package main",
|
|
248
|
+
"",
|
|
249
|
+
"import (",
|
|
250
|
+
...imports.map((i) => `\t"${i}"`),
|
|
251
|
+
")",
|
|
252
|
+
"",
|
|
253
|
+
"func main() {",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
if (hasBody) {
|
|
257
|
+
const body = generateExampleValue(op.requestBody!.schema, schemas)
|
|
258
|
+
lines.push(`\tbody, _ := json.Marshal(${JSON.stringify(body)})`)
|
|
259
|
+
lines.push(`\treq, _ := http.NewRequest("${method}", "${url}", bytes.NewBuffer(body))`)
|
|
260
|
+
} else {
|
|
261
|
+
lines.push(`\treq, _ := http.NewRequest("${method}", "${url}", nil)`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (auth) lines.push(`\treq.Header.Set("Authorization", "${auth}")`)
|
|
265
|
+
if (hasBody) lines.push(`\treq.Header.Set("Content-Type", "${op.requestBody!.contentType}")`)
|
|
266
|
+
|
|
267
|
+
lines.push(
|
|
268
|
+
"",
|
|
269
|
+
"\tresp, _ := http.DefaultClient.Do(req)",
|
|
270
|
+
"\tdefer resp.Body.Close()",
|
|
271
|
+
"\tbody2, _ := io.ReadAll(resp.Body)",
|
|
272
|
+
"\tfmt.Println(string(body2))",
|
|
273
|
+
"}"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return lines.join("\n")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function generateCodeSamples(
|
|
280
|
+
op: ParsedOperation,
|
|
281
|
+
baseUrl: string,
|
|
282
|
+
schemas: Record<string, SchemaObject>,
|
|
283
|
+
securitySchemes: Record<string, SecuritySchemeObject>,
|
|
284
|
+
languages: string[]
|
|
285
|
+
): CodeSample[] {
|
|
286
|
+
const generators: Record<string, () => string> = {
|
|
287
|
+
curl: () => generateCurl(op, baseUrl, schemas, securitySchemes),
|
|
288
|
+
python: () => generatePython(op, baseUrl, schemas, securitySchemes),
|
|
289
|
+
javascript: () => generateJavaScript(op, baseUrl, schemas, securitySchemes),
|
|
290
|
+
go: () => generateGo(op, baseUrl, schemas, securitySchemes),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return languages.flatMap((lang) => {
|
|
294
|
+
const gen = generators[lang.toLowerCase()]
|
|
295
|
+
if (!gen) return []
|
|
296
|
+
return [{ lang, source: gen() }]
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---- OpenAPI parser ----
|
|
301
|
+
|
|
302
|
+
const HTTP_METHODS: HttpMethod[] = ["get", "post", "put", "delete", "patch", "head", "options"]
|
|
303
|
+
|
|
304
|
+
function parseOperation(
|
|
305
|
+
method: HttpMethod,
|
|
306
|
+
urlPath: string,
|
|
307
|
+
rawOp: Record<string, unknown>,
|
|
308
|
+
globalSecurity: Array<Record<string, unknown>>,
|
|
309
|
+
schemas: Record<string, SchemaObject>,
|
|
310
|
+
securitySchemes: Record<string, SecuritySchemeObject>,
|
|
311
|
+
languages: string[]
|
|
312
|
+
): ParsedOperation {
|
|
313
|
+
const tags = (rawOp.tags as string[] | undefined) ?? ["default"]
|
|
314
|
+
const tag = tags[0]
|
|
315
|
+
const operationId = (rawOp.operationId as string | undefined) ??
|
|
316
|
+
`${method}-${urlPath.replace(/\//g, "-").replace(/[{}]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "")}`
|
|
317
|
+
|
|
318
|
+
const rawParams = (rawOp.parameters as Record<string, unknown>[] | undefined) ?? []
|
|
319
|
+
const parameters: ParsedParameter[] = rawParams.map((p) => ({
|
|
320
|
+
name: p.name as string,
|
|
321
|
+
in: p.in as ParsedParameter["in"],
|
|
322
|
+
description: p.description as string | undefined,
|
|
323
|
+
required: (p.required as boolean | undefined) ?? (p.in === "path"),
|
|
324
|
+
schema: (p.schema as SchemaObject | undefined) ?? { type: "string" },
|
|
325
|
+
example: p.example,
|
|
326
|
+
deprecated: p.deprecated as boolean | undefined,
|
|
327
|
+
}))
|
|
328
|
+
|
|
329
|
+
let requestBody: ParsedRequestBody | undefined
|
|
330
|
+
const rawBody = rawOp.requestBody as Record<string, unknown> | undefined
|
|
331
|
+
if (rawBody) {
|
|
332
|
+
const content = rawBody.content as Record<string, Record<string, unknown>> | undefined
|
|
333
|
+
const contentType = content ? Object.keys(content)[0] : "application/json"
|
|
334
|
+
const mediaType = content?.[contentType] ?? {}
|
|
335
|
+
requestBody = {
|
|
336
|
+
description: rawBody.description as string | undefined,
|
|
337
|
+
required: (rawBody.required as boolean | undefined) ?? false,
|
|
338
|
+
contentType,
|
|
339
|
+
schema: (mediaType.schema as SchemaObject | undefined) ?? {},
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const rawResponses = (rawOp.responses as Record<string, Record<string, unknown>> | undefined) ?? {}
|
|
344
|
+
const responses: ParsedResponse[] = Object.entries(rawResponses).map(([code, resp]) => {
|
|
345
|
+
const content = resp.content as Record<string, Record<string, unknown>> | undefined
|
|
346
|
+
const contentType = content ? Object.keys(content)[0] : undefined
|
|
347
|
+
const schema = contentType ? (content?.[contentType]?.schema as SchemaObject | undefined) : undefined
|
|
348
|
+
return {
|
|
349
|
+
statusCode: code,
|
|
350
|
+
description: (resp.description as string | undefined) ?? "",
|
|
351
|
+
contentType,
|
|
352
|
+
schema,
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const rawSecurity = (rawOp.security as Array<Record<string, unknown>> | undefined) ?? globalSecurity
|
|
357
|
+
const securitySchemeNames = rawSecurity.flatMap((req) => Object.keys(req))
|
|
358
|
+
|
|
359
|
+
const rawCodeSamples = rawOp["x-codeSamples"] as Array<{ lang: string; label?: string; source: string }> | undefined
|
|
360
|
+
const codeSamples = rawCodeSamples?.length
|
|
361
|
+
? rawCodeSamples
|
|
362
|
+
: generateCodeSamples(
|
|
363
|
+
{ tag, tagSlug: slugify(tag), operationId, summary: "", method, path: urlPath, parameters, requestBody, responses, securitySchemes: securitySchemeNames, codeSamples: [], deprecated: false },
|
|
364
|
+
"",
|
|
365
|
+
schemas,
|
|
366
|
+
securitySchemes,
|
|
367
|
+
languages
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
tag,
|
|
372
|
+
tagSlug: slugify(tag),
|
|
373
|
+
operationId,
|
|
374
|
+
summary: (rawOp.summary as string | undefined) ?? operationId,
|
|
375
|
+
description: rawOp.description as string | undefined,
|
|
376
|
+
method,
|
|
377
|
+
path: urlPath,
|
|
378
|
+
parameters,
|
|
379
|
+
requestBody,
|
|
380
|
+
responses,
|
|
381
|
+
securitySchemes: securitySchemeNames,
|
|
382
|
+
codeSamples,
|
|
383
|
+
deprecated: rawOp.deprecated as boolean | undefined,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function parseOpenApi(doc: Record<string, unknown>, configLanguages?: string[]): ParsedSpec {
|
|
388
|
+
const info = (doc.info as Record<string, unknown>) ?? {}
|
|
389
|
+
const servers = (doc.servers as Array<{ url: string; description?: string }> | undefined) ?? []
|
|
390
|
+
const baseUrl = servers[0]?.url ?? ""
|
|
391
|
+
const rawTags = (doc.tags as Array<{ name: string; description?: string }> | undefined) ?? []
|
|
392
|
+
const components = (doc.components as Record<string, unknown>) ?? {}
|
|
393
|
+
const schemas = ((components.schemas as Record<string, SchemaObject>) ?? {})
|
|
394
|
+
const securitySchemes = ((components.securitySchemes as Record<string, SecuritySchemeObject>) ?? {})
|
|
395
|
+
const globalSecurity = (doc.security as Array<Record<string, unknown>> | undefined) ?? []
|
|
396
|
+
|
|
397
|
+
const specLanguages = (doc as Record<string, unknown>)["x-languages"] as string[] | undefined
|
|
398
|
+
const languages = configLanguages ?? specLanguages ?? ["curl", "python", "javascript", "go"]
|
|
399
|
+
|
|
400
|
+
const paths = (doc.paths as Record<string, Record<string, unknown>>) ?? {}
|
|
401
|
+
const allOperations: ParsedOperation[] = []
|
|
402
|
+
|
|
403
|
+
for (const [urlPath, pathItem] of Object.entries(paths)) {
|
|
404
|
+
for (const method of HTTP_METHODS) {
|
|
405
|
+
const rawOp = pathItem[method] as Record<string, unknown> | undefined
|
|
406
|
+
if (!rawOp) continue
|
|
407
|
+
const op = parseOperation(method, urlPath, rawOp, globalSecurity, schemas, securitySchemes, languages)
|
|
408
|
+
// Re-generate code samples now that we have the base URL
|
|
409
|
+
if (!(rawOp["x-codeSamples"])) {
|
|
410
|
+
op.codeSamples = generateCodeSamples(op, baseUrl, schemas, securitySchemes, languages)
|
|
411
|
+
}
|
|
412
|
+
allOperations.push(op)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Build tag map — preserve order from spec tags, then append any unmentioned tags
|
|
417
|
+
const tagOrder = rawTags.map((t) => t.name)
|
|
418
|
+
const allTagNames = [...new Set([...tagOrder, ...allOperations.map((o) => o.tag)])]
|
|
419
|
+
|
|
420
|
+
const tagDescMap = Object.fromEntries(rawTags.map((t) => [t.name, t.description]))
|
|
421
|
+
|
|
422
|
+
const tags: ParsedTag[] = allTagNames.map((name) => ({
|
|
423
|
+
name,
|
|
424
|
+
slug: slugify(name),
|
|
425
|
+
description: tagDescMap[name],
|
|
426
|
+
operations: allOperations.filter((o) => o.tag === name),
|
|
427
|
+
})).filter((t) => t.operations.length > 0)
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
info: {
|
|
431
|
+
title: (info.title as string) ?? "API Reference",
|
|
432
|
+
version: (info.version as string) ?? "v1",
|
|
433
|
+
description: info.description as string | undefined,
|
|
434
|
+
},
|
|
435
|
+
baseUrl,
|
|
436
|
+
tags,
|
|
437
|
+
allOperations,
|
|
438
|
+
schemas,
|
|
439
|
+
securitySchemes,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "head" | "options"
|
|
2
|
+
|
|
3
|
+
export type SchemaObject = {
|
|
4
|
+
type?: string
|
|
5
|
+
format?: string
|
|
6
|
+
description?: string
|
|
7
|
+
title?: string
|
|
8
|
+
properties?: Record<string, SchemaObject>
|
|
9
|
+
additionalProperties?: SchemaObject | boolean
|
|
10
|
+
items?: SchemaObject
|
|
11
|
+
required?: string[]
|
|
12
|
+
enum?: (string | number | boolean | null)[]
|
|
13
|
+
$ref?: string
|
|
14
|
+
allOf?: SchemaObject[]
|
|
15
|
+
anyOf?: SchemaObject[]
|
|
16
|
+
oneOf?: SchemaObject[]
|
|
17
|
+
nullable?: boolean
|
|
18
|
+
example?: unknown
|
|
19
|
+
default?: unknown
|
|
20
|
+
minimum?: number
|
|
21
|
+
maximum?: number
|
|
22
|
+
minLength?: number
|
|
23
|
+
maxLength?: number
|
|
24
|
+
pattern?: string
|
|
25
|
+
readOnly?: boolean
|
|
26
|
+
writeOnly?: boolean
|
|
27
|
+
deprecated?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ParsedParameter = {
|
|
31
|
+
name: string
|
|
32
|
+
in: "path" | "query" | "header" | "cookie"
|
|
33
|
+
description?: string
|
|
34
|
+
required: boolean
|
|
35
|
+
schema: SchemaObject
|
|
36
|
+
example?: unknown
|
|
37
|
+
deprecated?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ParsedRequestBody = {
|
|
41
|
+
description?: string
|
|
42
|
+
required: boolean
|
|
43
|
+
contentType: string
|
|
44
|
+
schema: SchemaObject
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ParsedResponse = {
|
|
48
|
+
statusCode: string
|
|
49
|
+
description: string
|
|
50
|
+
contentType?: string
|
|
51
|
+
schema?: SchemaObject
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CodeSample = {
|
|
55
|
+
lang: string
|
|
56
|
+
label?: string
|
|
57
|
+
source: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ParsedOperation = {
|
|
61
|
+
tag: string
|
|
62
|
+
tagSlug: string
|
|
63
|
+
operationId: string
|
|
64
|
+
summary: string
|
|
65
|
+
description?: string
|
|
66
|
+
method: HttpMethod
|
|
67
|
+
path: string
|
|
68
|
+
parameters: ParsedParameter[]
|
|
69
|
+
requestBody?: ParsedRequestBody
|
|
70
|
+
responses: ParsedResponse[]
|
|
71
|
+
securitySchemes: string[]
|
|
72
|
+
codeSamples: CodeSample[]
|
|
73
|
+
deprecated?: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ParsedTag = {
|
|
77
|
+
name: string
|
|
78
|
+
slug: string
|
|
79
|
+
description?: string
|
|
80
|
+
operations: ParsedOperation[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type SecuritySchemeObject = {
|
|
84
|
+
type: "apiKey" | "http" | "oauth2" | "openIdConnect"
|
|
85
|
+
scheme?: string
|
|
86
|
+
in?: string
|
|
87
|
+
name?: string
|
|
88
|
+
description?: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type ParsedSpec = {
|
|
92
|
+
info: {
|
|
93
|
+
title: string
|
|
94
|
+
version: string
|
|
95
|
+
description?: string
|
|
96
|
+
}
|
|
97
|
+
baseUrl: string
|
|
98
|
+
tags: ParsedTag[]
|
|
99
|
+
allOperations: ParsedOperation[]
|
|
100
|
+
schemas: Record<string, SchemaObject>
|
|
101
|
+
securitySchemes: Record<string, SecuritySchemeObject>
|
|
102
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SchemaObject } from "./api-types"
|
|
2
|
+
|
|
3
|
+
export function resolveRef(
|
|
4
|
+
ref: string,
|
|
5
|
+
schemas: Record<string, SchemaObject>
|
|
6
|
+
): SchemaObject | null {
|
|
7
|
+
const match = ref.match(/^#\/components\/schemas\/(.+)$/)
|
|
8
|
+
if (!match) return null
|
|
9
|
+
return schemas[match[1]] ?? null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Shared example builder — used by both left-panel schema docs and right-panel response examples
|
|
13
|
+
export function buildExample(
|
|
14
|
+
schema: SchemaObject,
|
|
15
|
+
schemas: Record<string, SchemaObject>,
|
|
16
|
+
depth = 0
|
|
17
|
+
): unknown {
|
|
18
|
+
if (depth > 5) return {}
|
|
19
|
+
|
|
20
|
+
if (schema.$ref) {
|
|
21
|
+
const resolved = resolveRef(schema.$ref, schemas)
|
|
22
|
+
return resolved ? buildExample(resolved, schemas, depth + 1) : {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (schema.allOf?.length) {
|
|
26
|
+
const merged: Record<string, unknown> = {}
|
|
27
|
+
for (const sub of schema.allOf) {
|
|
28
|
+
const val = buildExample(sub, schemas, depth + 1)
|
|
29
|
+
if (typeof val === "object" && val !== null) Object.assign(merged, val)
|
|
30
|
+
}
|
|
31
|
+
return merged
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (schema.type === "array") {
|
|
35
|
+
const item = schema.items ? buildExample(schema.items, schemas, depth + 1) : "string"
|
|
36
|
+
return [item]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (schema.type === "object" || schema.properties) {
|
|
40
|
+
const obj: Record<string, unknown> = {}
|
|
41
|
+
for (const [key, prop] of Object.entries(schema.properties ?? {}) as [string, SchemaObject][]) {
|
|
42
|
+
if (prop.example !== undefined) { obj[key] = prop.example; continue }
|
|
43
|
+
if (prop.default !== undefined) { obj[key] = prop.default; continue }
|
|
44
|
+
obj[key] = buildExample(prop, schemas, depth + 1)
|
|
45
|
+
}
|
|
46
|
+
return obj
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (schema.example !== undefined) return schema.example
|
|
50
|
+
if (schema.default !== undefined) return schema.default
|
|
51
|
+
if (schema.enum?.length) return schema.enum[0]
|
|
52
|
+
if (schema.type === "string") {
|
|
53
|
+
if (schema.format === "date-time") return "2024-01-15T10:30:00Z"
|
|
54
|
+
if (schema.format === "date") return "2024-01-15"
|
|
55
|
+
if (schema.format === "uuid") return "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
56
|
+
if (schema.format === "email") return "user@example.com"
|
|
57
|
+
return "string"
|
|
58
|
+
}
|
|
59
|
+
if (schema.type === "integer" || schema.type === "number") return 0
|
|
60
|
+
if (schema.type === "boolean") return true
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
@@ -16,6 +16,20 @@ export type AuthConfig = {
|
|
|
16
16
|
publicPaths: string[]
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type SiteFeatures = {
|
|
20
|
+
showLastUpdated?: boolean
|
|
21
|
+
showLastUpdateAuthor?: boolean
|
|
22
|
+
showFeedbackWidget?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ApiReferenceConfig = {
|
|
26
|
+
enabled: boolean
|
|
27
|
+
spec: string
|
|
28
|
+
navLabel?: string
|
|
29
|
+
languages?: string[]
|
|
30
|
+
roles?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
export type CamelMindConfig = {
|
|
20
34
|
title: string
|
|
21
35
|
tagline: string
|
|
@@ -27,4 +41,6 @@ export type CamelMindConfig = {
|
|
|
27
41
|
links: {
|
|
28
42
|
github?: string
|
|
29
43
|
}
|
|
44
|
+
site?: SiteFeatures
|
|
45
|
+
apiReference?: ApiReferenceConfig
|
|
30
46
|
}
|
package/template/lib/config.ts
CHANGED
|
@@ -27,3 +27,15 @@ export function isPublicPath(pathname: string): boolean {
|
|
|
27
27
|
const { publicPaths } = getAuthConfig()
|
|
28
28
|
return publicPaths.some((p) => pathname === p || pathname.startsWith(p + "/"))
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export function showLastUpdated(): boolean {
|
|
32
|
+
return getConfig().site?.showLastUpdated !== false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function showLastUpdateAuthor(): boolean {
|
|
36
|
+
return getConfig().site?.showLastUpdateAuthor === true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function showFeedbackWidget(): boolean {
|
|
40
|
+
return getConfig().site?.showFeedbackWidget === true
|
|
41
|
+
}
|
package/template/lib/mdx.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs"
|
|
2
2
|
import path from "path"
|
|
3
|
+
import { execSync } from "child_process"
|
|
3
4
|
import matter from "gray-matter"
|
|
4
5
|
|
|
5
6
|
export type FrontMatter = {
|
|
@@ -8,6 +9,7 @@ export type FrontMatter = {
|
|
|
8
9
|
roles?: string[]
|
|
9
10
|
tags?: string[]
|
|
10
11
|
download_pdf?: string
|
|
12
|
+
last_updated?: string
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export type TocEntry = {
|
|
@@ -20,6 +22,8 @@ export type DocContent = {
|
|
|
20
22
|
frontmatter: FrontMatter
|
|
21
23
|
source: string
|
|
22
24
|
toc: TocEntry[]
|
|
25
|
+
lastUpdated: Date | null
|
|
26
|
+
lastUpdatedAuthor: string | null
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
function extractToc(source: string): TocEntry[] {
|
|
@@ -44,10 +48,32 @@ export function loadMdxFile(filePath: string): DocContent {
|
|
|
44
48
|
const fullPath = path.join(process.cwd(), filePath)
|
|
45
49
|
const raw = fs.readFileSync(fullPath, "utf-8")
|
|
46
50
|
const { data, content } = matter(raw)
|
|
51
|
+
const frontmatter = data as FrontMatter
|
|
52
|
+
|
|
53
|
+
let lastUpdated: Date | null = null
|
|
54
|
+
if (frontmatter.last_updated) {
|
|
55
|
+
const parsed = new Date(frontmatter.last_updated)
|
|
56
|
+
if (!isNaN(parsed.getTime())) lastUpdated = parsed
|
|
57
|
+
} else {
|
|
58
|
+
lastUpdated = fs.statSync(fullPath).mtime
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let lastUpdatedAuthor: string | null = null
|
|
62
|
+
try {
|
|
63
|
+
const result = execSync(`git log -1 --format="%an" -- "${fullPath}"`, {
|
|
64
|
+
encoding: "utf-8",
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
}).trim()
|
|
67
|
+
if (result) lastUpdatedAuthor = result
|
|
68
|
+
} catch {
|
|
69
|
+
// not a git repo or git unavailable
|
|
70
|
+
}
|
|
47
71
|
|
|
48
72
|
return {
|
|
49
|
-
frontmatter
|
|
73
|
+
frontmatter,
|
|
50
74
|
source: content,
|
|
51
75
|
toc: extractToc(content),
|
|
76
|
+
lastUpdated,
|
|
77
|
+
lastUpdatedAuthor,
|
|
52
78
|
}
|
|
53
79
|
}
|