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.
@@ -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
  }
@@ -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
+ }
@@ -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: data as FrontMatter,
73
+ frontmatter,
50
74
  source: content,
51
75
  toc: extractToc(content),
76
+ lastUpdated,
77
+ lastUpdatedAuthor,
52
78
  }
53
79
  }