@furystack/rest 8.0.41 → 8.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 (54) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +37 -1
  3. package/esm/api-endpoint-schema.d.ts +47 -2
  4. package/esm/api-endpoint-schema.d.ts.map +1 -1
  5. package/esm/index.d.ts +4 -1
  6. package/esm/index.d.ts.map +1 -1
  7. package/esm/index.js +4 -1
  8. package/esm/index.js.map +1 -1
  9. package/esm/openapi-document.d.ts +303 -0
  10. package/esm/openapi-document.d.ts.map +1 -0
  11. package/esm/openapi-document.js +2 -0
  12. package/esm/openapi-document.js.map +1 -0
  13. package/esm/openapi-resolve-refs.d.ts +20 -0
  14. package/esm/openapi-resolve-refs.d.ts.map +1 -0
  15. package/esm/openapi-resolve-refs.js +68 -0
  16. package/esm/openapi-resolve-refs.js.map +1 -0
  17. package/esm/openapi-resolve-refs.spec.d.ts +2 -0
  18. package/esm/openapi-resolve-refs.spec.d.ts.map +1 -0
  19. package/esm/openapi-resolve-refs.spec.js +294 -0
  20. package/esm/openapi-resolve-refs.spec.js.map +1 -0
  21. package/esm/openapi-to-rest-api.d.ts +197 -0
  22. package/esm/openapi-to-rest-api.d.ts.map +1 -0
  23. package/esm/openapi-to-rest-api.js +2 -0
  24. package/esm/openapi-to-rest-api.js.map +1 -0
  25. package/esm/openapi-to-rest-api.spec.d.ts +2 -0
  26. package/esm/openapi-to-rest-api.spec.d.ts.map +1 -0
  27. package/esm/openapi-to-rest-api.spec.js +665 -0
  28. package/esm/openapi-to-rest-api.spec.js.map +1 -0
  29. package/esm/openapi-to-schema.d.ts +24 -0
  30. package/esm/openapi-to-schema.d.ts.map +1 -0
  31. package/esm/openapi-to-schema.js +145 -0
  32. package/esm/openapi-to-schema.js.map +1 -0
  33. package/esm/openapi-to-schema.spec.d.ts +2 -0
  34. package/esm/openapi-to-schema.spec.d.ts.map +1 -0
  35. package/esm/openapi-to-schema.spec.js +610 -0
  36. package/esm/openapi-to-schema.spec.js.map +1 -0
  37. package/esm/rest-api.d.ts +21 -4
  38. package/esm/rest-api.d.ts.map +1 -1
  39. package/esm/swagger-document.d.ts +2 -195
  40. package/esm/swagger-document.d.ts.map +1 -1
  41. package/esm/swagger-document.js +2 -1
  42. package/esm/swagger-document.js.map +1 -1
  43. package/package.json +4 -4
  44. package/src/api-endpoint-schema.ts +56 -3
  45. package/src/index.ts +4 -1
  46. package/src/openapi-document.ts +328 -0
  47. package/src/openapi-resolve-refs.spec.ts +324 -0
  48. package/src/openapi-resolve-refs.ts +71 -0
  49. package/src/openapi-to-rest-api.spec.ts +823 -0
  50. package/src/openapi-to-rest-api.ts +263 -0
  51. package/src/openapi-to-schema.spec.ts +707 -0
  52. package/src/openapi-to-schema.ts +163 -0
  53. package/src/rest-api.ts +26 -5
  54. package/src/swagger-document.ts +2 -220
