Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'

This commit is contained in:
-Puter
2026-07-19 03:25:10 +05:30
parent dd1071cc10
commit 2daf979036
2214 changed files with 673090 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
import * as JsonSchemaGenerator from "@effect/openapi-generator/JsonSchemaGenerator"
import { describe, expect, it } from "@effect/vitest"
describe("JsonSchemaGenerator", () => {
it("schema & no definitions", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string" })
const definitions = {}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// schemas
export type A = string
export const A = Schema.String
`)
})
it("schema & definitions", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/B" })
const definitions = {
B: { type: "string" }
}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// non-recursive definitions
export type B = string
export const B = Schema.String
// schemas
export type A = B
export const A = B
`)
})
it("onEnter strips specified keys", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string", description: "desc", examples: ["ex"] })
const definitions = {}
const result = generator.generate("openapi-3.1", definitions, false, {
onEnter: (js) => {
const out = { ...js }
delete out.examples
return out
}
})
expect(result).toBe(`// schemas
export type A = string
export const A = Schema.String.annotate({ "description": "desc" })
`)
})
it("default preserves all annotations", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string", description: "desc", examples: ["ex"] })
const definitions = {}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// schemas
export type A = string
export const A = Schema.String.annotate({ "description": "desc", "examples": ["ex"] })
`)
})
it("generateHttpApi emits explicit type and const declarations", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { type: "string" })
generator.addSchema("B", {
type: "object",
properties: {
id: {
type: "string"
}
},
required: ["id"],
additionalProperties: false
})
const result = generator.generateHttpApi("openapi-3.1", {})
expect(result).toContain(`export type A = string
export const A = Schema.String`)
expect(result).toContain(`export type B = { readonly "id": string }
export const B = Schema.Struct({ "id": Schema.String })`)
expect(result).not.toContain("Schema.Class<")
expect(result).not.toContain("Schema.Opaque<")
})
it("recursive schema", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/B" })
const definitions = {
B: {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/B"
}
}
},
"required": [
"name",
"children"
],
"additionalProperties": false
}
}
const result = generator.generate("openapi-3.1", definitions, false)
expect(result).toBe(`// recursive definitions
export type B = { readonly "name": string, readonly "children": ReadonlyArray<B> }
export const B = Schema.Struct({ "name": Schema.String, "children": Schema.Array(Schema.suspend((): Schema.Codec<B> => B)) })
// schemas
export type A = B
export const A = B
`)
})
it("hoists recursive definitions referenced by earlier recursive definitions", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/ResourcesNetworkCard" })
const definitions = {
ResourcesNetworkCard: {
type: "object",
properties: {
sriov: {
$ref: "#/components/schemas/ResourcesNetworkCardSRIOV"
}
},
additionalProperties: false
},
ResourcesNetworkCardSRIOV: {
type: "object",
properties: {
vfs: {
type: "array",
items: {
$ref: "#/components/schemas/ResourcesNetworkCard"
}
}
},
additionalProperties: false
}
}
const result = generator.generate("openapi-3.1", definitions, false)
const recursiveDeclaration =
"export const ResourcesNetworkCardSRIOV = Schema.suspend((): Schema.Codec<ResourcesNetworkCardSRIOV> => __recursive_ResourcesNetworkCardSRIOV)"
expect(result).toContain(recursiveDeclaration)
expect(result).toContain("const __recursive_ResourcesNetworkCardSRIOV =")
expect(result.indexOf(recursiveDeclaration)).toBeLessThan(
result.indexOf("export const ResourcesNetworkCard =")
)
expect(result.indexOf("export const ResourcesNetworkCard =")).toBeLessThan(
result.indexOf("const __recursive_ResourcesNetworkCardSRIOV =")
)
})
it("renders recursive definitions before non-recursive references for runtime generation", () => {
const generator = JsonSchemaGenerator.make()
generator.addSchema("A", { $ref: "#/components/schemas/ErrorResponse" })
const definitions = {
InnerErrors: {
type: "object",
properties: {
field: {
type: "string"
}
},
required: ["field"],
additionalProperties: false
},
ErrorDetails: {
oneOf: [
{
type: "object",
additionalProperties: {
$ref: "#/components/schemas/ErrorDetails"
}
},
{
$ref: "#/components/schemas/InnerErrors"
}
]
},
ErrorResponse: {
type: "object",
properties: {
errors: {
$ref: "#/components/schemas/ErrorDetails"
}
},
additionalProperties: false
}
}
const runtimeResult = generator.generate("openapi-3.1", definitions, false)
const recursiveDeclaration =
"export const ErrorDetails = Schema.suspend((): Schema.Codec<ErrorDetails> => __recursive_ErrorDetails)"
expect(runtimeResult).toContain(recursiveDeclaration)
expect(runtimeResult).toContain("const __recursive_ErrorDetails =")
expect(runtimeResult.indexOf(recursiveDeclaration)).toBeLessThan(
runtimeResult.indexOf("export const ErrorResponse =")
)
const httpApiResult = generator.generateHttpApi("openapi-3.1", definitions)
expect(httpApiResult).toContain(recursiveDeclaration)
expect(httpApiResult).toContain("const __recursive_ErrorDetails =")
expect(httpApiResult.indexOf(recursiveDeclaration)).toBeLessThan(
httpApiResult.indexOf("export const ErrorResponse =")
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,147 @@
import * as NodeServices from "@effect/platform-node/NodeServices"
import { assert, describe, it } from "@effect/vitest"
import { Effect, Layer, Stdio, Stream } from "effect"
import * as Exit from "effect/Exit"
import { TestConsole } from "effect/testing"
import { CliOutput } from "effect/unstable/cli"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
const makeLayer = (args: ReadonlyArray<string>) =>
Layer.mergeAll(
TestConsole.layer,
CliOutput.layer(CliOutput.defaultFormatter({ colors: false })),
NodeServices.layer,
Stdio.layerTest({ args: Effect.succeed(args) })
)
const fixturePath = (fileName: string) => `${import.meta.dirname}/fixtures/${fileName}`
const cliProcessPath = `${import.meta.dirname}/../src/bin.ts`
type CliMainModule = {
readonly run: Effect.Effect<void>
}
const runCli = Effect.fnUntraced(function*(args: ReadonlyArray<string>) {
const module = (yield* Effect.promise(
() => import(new URL("../src/main.ts", import.meta.url).href)
)) as CliMainModule
return yield* Effect.gen(function*() {
const exit = yield* Effect.exit(module.run)
const stdoutLines = yield* TestConsole.logLines
const stderrLines = yield* TestConsole.errorLines
const stdout = stdoutLines.length > 0 ? String(stdoutLines[stdoutLines.length - 1]) : ""
const stderr = stderrLines.map(String).join("\n")
return { exit, stdout, stderr } as const
}).pipe(Effect.provide(makeLayer(args)))
})
const runCliProcess = Effect.fnUntraced(function*(args: ReadonlyArray<string>) {
const handle = yield* ChildProcess.make("node", [cliProcessPath, ...args])
return yield* Effect.all({
exitCode: handle.exitCode,
stdout: Stream.mkString(Stream.decodeText(handle.stdout)),
stderr: Stream.mkString(Stream.decodeText(handle.stderr))
}, { concurrency: "unbounded" })
})
describe("openapigen CLI", () => {
it.effect("documents --format values and default in --help", () =>
Effect.gen(function*() {
const result = yield* runCli(["--help"])
assert.isTrue(Exit.isSuccess(result.exit))
assert.include(result.stdout, "--format")
assert.include(result.stdout, "httpclient")
assert.include(result.stdout, "httpclient-type-only")
assert.include(result.stdout, "httpapi")
assert.include(result.stdout, "default: httpclient")
assert.strictEqual(result.stderr, "")
}))
it.effect("routes --format values and defaults to httpclient", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-basic-spec.json")
const defaultResult = yield* runCli(["--spec", spec, "--name", "CliClient"])
const httpclientResult = yield* runCli([
"--spec",
spec,
"--name",
"CliClient",
"--format",
"httpclient"
])
const typeOnlyResult = yield* runCli([
"--spec",
spec,
"--name",
"CliClient",
"--format",
"httpclient-type-only"
])
const httpapiResult = yield* runCli([
"--spec",
spec,
"--name",
"CliClient",
"--format",
"httpapi"
])
assert.isTrue(Exit.isSuccess(defaultResult.exit))
assert.isTrue(Exit.isSuccess(httpclientResult.exit))
assert.isTrue(Exit.isSuccess(typeOnlyResult.exit))
assert.isTrue(Exit.isSuccess(httpapiResult.exit))
assert.strictEqual(defaultResult.stderr, "")
assert.strictEqual(httpclientResult.stderr, "")
assert.strictEqual(typeOnlyResult.stderr, "")
assert.strictEqual(httpapiResult.stderr, "")
assert.strictEqual(defaultResult.stdout, httpclientResult.stdout)
assert.include(httpclientResult.stdout, "import * as Schema from \"effect/Schema\"")
assert.notInclude(typeOnlyResult.stdout, "import * as Schema from \"effect/Schema\"")
assert.include(typeOnlyResult.stdout, "import type * as HttpClient from \"effect/unstable/http/HttpClient\"")
assert.include(httpapiResult.stdout, "export class CliClient extends HttpApi.make(\"CliClient\")")
}))
it.effect("rejects legacy --type-only flag", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-basic-spec.json")
const result = yield* runCli(["--spec", spec, "--name", "CliClient", "--type-only"])
assert.isTrue(Exit.isFailure(result.exit))
assert.include(result.stdout, "USAGE")
assert.include(result.stderr, "Unrecognized flag: --type-only")
}))
it.effect("writes warnings to stderr and keeps stdout as generated source", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-warning-spec.json")
const result = yield* runCli(["--spec", spec, "--name", "CliClient"])
assert.isTrue(Exit.isSuccess(result.exit))
assert.include(result.stdout, "export const make = (")
assert.include(result.stderr, "WARNING [cookie-parameter-dropped]")
assert.include(result.stderr, "cookie-parameter-dropped")
assert.notInclude(result.stdout, "cookie-parameter-dropped")
assert.notInclude(result.stderr, "export const make = (")
}))
it.effect("separates generated source and warnings when spawned as a child process", () =>
Effect.gen(function*() {
const spec = fixturePath("cli-warning-spec.json")
const result = yield* runCliProcess(["--spec", spec, "--name", "CliClient"])
assert.strictEqual(result.exitCode, ChildProcessSpawner.ExitCode(0))
assert.include(result.stdout, "export const make = (")
assert.notInclude(result.stdout, "WARNING [")
assert.notInclude(result.stdout, "cookie-parameter-dropped")
assert.include(
result.stderr,
"WARNING [cookie-parameter-dropped] GET /users/{id} (getUser): Cookie parameter \"session\" was dropped because non-security cookie parameters are not supported."
)
assert.notInclude(result.stderr, "export const make = (")
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)))
})

View File

@@ -0,0 +1,380 @@
import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
import * as NodeServices from "@effect/platform-node/NodeServices"
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Path from "effect/Path"
const testLayer = NodeServices.layer
describe("OpenApiPatch", () => {
describe("parsePatchInput", () => {
describe("inline JSON", () => {
it.effect("parses valid inline JSON patch", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"add\",\"path\":\"/foo\",\"value\":\"bar\"}]"
)
assert.deepStrictEqual(result, [{ op: "add", path: "/foo", value: "bar" }])
}).pipe(Effect.provide(testLayer)))
it.effect("parses inline JSON with multiple operations", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"add\",\"path\":\"/a\",\"value\":1},{\"op\":\"remove\",\"path\":\"/b\"},{\"op\":\"replace\",\"path\":\"/c\",\"value\":true}]"
)
assert.strictEqual(result.length, 3)
assert.strictEqual(result[0].op, "add")
assert.strictEqual(result[1].op, "remove")
assert.strictEqual(result[2].op, "replace")
}).pipe(Effect.provide(testLayer)))
it.effect("fails on invalid JSON syntax", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"add\" \"path\":\"/foo\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on unsupported operation", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"copy\",\"from\":\"/a\",\"path\":\"/b\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on missing path field", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"add\",\"value\":\"test\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on missing value for add operation", () =>
Effect.gen(function*() {
const exit = yield* Effect.exit(
OpenApiPatch.parsePatchInput("[{\"op\":\"add\",\"path\":\"/foo\"}]")
)
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("allows missing value for remove operation", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"remove\",\"path\":\"/foo\"}]"
)
assert.deepStrictEqual(result, [{ op: "remove", path: "/foo" }])
}).pipe(Effect.provide(testLayer)))
})
describe("file paths", () => {
it.effect("parses valid JSON file", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/valid-add.json"
)
const result = yield* OpenApiPatch.parsePatchInput(filePath)
assert.strictEqual(result.length, 1)
assert.strictEqual(result[0].op, "add")
assert.strictEqual(result[0].path, "/info/x-custom")
}).pipe(Effect.provide(testLayer)))
it.effect("parses valid YAML file", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/valid-patch.yaml"
)
const result = yield* OpenApiPatch.parsePatchInput(filePath)
assert.strictEqual(result.length, 2)
assert.strictEqual(result[0].op, "replace")
assert.strictEqual(result[1].op, "add")
}).pipe(Effect.provide(testLayer)))
it.effect("parses multiple operations from JSON file", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/valid-multiple.json"
)
const result = yield* OpenApiPatch.parsePatchInput(filePath)
assert.strictEqual(result.length, 3)
}).pipe(Effect.provide(testLayer)))
it.effect("fails on file with unsupported operation", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/invalid-op.json"
)
const exit = yield* Effect.exit(OpenApiPatch.parsePatchInput(filePath))
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("fails on file with missing path", () =>
Effect.gen(function*() {
const pathService = yield* Path.Path
const filePath = pathService.join(
import.meta.dirname,
"fixtures/patches/missing-path.json"
)
const exit = yield* Effect.exit(OpenApiPatch.parsePatchInput(filePath))
assert.isTrue(Exit.isFailure(exit))
}).pipe(Effect.provide(testLayer)))
it.effect("falls back to inline JSON when file does not exist", () =>
Effect.gen(function*() {
const result = yield* OpenApiPatch.parsePatchInput(
"[{\"op\":\"add\",\"path\":\"/x\",\"value\":1}]"
)
assert.deepStrictEqual(result, [{ op: "add", path: "/x", value: 1 }])
}).pipe(Effect.provide(testLayer)))
})
})
describe("applyPatches", () => {
it.effect("applies single add patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test",
patch: [{ op: "add" as const, path: "/info/version", value: "1.0.0" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, { info: { title: "Test", version: "1.0.0" } })
}))
it.effect("applies single remove patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Test", deprecated: true } }
const patches = [{
source: "test",
patch: [{ op: "remove" as const, path: "/info/deprecated" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, { info: { title: "Test" } })
}))
it.effect("applies single replace patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Old Title" } }
const patches = [{
source: "test",
patch: [{ op: "replace" as const, path: "/info/title", value: "New Title" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, { info: { title: "New Title" } })
}))
it.effect("applies multiple patches in sequence", () =>
Effect.gen(function*() {
const document = { info: { title: "Original", version: "0.0.1" } }
const patches = [
{
source: "patch1",
patch: [{ op: "replace" as const, path: "/info/title", value: "Step 1" }]
},
{
source: "patch2",
patch: [{ op: "replace" as const, path: "/info/version", value: "1.0.0" }]
},
{
source: "patch3",
patch: [{ op: "add" as const, path: "/info/x-patched", value: true }]
}
]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, {
info: { title: "Step 1", version: "1.0.0", "x-patched": true }
})
}))
it.effect("applies multiple operations within a single patch", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" }, paths: {} }
const patches = [{
source: "test",
patch: [
{ op: "replace" as const, path: "/info/title", value: "Updated" },
{ op: "add" as const, path: "/info/version", value: "2.0.0" }
]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.deepStrictEqual(result, {
info: { title: "Updated", version: "2.0.0" },
paths: {}
})
}))
it.effect("fails when path does not exist for replace", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test",
patch: [{ op: "replace" as const, path: "/info/nonexistent", value: "x" }]
}]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
}))
it.effect("fails when path does not exist for remove", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test",
patch: [{ op: "remove" as const, path: "/info/nonexistent" }]
}]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
}))
it.effect("accumulates multiple errors", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [{
source: "test.json",
patch: [
{ op: "replace" as const, path: "/info/nonexistent1", value: "x" },
{ op: "remove" as const, path: "/info/nonexistent2" },
{ op: "replace" as const, path: "/info/nonexistent3", value: "y" }
]
}]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
const failure = exit.cause.reasons[0]
if (failure._tag === "Fail") {
assert.strictEqual(failure.error._tag, "JsonPatchAggregateError")
assert.strictEqual(failure.error.errors.length, 3)
assert.include(failure.error.message, "3 patch operations failed")
assert.include(failure.error.message, "/info/nonexistent1")
assert.include(failure.error.message, "/info/nonexistent2")
assert.include(failure.error.message, "/info/nonexistent3")
}
}
}))
it.effect("accumulates errors across multiple patches", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const patches = [
{
source: "patch1.json",
patch: [{ op: "remove" as const, path: "/info/missing1" }]
},
{
source: "patch2.json",
patch: [{ op: "remove" as const, path: "/info/missing2" }]
}
]
const exit = yield* Effect.exit(OpenApiPatch.applyPatches(patches, document))
assert.isTrue(Exit.isFailure(exit))
if (Exit.isFailure(exit)) {
const failure = exit.cause.reasons[0]
if (failure._tag === "Fail") {
assert.strictEqual(failure.error.errors.length, 2)
assert.include(failure.error.message, "patch1.json")
assert.include(failure.error.message, "patch2.json")
}
}
}))
it.effect("preserves unmodified parts of document", () =>
Effect.gen(function*() {
const document = {
info: { title: "Test", description: "Unchanged" },
paths: { "/users": { get: {} } },
components: { schemas: {} }
}
const patches = [{
source: "test",
patch: [{ op: "replace" as const, path: "/info/title", value: "Changed" }]
}]
const result = yield* OpenApiPatch.applyPatches(patches, document)
assert.strictEqual((result as { info: { description: string } }).info.description, "Unchanged")
assert.deepStrictEqual((result as { paths: object }).paths, { "/users": { get: {} } })
assert.deepStrictEqual((result as { components: object }).components, { schemas: {} })
}))
it.effect("returns original document when no patches provided", () =>
Effect.gen(function*() {
const document = { info: { title: "Test" } }
const result = yield* OpenApiPatch.applyPatches([], document)
assert.deepStrictEqual(result, document)
}))
})
describe("error messages", () => {
it.effect("JsonPatchParseError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchParseError({
source: "./fix.json",
reason: "Unexpected token"
})
assert.strictEqual(
error.message,
"Failed to parse patch from ./fix.json: Unexpected token"
)
}))
it.effect("JsonPatchValidationError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchValidationError({
source: "inline",
reason: "Missing 'path' field"
})
assert.strictEqual(
error.message,
"Invalid JSON Patch from inline: Missing 'path' field"
)
}))
it.effect("JsonPatchApplicationError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchApplicationError({
source: "./fix.json",
operationIndex: 2,
operation: "remove",
path: "/info/x",
reason: "Property does not exist"
})
assert.strictEqual(
error.message,
"Failed to apply patch from ./fix.json: operation 2 (remove at /info/x): Property does not exist"
)
}))
it.effect("JsonPatchAggregateError has descriptive message", () =>
Effect.gen(function*() {
const error = new OpenApiPatch.JsonPatchAggregateError({
errors: [
new OpenApiPatch.JsonPatchApplicationError({
source: "./fix.json",
operationIndex: 0,
operation: "replace",
path: "/info/x",
reason: "Property does not exist"
}),
new OpenApiPatch.JsonPatchApplicationError({
source: "./other.json",
operationIndex: 1,
operation: "remove",
path: "/paths/~1users",
reason: "Path not found"
})
]
})
assert.include(error.message, "2 patch operations failed")
assert.include(error.message, "1. [./fix.json] op 0 (replace at /info/x)")
assert.include(error.message, "2. [./other.json] op 1 (remove at /paths/~1users)")
}))
})
})

View File

@@ -0,0 +1,33 @@
import * as Utils from "@effect/openapi-generator/Utils"
import { describe, expect, it } from "vitest"
describe("Utils", () => {
describe("camelize", () => {
it("removes hyphens and capitalizes following letters", () => {
expect(Utils.camelize("my-operation-id")).toBe("myOperationId")
})
it("removes slashes and capitalizes following letters", () => {
expect(Utils.camelize("my/operation/id")).toBe("myOperationId")
})
it("handles numbers", () => {
expect(Utils.camelize("operation-2")).toBe("operation2")
})
it("removes leading numbers", () => {
expect(Utils.camelize("2operation")).toBe("operation")
})
it("handles empty string", () => {
expect(Utils.camelize("")).toBe("")
})
})
describe("identifier", () => {
it("capitalizes camelized string", () => {
expect(Utils.identifier("my-operation")).toBe("MyOperation")
expect(Utils.identifier("operation-2")).toBe("Operation2")
})
})
})

View File

@@ -0,0 +1,35 @@
{
"openapi": "3.0.0",
"info": {
"title": "CLI Basic API",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"operationId": "listUsers",
"responses": {
"200": {
"description": "List users",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@@ -0,0 +1,57 @@
{
"openapi": "3.0.0",
"info": {
"title": "CLI Warning API",
"version": "1.0.0"
},
"paths": {
"/users/{id}": {
"get": {
"operationId": "getUser",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "session",
"in": "cookie",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "User response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": ["id"],
"additionalProperties": false
}
}
}
}
}
}
}
},
"components": {
"schemas": {},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@@ -0,0 +1,7 @@
[
{
"op": "copy",
"from": "/info/title",
"path": "/info/x-title"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"op": "add",
"value": "test"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"op": "add",
"path": "/info/x-missing"
}
]

View File

@@ -0,0 +1,8 @@
[
{
"op": "add",
"path": "/info/x-custom",
"value": "custom-value",
"description": "Add custom extension"
}
]

View File

@@ -0,0 +1,17 @@
[
{
"op": "replace",
"path": "/info/title",
"value": "My Custom API"
},
{
"op": "add",
"path": "/info/x-generator",
"value": "effect-openapi"
},
{
"op": "replace",
"path": "/info/version",
"value": "2.0.0"
}
]

View File

@@ -0,0 +1,6 @@
- op: replace
path: /info/title
value: YAML Patched Title
- op: add
path: /info/x-yaml-patch
value: true

View File

@@ -0,0 +1,6 @@
[
{
"op": "remove",
"path": "/paths/~1deprecated"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"op": "replace",
"path": "/info/title",
"value": "Updated API Title"
}
]