1import * as NodeServices from "@effect/platform-node/NodeServices"
2import { assert, describe, it } from "@effect/vitest"
3import { Effect, Layer, Stdio, Stream } from "effect"
4import { CliOutput } from "effect/cli"
5import * as Exit from "effect/Exit"
6import { ChildProcess, ChildProcessSpawner } from "effect/process"
7import { TestConsole } from "effect/testing"
8
9const makeLayer = (args: ReadonlyArray<string>) =>
10 Layer.mergeAll(
11 TestConsole.layer,
12 CliOutput.layer(CliOutput.defaultFormatter({ colors: false })),
13 NodeServices.layer,
14 Stdio.layerTest({ args: Effect.succeed(args) })
15 )
16
17const fixturePath = (fileName: string) => `${import.meta.dirname}/fixtures/${fileName}`
18const cliProcessPath = `${import.meta.dirname}/../src/bin.ts`
19
20type CliMainModule = {
21 readonly run: Effect.Effect<void>
22}
23
24const runCli = Effect.fnUntraced(function*(args: ReadonlyArray<string>) {
25 const module = (yield* Effect.promise(
26 () => import(new URL("../src/main.ts", import.meta.url).href)
27 )) as CliMainModule
28
29 return yield* Effect.gen(function*() {
30 const exit = yield* Effect.exit(module.run)
31 const stdoutLines = yield* TestConsole.logLines
32 const stderrLines = yield* TestConsole.errorLines
33 const stdout = stdoutLines.length > 0 ? String(stdoutLines[stdoutLines.length - 1]) : ""
34 const stderr = stderrLines.map(String).join("\n")
35 return { exit, stdout, stderr } as const
36 }).pipe(Effect.provide(makeLayer(args)))
37})
38
39const runCliProcess = Effect.fnUntraced(function*(args: ReadonlyArray<string>) {
40 const handle = yield* ChildProcess.make("node", [cliProcessPath, ...args])
41 return yield* Effect.all({
42 exitCode: handle.exitCode,
43 stdout: Stream.mkString(Stream.decodeText(handle.stdout)),
44 stderr: Stream.mkString(Stream.decodeText(handle.stderr))
45 }, { concurrency: "unbounded" })
46})
47
48describe("openapigen CLI", () => {
49 it.effect("documents --format values and default in --help", () =>
50 Effect.gen(function*() {
51 const result = yield* runCli(["--help"])
52
53 assert.isTrue(Exit.isSuccess(result.exit))
54 assert.include(result.stdout, "--format")
55 assert.include(result.stdout, "httpclient")
56 assert.include(result.stdout, "httpclient-type-only")
57 assert.include(result.stdout, "httpapi")
58 assert.include(result.stdout, "default: httpclient")
59 assert.strictEqual(result.stderr, "")
60 }))
61
62 it.effect("routes --format values and defaults to httpclient", () =>
63 Effect.gen(function*() {
64 const spec = fixturePath("cli-basic-spec.json")
65
66 const defaultResult = yield* runCli(["--spec", spec, "--name", "CliClient"])
67 const httpclientResult = yield* runCli([
68 "--spec",
69 spec,
70 "--name",
71 "CliClient",
72 "--format",
73 "httpclient"
74 ])
75 const typeOnlyResult = yield* runCli([
76 "--spec",
77 spec,
78 "--name",
79 "CliClient",
80 "--format",
81 "httpclient-type-only"
82 ])
83 const httpapiResult = yield* runCli([
84 "--spec",
85 spec,
86 "--name",
87 "CliClient",
88 "--format",
89 "httpapi"
90 ])
91
92 assert.isTrue(Exit.isSuccess(defaultResult.exit))
93 assert.isTrue(Exit.isSuccess(httpclientResult.exit))
94 assert.isTrue(Exit.isSuccess(typeOnlyResult.exit))
95 assert.isTrue(Exit.isSuccess(httpapiResult.exit))
96
97 assert.strictEqual(defaultResult.stderr, "")
98 assert.strictEqual(httpclientResult.stderr, "")
99 assert.strictEqual(typeOnlyResult.stderr, "")
100 assert.strictEqual(httpapiResult.stderr, "")
101
102 assert.strictEqual(defaultResult.stdout, httpclientResult.stdout)
103 assert.include(httpclientResult.stdout, "import * as Schema from \"effect/Schema\"")
104 assert.notInclude(typeOnlyResult.stdout, "import * as Schema from \"effect/Schema\"")
105 assert.include(typeOnlyResult.stdout, "import type * as HttpClient from \"effect/http/HttpClient\"")
106 assert.include(httpapiResult.stdout, "export class CliClient extends HttpApi.make(\"CliClient\")")
107 }))
108
109 it.effect("rejects legacy --type-only flag", () =>
110 Effect.gen(function*() {
111 const spec = fixturePath("cli-basic-spec.json")
112 const result = yield* runCli(["--spec", spec, "--name", "CliClient", "--type-only"])
113
114 assert.isTrue(Exit.isFailure(result.exit))
115 assert.include(result.stdout, "USAGE")
116 assert.include(result.stderr, "Unrecognized flag: --type-only")
117 }))
118
119 it.effect("writes warnings to stderr and keeps stdout as generated source", () =>
120 Effect.gen(function*() {
121 const spec = fixturePath("cli-warning-spec.json")
122 const result = yield* runCli(["--spec", spec, "--name", "CliClient"])
123
124 assert.isTrue(Exit.isSuccess(result.exit))
125 assert.include(result.stdout, "export const make = (")
126 assert.include(result.stderr, "WARNING [cookie-parameter-dropped]")
127 assert.include(result.stderr, "cookie-parameter-dropped")
128 assert.notInclude(result.stdout, "cookie-parameter-dropped")
129 assert.notInclude(result.stderr, "export const make = (")
130 }))
131
132 it.effect("separates generated source and warnings when spawned as a child process", () =>
133 Effect.gen(function*() {
134 const spec = fixturePath("cli-warning-spec.json")
135 const result = yield* runCliProcess(["--spec", spec, "--name", "CliClient"])
136
137 assert.strictEqual(result.exitCode, ChildProcessSpawner.ExitCode(0))
138 assert.include(result.stdout, "export const make = (")
139 assert.notInclude(result.stdout, "WARNING [")
140 assert.notInclude(result.stdout, "cookie-parameter-dropped")
141 assert.include(
142 result.stderr,
143 "WARNING [cookie-parameter-dropped] GET /users/{id} (getUser): Cookie parameter \"session\" was dropped because non-security cookie parameters are not supported."
144 )
145 assert.notInclude(result.stderr, "export const make = (")
146 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)))
147})