@@ -0,0 +1,263 @@
1
+ import type { OpenApiDocument } from './openapi-document.js'
2
+ import type { RestApi } from './rest-api.js'
3
+
4
+ /**
5
+ * Converts an OpenAPI `{param}` path to FuryStack `:param` format at the type level.
6
+ */
7
+ export type ConvertOpenApiPath<P extends string> = P extends `${infer Before}{${infer Param}}${infer After}`
8
+ ? `${Before}:${Param}${ConvertOpenApiPath<After>}`
9
+ : P
10
+
11
+ // ─── $ref resolution ───────────────────────────────────────────────────────────
12
+
13
+ type ResolveRef<Doc extends OpenApiDocument, Ref extends string> = Ref extends `#/components/schemas/${infer Name}`
14
+ ? Doc['components'] extends { schemas: infer S }
15
+ ? Name extends keyof S
16
+ ? S[Name]
17
+ : unknown
18
+ : unknown
19
+ : Ref extends `#/components/parameters/${infer Name}`
20
+ ? Doc['components'] extends { parameters: infer S }
21
+ ? Name extends keyof S
22
+ ? S[Name]
23
+ : unknown
24
+ : unknown
25
+ : Ref extends `#/components/responses/${infer Name}`
26
+ ? Doc['components'] extends { responses: infer S }
27
+ ? Name extends keyof S
28
+ ? S[Name]
29
+ : unknown
30
+ : unknown
31
+ : Ref extends `#/components/requestBodies/${infer Name}`
32
+ ? Doc['components'] extends { requestBodies: infer S }
33
+ ? Name extends keyof S
34
+ ? S[Name]
35
+ : unknown
36
+ : unknown
37
+ : unknown
38
+
39
+ type ResolveSchemaOrRef<Doc extends OpenApiDocument, S> = S extends { $ref: infer Ref extends string }
40
+ ? ResolveRef<Doc, Ref>
41
+ : S
42
+
43
+ // ─── JSON Schema → TypeScript type mapping ─────────────────────────────────────
44
+
45
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never
46
+
47
+ /**
48
+ * Maps a JSON Schema type keyword to its TypeScript equivalent.
49
+ * Supports primitives, arrays, objects, enum, const, oneOf/anyOf/allOf, nullable, and $ref.
50
+ *
51
+ * The `Doc` parameter is threaded through for `$ref` resolution within schemas.
52
+ */
53
+ export type JsonSchemaToType<S, Doc extends OpenApiDocument = OpenApiDocument> =
54
+ // $ref
55
+ S extends { $ref: infer Ref extends string }
56
+ ? JsonSchemaToType<ResolveRef<Doc, Ref>, Doc>
57
+ : // const literal
58
+ S extends { const: infer C }
59
+ ? C
60
+ : // string enum
61
+ S extends { type: 'string'; enum: ReadonlyArray<infer E> }
62
+ ? E
63
+ : // string
64
+ S extends { type: 'string' }
65
+ ? string
66
+ : // number / integer
67
+ S extends { type: 'number' | 'integer' }
68
+ ? number
69
+ : // boolean
70
+ S extends { type: 'boolean' }
71
+ ? boolean
72
+ : // null
73
+ S extends { type: 'null' }
74
+ ? null
75
+ : // nullable (3.1 style: type is a tuple with 'null')
76
+ S extends { type: readonly [infer T, 'null'] }
77
+ ? JsonSchemaToType<{ type: T }, Doc> | null
78
+ : S extends { type: readonly ['null', infer T] }
79
+ ? JsonSchemaToType<{ type: T }, Doc> | null
80
+ : // array
81
+ S extends { type: 'array'; items: infer Items }
82
+ ? Array<JsonSchemaToType<Items, Doc>>
83
+ : // object with properties + required
84
+ S extends { type: 'object'; properties: infer Props extends Record<string, unknown> }
85
+ ? S extends { required: ReadonlyArray<infer R extends string> }
86
+ ? { [K in keyof Props & R]: JsonSchemaToType<Props[K], Doc> } & {
87
+ [K in Exclude<keyof Props, R>]?: JsonSchemaToType<Props[K], Doc>
88
+ }
89
+ : { [K in keyof Props]?: JsonSchemaToType<Props[K], Doc> }
90
+ : // object without properties
91
+ S extends { type: 'object' }
92
+ ? Record<string, unknown>
93
+ : // allOf → intersection
94
+ S extends { allOf: ReadonlyArray<infer Items> }
95
+ ? UnionToIntersection<JsonSchemaToType<Items, Doc>>
96
+ : // oneOf → union
97
+ S extends { oneOf: ReadonlyArray<infer Items> }
98
+ ? JsonSchemaToType<Items, Doc>
99
+ : // anyOf → union
100
+ S extends { anyOf: ReadonlyArray<infer Items> }
101
+ ? JsonSchemaToType<Items, Doc>
102
+ : unknown
103
+
104
+ // ─── HTTP method utilities ──────────────────────────────────────────────────────
105
+
106
+ type LowercaseHttpMethod = 'get' | 'put' | 'post' | 'delete' | 'patch' | 'head' | 'options' | 'trace'
107
+
108
+ type MethodMap = {
109
+ get: 'GET'
110
+ put: 'PUT'
111
+ post: 'POST'
112
+ delete: 'DELETE'
113
+ patch: 'PATCH'
114
+ head: 'HEAD'
115
+ options: 'OPTIONS'
116
+ trace: 'TRACE'
117
+ }
118
+
119
+ type UppercaseMethod<M extends string> = M extends keyof MethodMap ? MethodMap[M] : never
120
+
121
+ // ─── Path / operation extraction ────────────────────────────────────────────────
122
+
123
+ type PathsWithMethod<T extends OpenApiDocument, M extends LowercaseHttpMethod> = {
124
+ [P in keyof NonNullable<T['paths']> & string]: NonNullable<T['paths']>[P] extends infer PathItem
125
+ ? M extends keyof PathItem
126
+ ? PathItem[M] extends object
127
+ ? P
128
+ : never
129
+ : never
130
+ : never
131
+ }[keyof NonNullable<T['paths']> & string]
132
+
133
+ type GetOperation<T extends OpenApiDocument, P extends string, M extends LowercaseHttpMethod> = NonNullable<
134
+ T['paths']
135
+ >[P] extends infer PathItem
136
+ ? M extends keyof PathItem
137
+ ? PathItem[M]
138
+ : never
139
+ : never
140
+
141
+ // ─── Response / body / parameter extraction ─────────────────────────────────────
142
+
143
+ type ExtractResponseSchema<Doc extends OpenApiDocument, Op> = Op extends { responses: infer R }
144
+ ? R extends { '200': infer Resp200 }
145
+ ? ResolveSchemaOrRef<Doc, Resp200> extends { content: { 'application/json': { schema: infer S } } }
146
+ ? JsonSchemaToType<S, Doc>
147
+ : unknown
148
+ : R extends { '201': infer Resp201 }
149
+ ? ResolveSchemaOrRef<Doc, Resp201> extends { content: { 'application/json': { schema: infer S } } }
150
+ ? JsonSchemaToType<S, Doc>
151
+ : unknown
152
+ : unknown
153
+ : unknown
154
+
155
+ type ExtractRequestBodySchema<Doc extends OpenApiDocument, Op> = Op extends {
156
+ requestBody: infer RB
157
+ }
158
+ ? ResolveSchemaOrRef<Doc, RB> extends { content: { 'application/json': { schema: infer S } } }
159
+ ? JsonSchemaToType<S, Doc>
160
+ : never
161
+ : never
162
+
163
+ type ExtractPathParamsFromPath<P extends string> = P extends `${string}{${infer Param}}${infer Rest}`
164
+ ? { [K in Param | keyof ExtractPathParamsFromPath<Rest>]: string }
165
+ : never
166
+
167
+ type HasPathParams<P extends string> = P extends `${string}{${string}}${string}` ? true : false
168
+
169
+ type ResolvedParam<Doc extends OpenApiDocument, P> = P extends { $ref: infer Ref extends string }
170
+ ? ResolveRef<Doc, Ref>
171
+ : P
172
+
173
+ type ExtractQueryParamEntry<Doc extends OpenApiDocument, P> =
174
+ ResolvedParam<Doc, P> extends { in: 'query'; name: infer N extends string; schema: infer S }
175
+ ? { [K in N]: JsonSchemaToType<S, Doc> }
176
+ : ResolvedParam<Doc, P> extends { in: 'query'; name: infer N extends string }
177
+ ? { [K in N]: string }
178
+ : never
179
+
180
+ type BuildQueryParamsFromTuple<Doc extends OpenApiDocument, T> = T extends readonly [infer Head, ...infer Tail]
181
+ ? [ExtractQueryParamEntry<Doc, Head>] extends [never]
182
+ ? BuildQueryParamsFromTuple<Doc, Tail>
183
+ : ExtractQueryParamEntry<Doc, Head> & BuildQueryParamsFromTuple<Doc, Tail>
184
+ : unknown
185
+
186
+ type HasQueryParams<Doc extends OpenApiDocument, T> = T extends readonly [infer Head, ...infer Tail]
187
+ ? [ExtractQueryParamEntry<Doc, Head>] extends [never]
188
+ ? HasQueryParams<Doc, Tail>
189
+ : true
190
+ : false
191
+
192
+ type BuildQueryParams<Doc extends OpenApiDocument, Op> = Op extends {
193
+ parameters: infer Params extends readonly unknown[]
194
+ }
195
+ ? HasQueryParams<Doc, Params> extends true
196
+ ? BuildQueryParamsFromTuple<Doc, Params>
197
+ : never
198
+ : never
199
+
200
+ // ─── Metadata extraction ────────────────────────────────────────────────────────
201
+
202
+ type ExtractTags<Op> = Op extends { tags: infer T } ? T : never
203
+ type ExtractDeprecated<Op> = Op extends { deprecated: true } ? true : never
204
+ type ExtractSummary<Op> = Op extends { summary: infer S extends string } ? S : never
205
+ type ExtractDescription<Op> = Op extends { description: infer S extends string } ? S : never
206
+
207
+ // ─── Endpoint builder ───────────────────────────────────────────────────────────
208
+
209
+ type BuildEndpoint<T extends OpenApiDocument, P extends string, M extends LowercaseHttpMethod> = {
210
+ result: ExtractResponseSchema<T, GetOperation<T, P, M>>
211
+ } & ([ExtractRequestBodySchema<T, GetOperation<T, P, M>>] extends [never]
212
+ ? unknown
213
+ : { body: ExtractRequestBodySchema<T, GetOperation<T, P, M>> }) &
214
+ (HasPathParams<P> extends true ? { url: ExtractPathParamsFromPath<P> } : unknown) &
215
+ ([BuildQueryParams<T, GetOperation<T, P, M>>] extends [never]
216
+ ? unknown
217
+ : { query: BuildQueryParams<T, GetOperation<T, P, M>> }) &
218
+ ([ExtractTags<GetOperation<T, P, M>>] extends [never] ? unknown : { tags: ExtractTags<GetOperation<T, P, M>> }) &
219
+ ([ExtractDeprecated<GetOperation<T, P, M>>] extends [never] ? unknown : { deprecated: true }) &
220
+ ([ExtractSummary<GetOperation<T, P, M>>] extends [never]
221
+ ? unknown
222
+ : { summary: ExtractSummary<GetOperation<T, P, M>> }) &
223
+ ([ExtractDescription<GetOperation<T, P, M>>] extends [never]
224
+ ? unknown
225
+ : { description: ExtractDescription<GetOperation<T, P, M>> })
226
+
227
+ type EndpointsForMethod<T extends OpenApiDocument, M extends LowercaseHttpMethod> =
228
+ string extends PathsWithMethod<T, M>
229
+ ? never
230
+ : [PathsWithMethod<T, M>] extends [never]
231
+ ? never
232
+ : {
233
+ [P in PathsWithMethod<T, M> as ConvertOpenApiPath<P>]: BuildEndpoint<T, P, M>
234
+ }
235
+
236
+ type CleanObject<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] }
237
+
238
+ /**
239
+ * Extracts a strongly-typed `RestApi` from an OpenAPI document type.
240
+ *
241
+ * Supports `$ref` resolution, `allOf`/`oneOf`/`anyOf` composition, `nullable`, `const`,
242
+ * and metadata extraction (`tags`, `deprecated`, `summary`, `description`).
243
+ *
244
+ * Use with `as const satisfies OpenApiDocument` to get full type inference:
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * import type { OpenApiDocument, OpenApiToRestApi } from '@furystack/rest'
249
+ * import { createClient } from '@furystack/rest-client-fetch'
250
+ *
251
+ * const apiDoc = { ... } as const satisfies OpenApiDocument
252
+ * type MyApi = OpenApiToRestApi<typeof apiDoc>
253
+ * const client = createClient<MyApi>({ endpointUrl: 'https://api.example.com' })
254
+ * ```
255
+ */
256
+ export type OpenApiToRestApi<T extends OpenApiDocument> =
257
+ CleanObject<{
258
+ [M in LowercaseHttpMethod as [EndpointsForMethod<T, M>] extends [never]
259
+ ? never
260
+ : UppercaseMethod<M>]: EndpointsForMethod<T, M>
261
+ }> extends infer R extends RestApi
262
+ ? R
263
+ : never