@effect/openapi-generator 4.0.0-beta.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 +21 -0
- package/dist/JsonSchemaGenerator.d.ts +8 -0
- package/dist/JsonSchemaGenerator.d.ts.map +1 -0
- package/dist/JsonSchemaGenerator.js +75 -0
- package/dist/JsonSchemaGenerator.js.map +1 -0
- package/dist/OpenApiGenerator.d.ts +32 -0
- package/dist/OpenApiGenerator.d.ts.map +1 -0
- package/dist/OpenApiGenerator.js +206 -0
- package/dist/OpenApiGenerator.js.map +1 -0
- package/dist/OpenApiPatch.d.ts +296 -0
- package/dist/OpenApiPatch.d.ts.map +1 -0
- package/dist/OpenApiPatch.js +448 -0
- package/dist/OpenApiPatch.js.map +1 -0
- package/dist/OpenApiTransformer.d.ts +24 -0
- package/dist/OpenApiTransformer.d.ts.map +1 -0
- package/dist/OpenApiTransformer.js +740 -0
- package/dist/OpenApiTransformer.js.map +1 -0
- package/dist/ParsedOperation.d.ts +29 -0
- package/dist/ParsedOperation.d.ts.map +1 -0
- package/dist/ParsedOperation.js +13 -0
- package/dist/ParsedOperation.js.map +1 -0
- package/dist/Utils.d.ts +6 -0
- package/dist/Utils.d.ts.map +1 -0
- package/dist/Utils.js +42 -0
- package/dist/Utils.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +47 -0
- package/dist/main.js.map +1 -0
- package/package.json +67 -0
- package/src/JsonSchemaGenerator.ts +94 -0
- package/src/OpenApiGenerator.ts +309 -0
- package/src/OpenApiPatch.ts +514 -0
- package/src/OpenApiTransformer.ts +954 -0
- package/src/ParsedOperation.ts +43 -0
- package/src/Utils.ts +50 -0
- package/src/bin.ts +10 -0
- package/src/main.ts +68 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI spec patching utilities.
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing and applying JSON Patch documents (RFC 6902) to OpenAPI
|
|
5
|
+
* specs. Supports patches from:
|
|
6
|
+
* - JSON files (.json)
|
|
7
|
+
* - YAML files (.yaml, .yml)
|
|
8
|
+
* - Inline JSON strings
|
|
9
|
+
*
|
|
10
|
+
* @module OpenApiPatch
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as Effect from "effect/Effect"
|
|
14
|
+
import * as FileSystem from "effect/FileSystem"
|
|
15
|
+
import { constFalse, constUndefined } from "effect/Function"
|
|
16
|
+
import * as JsonPatch from "effect/JsonPatch"
|
|
17
|
+
import * as Path from "effect/Path"
|
|
18
|
+
import * as Predicate from "effect/Predicate"
|
|
19
|
+
import * as Schema from "effect/Schema"
|
|
20
|
+
import * as Yaml from "yaml"
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Error Types
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Error thrown when parsing a JSON Patch input fails.
|
|
28
|
+
*
|
|
29
|
+
* This error occurs when:
|
|
30
|
+
* - A patch file cannot be read
|
|
31
|
+
* - JSON or YAML syntax is invalid
|
|
32
|
+
* - The file format is unsupported
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
37
|
+
*
|
|
38
|
+
* const error = new OpenApiPatch.JsonPatchParseError({
|
|
39
|
+
* source: "./patches/fix.json",
|
|
40
|
+
* reason: "Unexpected token at position 42"
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* console.log(error.message)
|
|
44
|
+
* // "Failed to parse patch from ./patches/fix.json: Unexpected token at position 42"
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @since 1.0.0
|
|
48
|
+
* @category errors
|
|
49
|
+
*/
|
|
50
|
+
export class JsonPatchParseError extends Schema.ErrorClass("JsonPatchParseError")({
|
|
51
|
+
_tag: Schema.tag("JsonPatchParseError"),
|
|
52
|
+
source: Schema.String,
|
|
53
|
+
reason: Schema.String
|
|
54
|
+
}) {
|
|
55
|
+
override get message() {
|
|
56
|
+
return `Failed to parse patch from ${this.source}: ${this.reason}`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Error thrown when a parsed value does not conform to the JSON Patch schema.
|
|
62
|
+
*
|
|
63
|
+
* This error occurs when:
|
|
64
|
+
* - The patch is not an array
|
|
65
|
+
* - An operation is missing required fields (op, path)
|
|
66
|
+
* - An operation has an unsupported op value
|
|
67
|
+
* - An add/replace operation is missing the value field
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
72
|
+
*
|
|
73
|
+
* const error = new OpenApiPatch.JsonPatchValidationError({
|
|
74
|
+
* source: "inline",
|
|
75
|
+
* reason: "Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'"
|
|
76
|
+
* })
|
|
77
|
+
*
|
|
78
|
+
* console.log(error.message)
|
|
79
|
+
* // "Invalid JSON Patch from inline: Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'"
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @since 1.0.0
|
|
83
|
+
* @category errors
|
|
84
|
+
*/
|
|
85
|
+
export class JsonPatchValidationError extends Schema.ErrorClass("JsonPatchValidationError")({
|
|
86
|
+
_tag: Schema.tag("JsonPatchValidationError"),
|
|
87
|
+
source: Schema.String,
|
|
88
|
+
reason: Schema.String
|
|
89
|
+
}) {
|
|
90
|
+
override get message() {
|
|
91
|
+
return `Invalid JSON Patch from ${this.source}: ${this.reason}`
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Error thrown when applying a JSON Patch operation fails.
|
|
97
|
+
*
|
|
98
|
+
* This error occurs when:
|
|
99
|
+
* - A path does not exist for remove/replace operations
|
|
100
|
+
* - An array index is out of bounds
|
|
101
|
+
* - The target location is not a valid container
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
106
|
+
*
|
|
107
|
+
* const error = new OpenApiPatch.JsonPatchApplicationError({
|
|
108
|
+
* source: "./patches/fix.json",
|
|
109
|
+
* operationIndex: 2,
|
|
110
|
+
* operation: "remove",
|
|
111
|
+
* path: "/paths/~1users",
|
|
112
|
+
* reason: "Property \"users\" does not exist"
|
|
113
|
+
* })
|
|
114
|
+
*
|
|
115
|
+
* console.log(error.message)
|
|
116
|
+
* // "Failed to apply patch from ./patches/fix.json: operation 2 (remove at /paths/~1users): Property \"users\" does not exist"
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @since 1.0.0
|
|
120
|
+
* @category errors
|
|
121
|
+
*/
|
|
122
|
+
export class JsonPatchApplicationError extends Schema.ErrorClass("JsonPatchApplicationError")({
|
|
123
|
+
_tag: Schema.tag("JsonPatchApplicationError"),
|
|
124
|
+
source: Schema.String,
|
|
125
|
+
operationIndex: Schema.Number,
|
|
126
|
+
operation: Schema.String,
|
|
127
|
+
path: Schema.String,
|
|
128
|
+
reason: Schema.String
|
|
129
|
+
}) {
|
|
130
|
+
override get message() {
|
|
131
|
+
return `Failed to apply patch from ${this.source}: operation ${this.operationIndex} ` +
|
|
132
|
+
`(${this.operation} at ${this.path}): ${this.reason}`
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Error thrown when multiple JSON Patch operations fail.
|
|
138
|
+
*
|
|
139
|
+
* This error aggregates all application errors so users can see every
|
|
140
|
+
* failing operation at once instead of fixing them one at a time.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
145
|
+
*
|
|
146
|
+
* const error = new OpenApiPatch.JsonPatchAggregateError({
|
|
147
|
+
* errors: [
|
|
148
|
+
* new OpenApiPatch.JsonPatchApplicationError({
|
|
149
|
+
* source: "./fix.json",
|
|
150
|
+
* operationIndex: 0,
|
|
151
|
+
* operation: "replace",
|
|
152
|
+
* path: "/info/x",
|
|
153
|
+
* reason: "Property does not exist"
|
|
154
|
+
* }),
|
|
155
|
+
* new OpenApiPatch.JsonPatchApplicationError({
|
|
156
|
+
* source: "./fix.json",
|
|
157
|
+
* operationIndex: 2,
|
|
158
|
+
* operation: "remove",
|
|
159
|
+
* path: "/paths/~1users",
|
|
160
|
+
* reason: "Property does not exist"
|
|
161
|
+
* })
|
|
162
|
+
* ]
|
|
163
|
+
* })
|
|
164
|
+
*
|
|
165
|
+
* console.log(error.message)
|
|
166
|
+
* // "2 patch operations failed:\n 1. ..."
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @since 1.0.0
|
|
170
|
+
* @category errors
|
|
171
|
+
*/
|
|
172
|
+
export class JsonPatchAggregateError extends Schema.ErrorClass("JsonPatchAggregateError")({
|
|
173
|
+
_tag: Schema.tag("JsonPatchAggregateError"),
|
|
174
|
+
errors: Schema.Array(Schema.Unknown)
|
|
175
|
+
}) {
|
|
176
|
+
override get message() {
|
|
177
|
+
const errors = this.errors as ReadonlyArray<JsonPatchApplicationError>
|
|
178
|
+
const count = errors.length
|
|
179
|
+
const plural = count === 1 ? "operation" : "operations"
|
|
180
|
+
const details = errors
|
|
181
|
+
.map((e, i) => ` ${i + 1}. [${e.source}] op ${e.operationIndex} (${e.operation} at ${e.path}): ${e.reason}`)
|
|
182
|
+
.join("\n")
|
|
183
|
+
return `${count} patch ${plural} failed:\n${details}`
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// Schema
|
|
189
|
+
// =============================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Schema for a JSON Patch "add" operation.
|
|
193
|
+
*
|
|
194
|
+
* @since 1.0.0
|
|
195
|
+
* @category schemas
|
|
196
|
+
*/
|
|
197
|
+
export const JsonPatchAdd: Schema.Codec<
|
|
198
|
+
Extract<
|
|
199
|
+
JsonPatch.JsonPatchOperation,
|
|
200
|
+
{ op: "add" }
|
|
201
|
+
>
|
|
202
|
+
> = Schema.Struct({
|
|
203
|
+
op: Schema.Literal("add"),
|
|
204
|
+
path: Schema.String,
|
|
205
|
+
value: Schema.Json,
|
|
206
|
+
description: Schema.optionalKey(Schema.String)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Schema for a JSON Patch "remove" operation.
|
|
211
|
+
*
|
|
212
|
+
* @since 1.0.0
|
|
213
|
+
* @category schemas
|
|
214
|
+
*/
|
|
215
|
+
export const JsonPatchRemove: Schema.Codec<
|
|
216
|
+
Extract<
|
|
217
|
+
JsonPatch.JsonPatchOperation,
|
|
218
|
+
{ op: "remove" }
|
|
219
|
+
>
|
|
220
|
+
> = Schema.Struct({
|
|
221
|
+
op: Schema.Literal("remove"),
|
|
222
|
+
path: Schema.String,
|
|
223
|
+
description: Schema.optionalKey(Schema.String)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Schema for a JSON Patch "replace" operation.
|
|
228
|
+
*
|
|
229
|
+
* @since 1.0.0
|
|
230
|
+
* @category schemas
|
|
231
|
+
*/
|
|
232
|
+
export const JsonPatchReplace: Schema.Codec<
|
|
233
|
+
Extract<
|
|
234
|
+
JsonPatch.JsonPatchOperation,
|
|
235
|
+
{ op: "replace" }
|
|
236
|
+
>
|
|
237
|
+
> = Schema.Struct({
|
|
238
|
+
op: Schema.Literal("replace"),
|
|
239
|
+
path: Schema.String,
|
|
240
|
+
value: Schema.Json,
|
|
241
|
+
description: Schema.optionalKey(Schema.String)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Schema for a single JSON Patch operation.
|
|
246
|
+
*
|
|
247
|
+
* Supports the subset of RFC 6902 operations that Effect's JsonPatch module
|
|
248
|
+
* implements: `add`, `remove`, and `replace`.
|
|
249
|
+
*
|
|
250
|
+
* @since 1.0.0
|
|
251
|
+
* @category schemas
|
|
252
|
+
*/
|
|
253
|
+
export const JsonPatchOperation: Schema.Codec<JsonPatch.JsonPatchOperation> = Schema.Union([
|
|
254
|
+
JsonPatchAdd,
|
|
255
|
+
JsonPatchRemove,
|
|
256
|
+
JsonPatchReplace
|
|
257
|
+
])
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Schema for a JSON Patch document (array of operations).
|
|
261
|
+
*
|
|
262
|
+
* A JSON Patch document is an ordered list of operations to apply to a JSON
|
|
263
|
+
* document. Operations are applied in sequence, with each operation seeing
|
|
264
|
+
* the result of previous operations.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```ts
|
|
268
|
+
* import { Schema } from "effect"
|
|
269
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
270
|
+
*
|
|
271
|
+
* const patch = Schema.decodeUnknownSync(OpenApiPatch.JsonPatchDocument)([
|
|
272
|
+
* { op: "add", path: "/foo", value: "bar" },
|
|
273
|
+
* { op: "remove", path: "/baz" },
|
|
274
|
+
* { op: "replace", path: "/qux", value: 42 }
|
|
275
|
+
* ])
|
|
276
|
+
* ```
|
|
277
|
+
*
|
|
278
|
+
* @since 1.0.0
|
|
279
|
+
* @category schemas
|
|
280
|
+
*/
|
|
281
|
+
export const JsonPatchDocument = Schema.Array(JsonPatchOperation)
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Type for a JSON Patch document.
|
|
285
|
+
*
|
|
286
|
+
* @since 1.0.0
|
|
287
|
+
* @category types
|
|
288
|
+
*/
|
|
289
|
+
export type JsonPatchDocument = typeof JsonPatchDocument.Type
|
|
290
|
+
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// Parsing Functions
|
|
293
|
+
// =============================================================================
|
|
294
|
+
|
|
295
|
+
const decodeJsonPatchDocument = Schema.decodeUnknownEffect(JsonPatchDocument)
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Check if a string looks like it could be a file path.
|
|
299
|
+
*
|
|
300
|
+
* Heuristic: contains path separators or ends with a known extension.
|
|
301
|
+
*/
|
|
302
|
+
const looksLikeFilePath = (input: string): boolean => {
|
|
303
|
+
const trimmed = input.trim()
|
|
304
|
+
if (trimmed.startsWith("[")) return false
|
|
305
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) return true
|
|
306
|
+
if (/\.(json|yaml|yml)$/i.test(trimmed)) return true
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Determine file format from extension.
|
|
312
|
+
*/
|
|
313
|
+
const getFileFormat = Effect.fn(function*(filePath: string) {
|
|
314
|
+
const path = yield* Path.Path
|
|
315
|
+
const { ext } = path.parse(filePath)
|
|
316
|
+
if (ext === ".json") return "json"
|
|
317
|
+
if (ext === ".yaml" || ext === ".yml") return "yaml"
|
|
318
|
+
return undefined
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Check if a file path exists and is a file.
|
|
323
|
+
*/
|
|
324
|
+
const checkFileExists = Effect.fn("checkFileExists")(function*(filePath: string) {
|
|
325
|
+
const fs = yield* FileSystem.FileSystem
|
|
326
|
+
const path = yield* Path.Path
|
|
327
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath)
|
|
328
|
+
const exists = yield* Effect.orElseSucceed(fs.exists(absolutePath), constFalse)
|
|
329
|
+
if (!exists) return false
|
|
330
|
+
const stat = yield* Effect.orElseSucceed(fs.stat(absolutePath), constUndefined)
|
|
331
|
+
return Predicate.isNotUndefined(stat) && stat.type === "File"
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Parse content as JSON.
|
|
336
|
+
*/
|
|
337
|
+
const parseJsonContent = Effect.fnUntraced(function*(content: string, source: string) {
|
|
338
|
+
return yield* Effect.try({
|
|
339
|
+
try: () => JSON.parse(content) as unknown,
|
|
340
|
+
catch: (error) =>
|
|
341
|
+
new JsonPatchParseError({
|
|
342
|
+
source,
|
|
343
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Parse content as YAML.
|
|
350
|
+
*/
|
|
351
|
+
const parseYamlContent = Effect.fnUntraced(function*(content: string, source: string) {
|
|
352
|
+
return yield* Effect.try({
|
|
353
|
+
try: () => Yaml.parse(content) as unknown,
|
|
354
|
+
catch: (error) =>
|
|
355
|
+
new JsonPatchParseError({
|
|
356
|
+
source,
|
|
357
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Read and parse a patch file.
|
|
364
|
+
*/
|
|
365
|
+
const parsePatchFile = Effect.fn("parsePatchFile")(function*(filePath: string) {
|
|
366
|
+
const fs = yield* FileSystem.FileSystem
|
|
367
|
+
const path = yield* Path.Path
|
|
368
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(filePath)
|
|
369
|
+
|
|
370
|
+
const fileFormat = yield* getFileFormat(filePath)
|
|
371
|
+
if (Predicate.isUndefined(fileFormat)) {
|
|
372
|
+
return yield* new JsonPatchParseError({
|
|
373
|
+
source: filePath,
|
|
374
|
+
reason: `Unsupported file format. Expected .json, .yaml, or .yml`
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const content = yield* Effect.mapError(fs.readFileString(absolutePath), (error) =>
|
|
379
|
+
new JsonPatchParseError({
|
|
380
|
+
source: filePath,
|
|
381
|
+
reason: `Failed to read file: ${error.message}`
|
|
382
|
+
}))
|
|
383
|
+
|
|
384
|
+
const parsed = fileFormat === "json"
|
|
385
|
+
? yield* parseJsonContent(content, filePath)
|
|
386
|
+
: yield* parseYamlContent(content, filePath)
|
|
387
|
+
|
|
388
|
+
return yield* Effect.mapError(decodeJsonPatchDocument(parsed), (error) =>
|
|
389
|
+
new JsonPatchValidationError({
|
|
390
|
+
source: filePath,
|
|
391
|
+
reason: error.message
|
|
392
|
+
}))
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Parse inline JSON string as a patch document.
|
|
397
|
+
*/
|
|
398
|
+
const parseInlinePatch = Effect.fn("parseInlinePatch")(function*(input: string) {
|
|
399
|
+
const parsed = yield* parseJsonContent(input, "inline")
|
|
400
|
+
return yield* Effect.mapError(decodeJsonPatchDocument(parsed), (error) =>
|
|
401
|
+
new JsonPatchValidationError({
|
|
402
|
+
source: "inline",
|
|
403
|
+
reason: error.message
|
|
404
|
+
}))
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Parse a JSON Patch from either a file path or inline JSON string.
|
|
409
|
+
*
|
|
410
|
+
* The input is first checked as a file path. If the file exists, it is read
|
|
411
|
+
* and parsed based on its extension (.json, .yaml, .yml). Otherwise, the
|
|
412
|
+
* input is parsed as inline JSON.
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* ```ts
|
|
416
|
+
* import { Effect } from "effect"
|
|
417
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
418
|
+
*
|
|
419
|
+
* // From inline JSON
|
|
420
|
+
* const fromInline = OpenApiPatch.parsePatchInput(
|
|
421
|
+
* '[{"op":"replace","path":"/info/title","value":"My API"}]'
|
|
422
|
+
* )
|
|
423
|
+
*
|
|
424
|
+
* // From file path
|
|
425
|
+
* const fromFile = OpenApiPatch.parsePatchInput("./patches/fix-api.json")
|
|
426
|
+
*
|
|
427
|
+
* const program = Effect.gen(function*() {
|
|
428
|
+
* const patch = yield* fromInline
|
|
429
|
+
* console.log(patch)
|
|
430
|
+
* // [{ op: "replace", path: "/info/title", value: "My API" }]
|
|
431
|
+
* })
|
|
432
|
+
* ```
|
|
433
|
+
*
|
|
434
|
+
* @since 1.0.0
|
|
435
|
+
* @category parsing
|
|
436
|
+
*/
|
|
437
|
+
export const parsePatchInput = Effect.fn("parsePatchInput")(function*(input: string) {
|
|
438
|
+
if (looksLikeFilePath(input)) {
|
|
439
|
+
const exists = yield* checkFileExists(input)
|
|
440
|
+
if (exists) {
|
|
441
|
+
return yield* parsePatchFile(input)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return yield* parseInlinePatch(input)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
// =============================================================================
|
|
448
|
+
// Application Functions
|
|
449
|
+
// =============================================================================
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Apply a sequence of JSON patches to a document.
|
|
453
|
+
*
|
|
454
|
+
* Patches are applied in order, with each patch operating on the result of
|
|
455
|
+
* the previous one. All operations are attempted, and if any fail, the errors
|
|
456
|
+
* are accumulated and reported together so users can fix all issues at once.
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* import { Effect } from "effect"
|
|
461
|
+
* import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
|
|
462
|
+
*
|
|
463
|
+
* const document = { info: { title: "Old Title" }, paths: {} }
|
|
464
|
+
* const patches = [
|
|
465
|
+
* {
|
|
466
|
+
* source: "inline",
|
|
467
|
+
* patch: [{ op: "replace" as const, path: "/info/title", value: "New Title" }]
|
|
468
|
+
* }
|
|
469
|
+
* ]
|
|
470
|
+
*
|
|
471
|
+
* const program = Effect.gen(function*() {
|
|
472
|
+
* const result = yield* OpenApiPatch.applyPatches(patches, document)
|
|
473
|
+
* console.log(result)
|
|
474
|
+
* // { info: { title: "New Title" }, paths: {} }
|
|
475
|
+
* })
|
|
476
|
+
* ```
|
|
477
|
+
*
|
|
478
|
+
* @since 1.0.0
|
|
479
|
+
* @category application
|
|
480
|
+
*/
|
|
481
|
+
export const applyPatches = Effect.fn("applyPatches")(function*(
|
|
482
|
+
patches: ReadonlyArray<{ readonly source: string; readonly patch: JsonPatchDocument }>,
|
|
483
|
+
document: Schema.Json
|
|
484
|
+
) {
|
|
485
|
+
let result: Schema.Json = document
|
|
486
|
+
const errors: Array<JsonPatchApplicationError> = []
|
|
487
|
+
|
|
488
|
+
for (const { source, patch } of patches) {
|
|
489
|
+
for (let i = 0; i < patch.length; i++) {
|
|
490
|
+
const op = patch[i]
|
|
491
|
+
yield* Effect.ignore(Effect.try({
|
|
492
|
+
try: () => {
|
|
493
|
+
result = JsonPatch.apply([op], result)
|
|
494
|
+
},
|
|
495
|
+
catch: (error) =>
|
|
496
|
+
errors.push(
|
|
497
|
+
new JsonPatchApplicationError({
|
|
498
|
+
source,
|
|
499
|
+
operationIndex: i,
|
|
500
|
+
operation: op.op,
|
|
501
|
+
path: op.path,
|
|
502
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
503
|
+
})
|
|
504
|
+
)
|
|
505
|
+
}))
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (errors.length > 0) {
|
|
510
|
+
return yield* new JsonPatchAggregateError({ errors })
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return result
|
|
514
|
+
})
|