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.
Files changed (142) hide show
  1. package/LICENSE.MD +21 -0
  2. package/README.md +157 -0
  3. package/dist/app/api/mdx-watch/route.d.mts +12 -0
  4. package/dist/app/api/mdx-watch/route.d.ts +12 -0
  5. package/dist/app/api/mdx-watch/route.js +98 -0
  6. package/dist/app/api/mdx-watch/route.js.map +1 -0
  7. package/dist/app/api/mdx-watch/route.mjs +71 -0
  8. package/dist/app/api/mdx-watch/route.mjs.map +1 -0
  9. package/dist/app/docs-page.d.mts +32 -0
  10. package/dist/app/docs-page.d.ts +32 -0
  11. package/dist/app/docs-page.js +4072 -0
  12. package/dist/app/docs-page.js.map +1 -0
  13. package/dist/app/docs-page.mjs +14 -0
  14. package/dist/app/docs-page.mjs.map +1 -0
  15. package/dist/app/layout.css +297 -0
  16. package/dist/app/layout.css.map +1 -0
  17. package/dist/app/layout.d.mts +19 -0
  18. package/dist/app/layout.d.ts +19 -0
  19. package/dist/app/layout.js +112 -0
  20. package/dist/app/layout.js.map +1 -0
  21. package/dist/app/layout.mjs +13 -0
  22. package/dist/app/layout.mjs.map +1 -0
  23. package/dist/chunk-DR4EPLMT.mjs +1013 -0
  24. package/dist/chunk-DR4EPLMT.mjs.map +1 -0
  25. package/dist/chunk-INL2EC72.mjs +170 -0
  26. package/dist/chunk-INL2EC72.mjs.map +1 -0
  27. package/dist/chunk-IZFGEAD6.mjs +61 -0
  28. package/dist/chunk-IZFGEAD6.mjs.map +1 -0
  29. package/dist/chunk-KTRWWAGL.mjs +50 -0
  30. package/dist/chunk-KTRWWAGL.mjs.map +1 -0
  31. package/dist/chunk-MZJHJ6BV.mjs +21 -0
  32. package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
  33. package/dist/chunk-NXRIAL7T.mjs +3119 -0
  34. package/dist/chunk-NXRIAL7T.mjs.map +1 -0
  35. package/dist/components/index.d.mts +822 -0
  36. package/dist/components/index.d.ts +822 -0
  37. package/dist/components/index.js +3738 -0
  38. package/dist/components/index.js.map +1 -0
  39. package/dist/components/index.mjs +3627 -0
  40. package/dist/components/index.mjs.map +1 -0
  41. package/dist/index.css +297 -0
  42. package/dist/index.css.map +1 -0
  43. package/dist/index.d.mts +545 -0
  44. package/dist/index.d.ts +545 -0
  45. package/dist/index.js +4648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +347 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/lib/index.d.mts +798 -0
  50. package/dist/lib/index.d.ts +798 -0
  51. package/dist/lib/index.js +1301 -0
  52. package/dist/lib/index.js.map +1 -0
  53. package/dist/lib/index.mjs +89 -0
  54. package/dist/lib/index.mjs.map +1 -0
  55. package/package.json +119 -0
  56. package/src/app/api/mdx-watch/route.ts +86 -0
  57. package/src/app/docs-page.tsx +212 -0
  58. package/src/app/layout.tsx +74 -0
  59. package/src/components/docs/accordion.tsx +53 -0
  60. package/src/components/docs/api/api-endpoint.tsx +59 -0
  61. package/src/components/docs/api/api-params.tsx +43 -0
  62. package/src/components/docs/api/api-playground.tsx +233 -0
  63. package/src/components/docs/api/api-reference.tsx +291 -0
  64. package/src/components/docs/api/api-response.tsx +48 -0
  65. package/src/components/docs/api/index.ts +5 -0
  66. package/src/components/docs/badge.tsx +22 -0
  67. package/src/components/docs/breadcrumb.tsx +51 -0
  68. package/src/components/docs/callout.tsx +109 -0
  69. package/src/components/docs/card.tsx +84 -0
  70. package/src/components/docs/category-index.tsx +112 -0
  71. package/src/components/docs/code-block.tsx +129 -0
  72. package/src/components/docs/columns.tsx +45 -0
  73. package/src/components/docs/componentTextProps.ts +85 -0
  74. package/src/components/docs/dev-mode-badge.tsx +35 -0
  75. package/src/components/docs/doc-layout-wrapper.tsx +54 -0
  76. package/src/components/docs/doc-layout.tsx +111 -0
  77. package/src/components/docs/doc-loading.tsx +15 -0
  78. package/src/components/docs/doc-metadata.tsx +55 -0
  79. package/src/components/docs/doc-navigation.tsx +62 -0
  80. package/src/components/docs/doc-tags.tsx +25 -0
  81. package/src/components/docs/draft-badge.tsx +10 -0
  82. package/src/components/docs/footer.tsx +47 -0
  83. package/src/components/docs/frame.tsx +22 -0
  84. package/src/components/docs/header.tsx +122 -0
  85. package/src/components/docs/hot-reload-indicator.tsx +77 -0
  86. package/src/components/docs/icon.tsx +70 -0
  87. package/src/components/docs/image-card.tsx +95 -0
  88. package/src/components/docs/image.tsx +73 -0
  89. package/src/components/docs/index.ts +48 -0
  90. package/src/components/docs/math.tsx +46 -0
  91. package/src/components/docs/mdx-components.tsx +166 -0
  92. package/src/components/docs/mdx-hot-reload.tsx +37 -0
  93. package/src/components/docs/mermaid.tsx +77 -0
  94. package/src/components/docs/mobile-doc-layout.tsx +115 -0
  95. package/src/components/docs/not-found-content.tsx +55 -0
  96. package/src/components/docs/search-highlight.tsx +127 -0
  97. package/src/components/docs/search-modal.tsx +223 -0
  98. package/src/components/docs/sidebar-skeleton.tsx +39 -0
  99. package/src/components/docs/sidebar.tsx +323 -0
  100. package/src/components/docs/site-banner.tsx +92 -0
  101. package/src/components/docs/steps.tsx +29 -0
  102. package/src/components/docs/tab-context.tsx +28 -0
  103. package/src/components/docs/tab-groups.tsx +50 -0
  104. package/src/components/docs/table-of-contents.tsx +104 -0
  105. package/src/components/docs/tabs.tsx +63 -0
  106. package/src/components/docs/theme-toggle.tsx +39 -0
  107. package/src/components/docs/tooltip.tsx +37 -0
  108. package/src/components/docs/version-switcher.tsx +52 -0
  109. package/src/components/docs/video.tsx +80 -0
  110. package/src/components/global/index.ts +3 -0
  111. package/src/components/global/version-not-found.tsx +26 -0
  112. package/src/components/index.ts +8 -0
  113. package/src/components/theme-provider.tsx +11 -0
  114. package/src/components/ui/badge.tsx +46 -0
  115. package/src/components/ui/button.tsx +60 -0
  116. package/src/components/ui/dialog.tsx +143 -0
  117. package/src/components/ui/index.ts +6 -0
  118. package/src/components/ui/input.tsx +21 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/index.ts +41 -0
  121. package/src/lib/api-parser.types.ts +78 -0
  122. package/src/lib/api.types.ts +202 -0
  123. package/src/lib/category.ts +71 -0
  124. package/src/lib/config.server.ts +170 -0
  125. package/src/lib/config.ts +20 -0
  126. package/src/lib/config.types.ts +295 -0
  127. package/src/lib/dev-utils.ts +75 -0
  128. package/src/lib/index.ts +27 -0
  129. package/src/lib/mdx-cache.ts +200 -0
  130. package/src/lib/mdx.ts +402 -0
  131. package/src/lib/parsers/base-parser.ts +16 -0
  132. package/src/lib/parsers/index.ts +69 -0
  133. package/src/lib/parsers/openapi-parser.ts +251 -0
  134. package/src/lib/parsers/postman-parser.ts +301 -0
  135. package/src/lib/parsers/specra-parser.ts +24 -0
  136. package/src/lib/redirects.ts +40 -0
  137. package/src/lib/remark-code-meta.ts +23 -0
  138. package/src/lib/sidebar-utils.ts +188 -0
  139. package/src/lib/toc.ts +24 -0
  140. package/src/lib/utils.ts +36 -0
  141. package/src/specra.config.json +124 -0
  142. 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
+ }