specra 0.1.0
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/LICENSE.MD +21 -0
- package/README.md +157 -0
- package/dist/app/api/mdx-watch/route.d.mts +12 -0
- package/dist/app/api/mdx-watch/route.d.ts +12 -0
- package/dist/app/api/mdx-watch/route.js +98 -0
- package/dist/app/api/mdx-watch/route.js.map +1 -0
- package/dist/app/api/mdx-watch/route.mjs +71 -0
- package/dist/app/api/mdx-watch/route.mjs.map +1 -0
- package/dist/app/docs-page.d.mts +32 -0
- package/dist/app/docs-page.d.ts +32 -0
- package/dist/app/docs-page.js +4072 -0
- package/dist/app/docs-page.js.map +1 -0
- package/dist/app/docs-page.mjs +14 -0
- package/dist/app/docs-page.mjs.map +1 -0
- package/dist/app/layout.css +297 -0
- package/dist/app/layout.css.map +1 -0
- package/dist/app/layout.d.mts +19 -0
- package/dist/app/layout.d.ts +19 -0
- package/dist/app/layout.js +112 -0
- package/dist/app/layout.js.map +1 -0
- package/dist/app/layout.mjs +13 -0
- package/dist/app/layout.mjs.map +1 -0
- package/dist/chunk-DR4EPLMT.mjs +1013 -0
- package/dist/chunk-DR4EPLMT.mjs.map +1 -0
- package/dist/chunk-INL2EC72.mjs +170 -0
- package/dist/chunk-INL2EC72.mjs.map +1 -0
- package/dist/chunk-IZFGEAD6.mjs +61 -0
- package/dist/chunk-IZFGEAD6.mjs.map +1 -0
- package/dist/chunk-KTRWWAGL.mjs +50 -0
- package/dist/chunk-KTRWWAGL.mjs.map +1 -0
- package/dist/chunk-MZJHJ6BV.mjs +21 -0
- package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
- package/dist/chunk-NXRIAL7T.mjs +3119 -0
- package/dist/chunk-NXRIAL7T.mjs.map +1 -0
- package/dist/components/index.d.mts +822 -0
- package/dist/components/index.d.ts +822 -0
- package/dist/components/index.js +3738 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +3627 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/index.css +297 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +4648 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +347 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/index.d.mts +798 -0
- package/dist/lib/index.d.ts +798 -0
- package/dist/lib/index.js +1301 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/index.mjs +89 -0
- package/dist/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/app/api/mdx-watch/route.ts +86 -0
- package/src/app/docs-page.tsx +212 -0
- package/src/app/layout.tsx +74 -0
- package/src/components/docs/accordion.tsx +53 -0
- package/src/components/docs/api/api-endpoint.tsx +59 -0
- package/src/components/docs/api/api-params.tsx +43 -0
- package/src/components/docs/api/api-playground.tsx +233 -0
- package/src/components/docs/api/api-reference.tsx +291 -0
- package/src/components/docs/api/api-response.tsx +48 -0
- package/src/components/docs/api/index.ts +5 -0
- package/src/components/docs/badge.tsx +22 -0
- package/src/components/docs/breadcrumb.tsx +51 -0
- package/src/components/docs/callout.tsx +109 -0
- package/src/components/docs/card.tsx +84 -0
- package/src/components/docs/category-index.tsx +112 -0
- package/src/components/docs/code-block.tsx +129 -0
- package/src/components/docs/columns.tsx +45 -0
- package/src/components/docs/componentTextProps.ts +85 -0
- package/src/components/docs/dev-mode-badge.tsx +35 -0
- package/src/components/docs/doc-layout-wrapper.tsx +54 -0
- package/src/components/docs/doc-layout.tsx +111 -0
- package/src/components/docs/doc-loading.tsx +15 -0
- package/src/components/docs/doc-metadata.tsx +55 -0
- package/src/components/docs/doc-navigation.tsx +62 -0
- package/src/components/docs/doc-tags.tsx +25 -0
- package/src/components/docs/draft-badge.tsx +10 -0
- package/src/components/docs/footer.tsx +47 -0
- package/src/components/docs/frame.tsx +22 -0
- package/src/components/docs/header.tsx +122 -0
- package/src/components/docs/hot-reload-indicator.tsx +77 -0
- package/src/components/docs/icon.tsx +70 -0
- package/src/components/docs/image-card.tsx +95 -0
- package/src/components/docs/image.tsx +73 -0
- package/src/components/docs/index.ts +48 -0
- package/src/components/docs/math.tsx +46 -0
- package/src/components/docs/mdx-components.tsx +166 -0
- package/src/components/docs/mdx-hot-reload.tsx +37 -0
- package/src/components/docs/mermaid.tsx +77 -0
- package/src/components/docs/mobile-doc-layout.tsx +115 -0
- package/src/components/docs/not-found-content.tsx +55 -0
- package/src/components/docs/search-highlight.tsx +127 -0
- package/src/components/docs/search-modal.tsx +223 -0
- package/src/components/docs/sidebar-skeleton.tsx +39 -0
- package/src/components/docs/sidebar.tsx +323 -0
- package/src/components/docs/site-banner.tsx +92 -0
- package/src/components/docs/steps.tsx +29 -0
- package/src/components/docs/tab-context.tsx +28 -0
- package/src/components/docs/tab-groups.tsx +50 -0
- package/src/components/docs/table-of-contents.tsx +104 -0
- package/src/components/docs/tabs.tsx +63 -0
- package/src/components/docs/theme-toggle.tsx +39 -0
- package/src/components/docs/tooltip.tsx +37 -0
- package/src/components/docs/version-switcher.tsx +52 -0
- package/src/components/docs/video.tsx +80 -0
- package/src/components/global/index.ts +3 -0
- package/src/components/global/version-not-found.tsx +26 -0
- package/src/components/index.ts +8 -0
- package/src/components/theme-provider.tsx +11 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/index.ts +41 -0
- package/src/lib/api-parser.types.ts +78 -0
- package/src/lib/api.types.ts +202 -0
- package/src/lib/category.ts +71 -0
- package/src/lib/config.server.ts +170 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/config.types.ts +295 -0
- package/src/lib/dev-utils.ts +75 -0
- package/src/lib/index.ts +27 -0
- package/src/lib/mdx-cache.ts +200 -0
- package/src/lib/mdx.ts +402 -0
- package/src/lib/parsers/base-parser.ts +16 -0
- package/src/lib/parsers/index.ts +69 -0
- package/src/lib/parsers/openapi-parser.ts +251 -0
- package/src/lib/parsers/postman-parser.ts +301 -0
- package/src/lib/parsers/specra-parser.ts +24 -0
- package/src/lib/redirects.ts +40 -0
- package/src/lib/remark-code-meta.ts +23 -0
- package/src/lib/sidebar-utils.ts +188 -0
- package/src/lib/toc.ts +24 -0
- package/src/lib/utils.ts +36 -0
- package/src/specra.config.json +124 -0
- package/src/styles/globals.css +427 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { SpecraApiSpec, ApiEndpointSpec, ApiParam, ApiResponse } from "../api-parser.types"
|
|
2
|
+
import type { ApiSpecParser } from "./base-parser"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parser for OpenAPI 3.0/3.1 specifications
|
|
6
|
+
*/
|
|
7
|
+
export class OpenApiParser implements ApiSpecParser {
|
|
8
|
+
validate(input: any): boolean {
|
|
9
|
+
return (
|
|
10
|
+
typeof input === "object" &&
|
|
11
|
+
input !== null &&
|
|
12
|
+
("openapi" in input || "swagger" in input) &&
|
|
13
|
+
"paths" in input
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parse(input: any): SpecraApiSpec {
|
|
18
|
+
if (!this.validate(input)) {
|
|
19
|
+
throw new Error("Invalid OpenAPI spec format")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const baseUrl = this.extractBaseUrl(input)
|
|
23
|
+
const endpoints: ApiEndpointSpec[] = []
|
|
24
|
+
|
|
25
|
+
// Parse paths
|
|
26
|
+
for (const [path, pathItem] of Object.entries(input.paths || {})) {
|
|
27
|
+
const methods = ["get", "post", "put", "patch", "delete"] as const
|
|
28
|
+
|
|
29
|
+
for (const method of methods) {
|
|
30
|
+
const operation = (pathItem as any)[method]
|
|
31
|
+
if (!operation) continue
|
|
32
|
+
|
|
33
|
+
const endpoint = this.parseOperation(path, method.toUpperCase() as any, operation, input)
|
|
34
|
+
endpoints.push(endpoint)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
version: input.info?.version,
|
|
40
|
+
title: input.info?.title,
|
|
41
|
+
description: input.info?.description,
|
|
42
|
+
baseUrl,
|
|
43
|
+
auth: this.extractAuth(input),
|
|
44
|
+
endpoints,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private extractBaseUrl(spec: any): string {
|
|
49
|
+
// OpenAPI 3.x servers
|
|
50
|
+
if (spec.servers && spec.servers.length > 0) {
|
|
51
|
+
return spec.servers[0].url
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Swagger 2.0
|
|
55
|
+
if (spec.host) {
|
|
56
|
+
const scheme = spec.schemes?.[0] || "https"
|
|
57
|
+
const basePath = spec.basePath || ""
|
|
58
|
+
return `${scheme}://${spec.host}${basePath}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ""
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private extractAuth(spec: any): SpecraApiSpec["auth"] {
|
|
65
|
+
const securitySchemes = spec.components?.securitySchemes || spec.securityDefinitions
|
|
66
|
+
|
|
67
|
+
if (!securitySchemes) return undefined
|
|
68
|
+
|
|
69
|
+
// Get the first security scheme
|
|
70
|
+
const firstScheme = Object.values(securitySchemes)[0] as any
|
|
71
|
+
if (!firstScheme) return undefined
|
|
72
|
+
|
|
73
|
+
if (firstScheme.type === "http" && firstScheme.scheme === "bearer") {
|
|
74
|
+
return {
|
|
75
|
+
type: "bearer",
|
|
76
|
+
description: firstScheme.description,
|
|
77
|
+
tokenPrefix: "Bearer",
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (firstScheme.type === "apiKey") {
|
|
82
|
+
return {
|
|
83
|
+
type: "apiKey",
|
|
84
|
+
description: firstScheme.description,
|
|
85
|
+
headerName: firstScheme.name || "X-API-Key",
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (firstScheme.type === "http" && firstScheme.scheme === "basic") {
|
|
90
|
+
return {
|
|
91
|
+
type: "basic",
|
|
92
|
+
description: firstScheme.description,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private parseOperation(
|
|
100
|
+
path: string,
|
|
101
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
|
102
|
+
operation: any,
|
|
103
|
+
spec: any
|
|
104
|
+
): ApiEndpointSpec {
|
|
105
|
+
const endpoint: ApiEndpointSpec = {
|
|
106
|
+
title: operation.summary || operation.operationId || `${method} ${path}`,
|
|
107
|
+
method,
|
|
108
|
+
path: this.convertPathParams(path),
|
|
109
|
+
description: operation.description,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Parse parameters
|
|
113
|
+
const params = this.parseParameters(operation.parameters || [], spec)
|
|
114
|
+
if (params.path.length > 0) endpoint.pathParams = params.path
|
|
115
|
+
if (params.query.length > 0) endpoint.queryParams = params.query
|
|
116
|
+
if (params.header.length > 0) {
|
|
117
|
+
endpoint.headers = params.header.map((p) => ({
|
|
118
|
+
name: p.name,
|
|
119
|
+
value: p.example || "",
|
|
120
|
+
description: p.description,
|
|
121
|
+
}))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse request body
|
|
125
|
+
if (operation.requestBody) {
|
|
126
|
+
endpoint.body = this.parseRequestBody(operation.requestBody, spec)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Parse responses
|
|
130
|
+
const responses = this.parseResponses(operation.responses || {}, spec)
|
|
131
|
+
if (responses.success) endpoint.successResponse = responses.success
|
|
132
|
+
if (responses.errors.length > 0) endpoint.errorResponses = responses.errors
|
|
133
|
+
|
|
134
|
+
return endpoint
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private convertPathParams(path: string): string {
|
|
138
|
+
// Convert OpenAPI {param} to :param
|
|
139
|
+
return path.replace(/\{([^}]+)\}/g, ":$1")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private parseParameters(
|
|
143
|
+
parameters: any[],
|
|
144
|
+
spec: any
|
|
145
|
+
): { path: ApiParam[]; query: ApiParam[]; header: ApiParam[] } {
|
|
146
|
+
const result = { path: [] as ApiParam[], query: [] as ApiParam[], header: [] as ApiParam[] }
|
|
147
|
+
|
|
148
|
+
for (const param of parameters) {
|
|
149
|
+
// Resolve $ref if present
|
|
150
|
+
const resolved = param.$ref ? this.resolveRef(param.$ref, spec) : param
|
|
151
|
+
|
|
152
|
+
const apiParam: ApiParam = {
|
|
153
|
+
name: resolved.name,
|
|
154
|
+
type: resolved.schema?.type || resolved.type || "string",
|
|
155
|
+
required: resolved.required,
|
|
156
|
+
description: resolved.description,
|
|
157
|
+
example: resolved.example || resolved.schema?.example,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (resolved.in === "path") result.path.push(apiParam)
|
|
161
|
+
else if (resolved.in === "query") result.query.push(apiParam)
|
|
162
|
+
else if (resolved.in === "header") result.header.push(apiParam)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private parseRequestBody(requestBody: any, spec: any): ApiEndpointSpec["body"] {
|
|
169
|
+
const content = requestBody.content?.["application/json"]
|
|
170
|
+
if (!content) return undefined
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
description: requestBody.description,
|
|
174
|
+
example: content.example || this.generateExample(content.schema, spec),
|
|
175
|
+
schema: content.schema,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private parseResponses(
|
|
180
|
+
responses: any,
|
|
181
|
+
spec: any
|
|
182
|
+
): { success?: ApiResponse; errors: ApiResponse[] } {
|
|
183
|
+
const result: { success?: ApiResponse; errors: ApiResponse[] } = { errors: [] }
|
|
184
|
+
|
|
185
|
+
for (const [statusCode, response] of Object.entries(responses)) {
|
|
186
|
+
const status = parseInt(statusCode)
|
|
187
|
+
if (isNaN(status)) continue
|
|
188
|
+
|
|
189
|
+
const resolved = (response as any).$ref ? this.resolveRef((response as any).$ref, spec) : response
|
|
190
|
+
const content = (resolved as any).content?.["application/json"]
|
|
191
|
+
|
|
192
|
+
const apiResponse: ApiResponse = {
|
|
193
|
+
status,
|
|
194
|
+
description: (resolved as any).description,
|
|
195
|
+
example: content?.example || this.generateExample(content?.schema, spec),
|
|
196
|
+
schema: content?.schema,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (status >= 200 && status < 300) {
|
|
200
|
+
result.success = apiResponse
|
|
201
|
+
} else {
|
|
202
|
+
result.errors.push(apiResponse)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private generateExample(schema: any, spec: any): any {
|
|
210
|
+
if (!schema) return undefined
|
|
211
|
+
if (schema.$ref) schema = this.resolveRef(schema.$ref, spec)
|
|
212
|
+
if (schema.example) return schema.example
|
|
213
|
+
|
|
214
|
+
// Simple example generation based on schema type
|
|
215
|
+
if (schema.type === "object" && schema.properties) {
|
|
216
|
+
const example: any = {}
|
|
217
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
218
|
+
example[key] = this.generateExample(prop, spec)
|
|
219
|
+
}
|
|
220
|
+
return example
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (schema.type === "array" && schema.items) {
|
|
224
|
+
return [this.generateExample(schema.items, spec)]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Default values by type
|
|
228
|
+
const defaults: any = {
|
|
229
|
+
string: "string",
|
|
230
|
+
number: 0,
|
|
231
|
+
integer: 0,
|
|
232
|
+
boolean: false,
|
|
233
|
+
object: {},
|
|
234
|
+
array: [],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return defaults[schema.type] || null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private resolveRef(ref: string, spec: any): any {
|
|
241
|
+
const path = ref.replace(/^#\//, "").split("/")
|
|
242
|
+
let current = spec
|
|
243
|
+
|
|
244
|
+
for (const segment of path) {
|
|
245
|
+
current = current[segment]
|
|
246
|
+
if (!current) return {}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return current
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { SpecraApiSpec, ApiEndpointSpec, ApiParam, ApiHeader } from "../api-parser.types"
|
|
2
|
+
import type { ApiSpecParser } from "./base-parser"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parser for Postman Collection v2.0/v2.1
|
|
6
|
+
*/
|
|
7
|
+
export class PostmanParser implements ApiSpecParser {
|
|
8
|
+
validate(input: any): boolean {
|
|
9
|
+
return (
|
|
10
|
+
typeof input === "object" &&
|
|
11
|
+
input !== null &&
|
|
12
|
+
"info" in input &&
|
|
13
|
+
input.info?.schema?.includes("v2")
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parse(input: any): SpecraApiSpec {
|
|
18
|
+
if (!this.validate(input)) {
|
|
19
|
+
throw new Error("Invalid Postman Collection format (requires v2.0 or v2.1)")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const baseUrl = this.extractBaseUrl(input)
|
|
23
|
+
const endpoints: ApiEndpointSpec[] = []
|
|
24
|
+
|
|
25
|
+
// Parse items (can be nested in folders)
|
|
26
|
+
this.parseItems(input.item || [], endpoints, baseUrl, input)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
version: input.info?.version,
|
|
30
|
+
title: input.info?.name,
|
|
31
|
+
description: input.info?.description,
|
|
32
|
+
baseUrl,
|
|
33
|
+
auth: this.extractAuth(input.auth),
|
|
34
|
+
globalHeaders: this.extractGlobalHeaders(input),
|
|
35
|
+
endpoints,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private extractBaseUrl(collection: any): string {
|
|
40
|
+
// Try to get from variables
|
|
41
|
+
const baseUrlVar = collection.variable?.find(
|
|
42
|
+
(v: any) => v.key === "baseUrl" || v.key === "base_url" || v.key === "url"
|
|
43
|
+
)
|
|
44
|
+
if (baseUrlVar) return baseUrlVar.value
|
|
45
|
+
|
|
46
|
+
// Try to extract from first request
|
|
47
|
+
if (collection.item && collection.item.length > 0) {
|
|
48
|
+
const firstRequest = this.findFirstRequest(collection.item)
|
|
49
|
+
if (firstRequest?.request?.url) {
|
|
50
|
+
const url = this.parseUrl(firstRequest.request.url)
|
|
51
|
+
if (url.host) {
|
|
52
|
+
return `${url.protocol}://${url.host.join(".")}`
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return ""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private findFirstRequest(items: any[]): any {
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
if (item.request) return item
|
|
63
|
+
if (item.item) {
|
|
64
|
+
const found = this.findFirstRequest(item.item)
|
|
65
|
+
if (found) return found
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private extractAuth(auth: any): SpecraApiSpec["auth"] {
|
|
72
|
+
if (!auth) return undefined
|
|
73
|
+
|
|
74
|
+
if (auth.type === "bearer") {
|
|
75
|
+
return {
|
|
76
|
+
type: "bearer",
|
|
77
|
+
tokenPrefix: "Bearer",
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (auth.type === "apikey") {
|
|
82
|
+
const keyData = auth.apikey?.find((a: any) => a.key === "key")
|
|
83
|
+
const keyName = keyData?.value || "X-API-Key"
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
type: "apiKey",
|
|
87
|
+
headerName: keyName,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (auth.type === "basic") {
|
|
92
|
+
return {
|
|
93
|
+
type: "basic",
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private extractGlobalHeaders(collection: any): ApiHeader[] {
|
|
101
|
+
// Postman doesn't have global headers in the same way, but we can check for common patterns
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private parseItems(items: any[], endpoints: ApiEndpointSpec[], baseUrl: string, collection: any) {
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
// If it's a folder, recurse
|
|
108
|
+
if (item.item && Array.isArray(item.item)) {
|
|
109
|
+
this.parseItems(item.item, endpoints, baseUrl, collection)
|
|
110
|
+
}
|
|
111
|
+
// If it's a request
|
|
112
|
+
else if (item.request) {
|
|
113
|
+
const endpoint = this.parseRequest(item, baseUrl, collection)
|
|
114
|
+
endpoints.push(endpoint)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private parseRequest(item: any, baseUrl: string, collection: any): ApiEndpointSpec {
|
|
120
|
+
const request = item.request
|
|
121
|
+
const url = this.parseUrl(request.url)
|
|
122
|
+
|
|
123
|
+
const endpoint: ApiEndpointSpec = {
|
|
124
|
+
title: item.name,
|
|
125
|
+
method: request.method.toUpperCase(),
|
|
126
|
+
path: this.buildPath(url, baseUrl),
|
|
127
|
+
description: item.request.description || item.description,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Parse URL parameters (path and query)
|
|
131
|
+
const params = this.parseUrlParams(url)
|
|
132
|
+
if (params.path.length > 0) endpoint.pathParams = params.path
|
|
133
|
+
if (params.query.length > 0) endpoint.queryParams = params.query
|
|
134
|
+
|
|
135
|
+
// Parse headers
|
|
136
|
+
if (request.header && request.header.length > 0) {
|
|
137
|
+
endpoint.headers = request.header
|
|
138
|
+
.filter((h: any) => !h.disabled)
|
|
139
|
+
.map((h: any) => ({
|
|
140
|
+
name: h.key,
|
|
141
|
+
value: h.value || "",
|
|
142
|
+
description: h.description,
|
|
143
|
+
}))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Parse request body
|
|
147
|
+
if (request.body) {
|
|
148
|
+
endpoint.body = this.parseBody(request.body)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Parse response examples
|
|
152
|
+
const responses = this.parseResponses(item.response || [])
|
|
153
|
+
if (responses.success) endpoint.successResponse = responses.success
|
|
154
|
+
if (responses.errors.length > 0) endpoint.errorResponses = responses.errors
|
|
155
|
+
|
|
156
|
+
return endpoint
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private parseUrl(url: any): {
|
|
160
|
+
protocol: string
|
|
161
|
+
host: string[]
|
|
162
|
+
path: string[]
|
|
163
|
+
query: any[]
|
|
164
|
+
variable: any[]
|
|
165
|
+
} {
|
|
166
|
+
if (typeof url === "string") {
|
|
167
|
+
// Parse string URL
|
|
168
|
+
const urlObj = new URL(url)
|
|
169
|
+
return {
|
|
170
|
+
protocol: urlObj.protocol.replace(":", ""),
|
|
171
|
+
host: urlObj.hostname.split("."),
|
|
172
|
+
path: urlObj.pathname.split("/").filter(Boolean),
|
|
173
|
+
query: [],
|
|
174
|
+
variable: [],
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
protocol: url.protocol || "https",
|
|
180
|
+
host: url.host || [],
|
|
181
|
+
path: url.path || [],
|
|
182
|
+
query: url.query || [],
|
|
183
|
+
variable: url.variable || [],
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private buildPath(url: any, baseUrl: string): string {
|
|
188
|
+
let path = "/"
|
|
189
|
+
|
|
190
|
+
if (url.path && url.path.length > 0) {
|
|
191
|
+
path += url.path.join("/")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Convert Postman :param to our :param format (they're the same!)
|
|
195
|
+
// But we need to handle {{variable}} syntax
|
|
196
|
+
path = path.replace(/\{\{([^}]+)\}\}/g, ":$1")
|
|
197
|
+
|
|
198
|
+
return path
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private parseUrlParams(url: any): { path: ApiParam[]; query: ApiParam[] } {
|
|
202
|
+
const result = { path: [] as ApiParam[], query: [] as ApiParam[] }
|
|
203
|
+
|
|
204
|
+
// Path parameters from variables
|
|
205
|
+
if (url.variable && url.variable.length > 0) {
|
|
206
|
+
for (const v of url.variable) {
|
|
207
|
+
result.path.push({
|
|
208
|
+
name: v.key,
|
|
209
|
+
type: v.type || "string",
|
|
210
|
+
description: v.description,
|
|
211
|
+
example: v.value,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Extract path params from the path itself
|
|
217
|
+
if (url.path && url.path.length > 0) {
|
|
218
|
+
for (const segment of url.path) {
|
|
219
|
+
if (segment.startsWith(":")) {
|
|
220
|
+
const paramName = segment.slice(1)
|
|
221
|
+
// Only add if not already added from variables
|
|
222
|
+
if (!result.path.find((p) => p.name === paramName)) {
|
|
223
|
+
result.path.push({
|
|
224
|
+
name: paramName,
|
|
225
|
+
type: "string",
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Query parameters
|
|
233
|
+
if (url.query && url.query.length > 0) {
|
|
234
|
+
for (const q of url.query) {
|
|
235
|
+
if (q.disabled) continue
|
|
236
|
+
result.query.push({
|
|
237
|
+
name: q.key,
|
|
238
|
+
type: "string",
|
|
239
|
+
description: q.description,
|
|
240
|
+
example: q.value,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private parseBody(body: any): ApiEndpointSpec["body"] {
|
|
249
|
+
if (!body) return undefined
|
|
250
|
+
|
|
251
|
+
let example: any
|
|
252
|
+
let description = body.description
|
|
253
|
+
|
|
254
|
+
if (body.mode === "raw") {
|
|
255
|
+
try {
|
|
256
|
+
example = JSON.parse(body.raw)
|
|
257
|
+
} catch {
|
|
258
|
+
example = body.raw
|
|
259
|
+
}
|
|
260
|
+
} else if (body.mode === "formdata" || body.mode === "urlencoded") {
|
|
261
|
+
example = {}
|
|
262
|
+
for (const item of body[body.mode] || []) {
|
|
263
|
+
if (!item.disabled) {
|
|
264
|
+
example[item.key] = item.value
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
description,
|
|
271
|
+
example,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private parseResponses(responses: any[]): { success?: any; errors: any[] } {
|
|
276
|
+
const result: { success?: any; errors: any[] } = { errors: [] }
|
|
277
|
+
|
|
278
|
+
for (const response of responses) {
|
|
279
|
+
let example: any
|
|
280
|
+
try {
|
|
281
|
+
example = JSON.parse(response.body)
|
|
282
|
+
} catch {
|
|
283
|
+
example = response.body
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const apiResponse = {
|
|
287
|
+
status: response.code || 200,
|
|
288
|
+
description: response.name,
|
|
289
|
+
example,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (apiResponse.status >= 200 && apiResponse.status < 300) {
|
|
293
|
+
if (!result.success) result.success = apiResponse
|
|
294
|
+
} else {
|
|
295
|
+
result.errors.push(apiResponse)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SpecraApiSpec } from "../api-parser.types"
|
|
2
|
+
import type { ApiSpecParser } from "./base-parser"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parser for native Specra API format
|
|
6
|
+
* This is a pass-through parser since the input is already in the correct format
|
|
7
|
+
*/
|
|
8
|
+
export class SpecraParser implements ApiSpecParser {
|
|
9
|
+
validate(input: any): boolean {
|
|
10
|
+
return (
|
|
11
|
+
typeof input === "object" &&
|
|
12
|
+
input !== null &&
|
|
13
|
+
"endpoints" in input &&
|
|
14
|
+
Array.isArray(input.endpoints)
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
parse(input: any): SpecraApiSpec {
|
|
19
|
+
if (!this.validate(input)) {
|
|
20
|
+
throw new Error("Invalid Specra API spec format")
|
|
21
|
+
}
|
|
22
|
+
return input as SpecraApiSpec
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getAllDocs, getVersions } from "./mdx"
|
|
2
|
+
|
|
3
|
+
export interface RedirectMapping {
|
|
4
|
+
from: string
|
|
5
|
+
to: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build redirect mappings from all docs' redirect_from frontmatter
|
|
10
|
+
*/
|
|
11
|
+
export async function buildRedirectMappings(): Promise<RedirectMapping[]> {
|
|
12
|
+
const versions = getVersions()
|
|
13
|
+
const redirects: RedirectMapping[] = []
|
|
14
|
+
|
|
15
|
+
for (const version of versions) {
|
|
16
|
+
const docs = await getAllDocs(version)
|
|
17
|
+
|
|
18
|
+
for (const doc of docs) {
|
|
19
|
+
if (doc.meta.redirect_from && Array.isArray(doc.meta.redirect_from)) {
|
|
20
|
+
for (const oldPath of doc.meta.redirect_from) {
|
|
21
|
+
redirects.push({
|
|
22
|
+
from: oldPath,
|
|
23
|
+
to: `/docs/${version}/${doc.slug}`,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return redirects
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find redirect destination for a given path
|
|
35
|
+
*/
|
|
36
|
+
export async function findRedirect(path: string): Promise<string | null> {
|
|
37
|
+
const redirects = await buildRedirectMappings()
|
|
38
|
+
const redirect = redirects.find((r) => r.from === path)
|
|
39
|
+
return redirect ? redirect.to : null
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remark plugin to extract code block meta strings and pass them as props
|
|
3
|
+
* Converts: ```js filename.js
|
|
4
|
+
* Into props: { language: 'js', meta: 'filename.js' }
|
|
5
|
+
*/
|
|
6
|
+
export function remarkCodeMeta() {
|
|
7
|
+
return (tree: any) => {
|
|
8
|
+
const visit = (node: any) => {
|
|
9
|
+
if (node.type === 'code' && node.meta) {
|
|
10
|
+
// Store the meta string in the node's data
|
|
11
|
+
node.data = node.data || {}
|
|
12
|
+
node.data.hProperties = node.data.hProperties || {}
|
|
13
|
+
node.data.hProperties.meta = node.meta
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (node.children) {
|
|
17
|
+
node.children.forEach(visit)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
visit(tree)
|
|
22
|
+
}
|
|
23
|
+
}
|