1/**
2 * Command-line entry point for generating Effect HTTP clients or HttpApi
3 * definitions from an OpenAPI specification.
4 *
5 * The CLI reads a spec file, optionally applies JSON patches in order, selects
6 * the generator layer for the requested output format, reports generation
7 * warnings to stderr, and writes the generated source to stdout.
8 *
9 * @since 4.0.0
10 */
11import * as CliError from "effect/cli/CliError"
12import * as Command from "effect/cli/Command"
13import * as Flag from "effect/cli/Flag"
14import * as Console from "effect/Console"
15import * as Effect from "effect/Effect"
16import type { OpenAPISpec } from "effect/http-api/OpenApi"
17import type * as Schema from "effect/Schema"
18import * as OpenApiGenerator from "./OpenApiGenerator.ts"
19import * as OpenApiPatch from "./OpenApiPatch.ts"
20
21const spec = Flag.FileParse("spec").pipe(
22 Flag.withAlias("s"),
23 Flag.withDescription("The OpenAPI spec file to generate output from")
24)
25
26const name = Flag.String("name").pipe(
27 Flag.withAlias("n"),
28 Flag.withDescription("The name of the generated output"),
29 Flag.withDefault("Client")
30)
31
32const format = Flag.Literals("format", ["httpclient", "httpclient-type-only", "httpapi"] as const).pipe(
33 Flag.withAlias("f"),
34 Flag.withDescription(
35 "Output format to generate: httpclient | httpclient-type-only | httpapi (default: httpclient)"
36 ),
37 Flag.withDefault("httpclient")
38)
39
40const patch = Flag.String("patch").pipe(
41 Flag.withAlias("p"),
42 Flag.withDescription(
43 "JSON patch to apply to OpenAPI spec before generation. " +
44 "Can be a file path (.json, .yaml, .yml) or inline JSON array. " +
45 "Multiple patches are applied in order."
46 ),
47 Flag.between(0, Infinity)
48)
49
50const root = Command.make("openapigen", { spec, format, name, patch }).pipe(
51 Command.withHandler(Effect.fnUntraced(function*({ name, spec, format, patch }) {
52 let patchedSpec: Schema.Json = spec as Schema.Json
53
54 if (patch.length > 0) {
55 const parsedPatches = yield* Effect.forEach(
56 patch,
57 (input) =>
58 OpenApiPatch.parsePatchInput(input).pipe(
59 Effect.map((p) => ({ source: input, patch: p })),
60 Effect.mapError((error) => new CliError.UserError({ cause: error }))
61 )
62 )
63 patchedSpec = yield* OpenApiPatch.applyPatches(parsedPatches, patchedSpec).pipe(
64 Effect.mapError((error) => new CliError.UserError({ cause: error }))
65 )
66 }
67
68 const generator = yield* OpenApiGenerator.OpenApiGenerator
69 const warnings: Array<OpenApiGenerator.OpenApiGeneratorWarning> = []
70 const source = yield* generator.generate(patchedSpec as unknown as OpenAPISpec, {
71 name,
72 format,
73 onWarning: (warning) => {
74 warnings.push(warning)
75 }
76 })
77 yield* Effect.forEach(
78 warnings,
79 (warning) => Console.error(formatWarning(warning)),
80 { discard: true }
81 )
82 return yield* Console.log(source)
83 })),
84 Command.provide(({ format }) =>
85 format === "httpclient-type-only"
86 ? OpenApiGenerator.layerTransformerTs
87 : OpenApiGenerator.layerTransformerSchema
88 )
89)
90
91/**
92 * Runs the OpenAPI generator command-line program.
93 *
94 * **Details**
95 *
96 * The command reads an OpenAPI specification, optionally applies JSON patches,
97 * generates source code in the selected format, writes any generation warnings
98 * to stderr, and prints the generated source to stdout.
99 *
100 * @category running
101 * @since 4.0.0
102 */
103export const run: Effect.Effect<void, CliError.CliError, Command.Environment> = Command.run(root, {
104 version: "0.0.0"
105})
106
107const formatWarning = (warning: OpenApiGenerator.OpenApiGeneratorWarning): string => {
108 const context = [
109 warning.method?.toUpperCase(),
110 warning.path,
111 warning.operationId ? `(${warning.operationId})` : undefined
112 ].filter((value): value is string => value !== undefined)
113 return context.length > 0
114 ? `WARNING [${warning.code}] ${context.join(" ")}: ${warning.message}`
115 : `WARNING [${warning.code}] ${warning.message}`
116}